mirror of
https://github.com/vector-im/element-web.git
synced 2024-11-15 20:54:59 +08:00
Fix thread summary layout for narrow right panel timeline (#7838)
This commit is contained in:
parent
5e76d988ca
commit
d8ac7cf202
@ -58,6 +58,12 @@ limitations under the License.
|
|||||||
padding-right: 36px;
|
padding-right: 36px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_EventTile:not([data-layout="bubble"]) .mx_ThreadInfo {
|
||||||
|
margin-left: 36px;
|
||||||
|
margin-right: 0;
|
||||||
|
max-width: min(calc(100% - 36px), 600px);
|
||||||
|
}
|
||||||
|
|
||||||
.mx_GroupLayout .mx_EventTile > .mx_SenderProfile {
|
.mx_GroupLayout .mx_EventTile > .mx_SenderProfile {
|
||||||
margin-left: 36px;
|
margin-left: 36px;
|
||||||
}
|
}
|
||||||
|
@ -723,7 +723,7 @@ $left-gutter: 64px;
|
|||||||
.mx_ThreadInfo {
|
.mx_ThreadInfo {
|
||||||
min-width: 267px;
|
min-width: 267px;
|
||||||
max-width: min(calc(100% - 64px), 600px);
|
max-width: min(calc(100% - 64px), 600px);
|
||||||
width: auto;
|
width: fit-content;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
position: relative;
|
position: relative;
|
||||||
background-color: $system;
|
background-color: $system;
|
||||||
@ -777,6 +777,12 @@ $left-gutter: 64px;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_MessagePanel_narrow .mx_ThreadInfo {
|
||||||
|
min-width: initial;
|
||||||
|
max-width: initial;
|
||||||
|
width: initial;
|
||||||
|
}
|
||||||
|
|
||||||
.mx_ThreadInfo_content {
|
.mx_ThreadInfo_content {
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
Copyright 2016 OpenMarket Ltd
|
Copyright 2016 OpenMarket Ltd
|
||||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
Copyright 2019 - 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React, { createRef } from 'react';
|
||||||
import { Filter } from 'matrix-js-sdk/src/filter';
|
import { Filter } from 'matrix-js-sdk/src/filter';
|
||||||
import { EventTimelineSet, IRoomTimelineData } from "matrix-js-sdk/src/models/event-timeline-set";
|
import { EventTimelineSet, IRoomTimelineData } from "matrix-js-sdk/src/models/event-timeline-set";
|
||||||
import { Direction } from "matrix-js-sdk/src/models/event-timeline";
|
import { Direction } from "matrix-js-sdk/src/models/event-timeline";
|
||||||
@ -35,6 +35,7 @@ import TimelinePanel from "./TimelinePanel";
|
|||||||
import Spinner from "../views/elements/Spinner";
|
import Spinner from "../views/elements/Spinner";
|
||||||
import { Layout } from "../../settings/enums/Layout";
|
import { Layout } from "../../settings/enums/Layout";
|
||||||
import RoomContext, { TimelineRenderingType } from '../../contexts/RoomContext';
|
import RoomContext, { TimelineRenderingType } from '../../contexts/RoomContext';
|
||||||
|
import Measured from '../views/elements/Measured';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
roomId: string;
|
roomId: string;
|
||||||
@ -44,6 +45,7 @@ interface IProps {
|
|||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
timelineSet: EventTimelineSet;
|
timelineSet: EventTimelineSet;
|
||||||
|
narrow: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -51,14 +53,17 @@ interface IState {
|
|||||||
*/
|
*/
|
||||||
@replaceableComponent("structures.FilePanel")
|
@replaceableComponent("structures.FilePanel")
|
||||||
class FilePanel extends React.Component<IProps, IState> {
|
class FilePanel extends React.Component<IProps, IState> {
|
||||||
|
static contextType = RoomContext;
|
||||||
|
|
||||||
// This is used to track if a decrypted event was a live event and should be
|
// This is used to track if a decrypted event was a live event and should be
|
||||||
// added to the timeline.
|
// added to the timeline.
|
||||||
private decryptingEvents = new Set<string>();
|
private decryptingEvents = new Set<string>();
|
||||||
public noRoom: boolean;
|
public noRoom: boolean;
|
||||||
static contextType = RoomContext;
|
private card = createRef<HTMLDivElement>();
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
timelineSet: null,
|
timelineSet: null,
|
||||||
|
narrow: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
private onRoomTimeline = (
|
private onRoomTimeline = (
|
||||||
@ -184,6 +189,10 @@ class FilePanel extends React.Component<IProps, IState> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private onMeasurement = (narrow: boolean): void => {
|
||||||
|
this.setState({ narrow });
|
||||||
|
};
|
||||||
|
|
||||||
public async updateTimelineSet(roomId: string): Promise<void> {
|
public async updateTimelineSet(roomId: string): Promise<void> {
|
||||||
const client = MatrixClientPeg.get();
|
const client = MatrixClientPeg.get();
|
||||||
const room = client.getRoom(roomId);
|
const room = client.getRoom(roomId);
|
||||||
@ -256,12 +265,18 @@ class FilePanel extends React.Component<IProps, IState> {
|
|||||||
<RoomContext.Provider value={{
|
<RoomContext.Provider value={{
|
||||||
...this.context,
|
...this.context,
|
||||||
timelineRenderingType: TimelineRenderingType.File,
|
timelineRenderingType: TimelineRenderingType.File,
|
||||||
|
narrow: this.state.narrow,
|
||||||
}}>
|
}}>
|
||||||
<BaseCard
|
<BaseCard
|
||||||
className="mx_FilePanel"
|
className="mx_FilePanel"
|
||||||
onClose={this.props.onClose}
|
onClose={this.props.onClose}
|
||||||
withoutScrollContainer
|
withoutScrollContainer
|
||||||
|
ref={this.card}
|
||||||
>
|
>
|
||||||
|
<Measured
|
||||||
|
sensor={this.card.current}
|
||||||
|
onMeasurement={this.onMeasurement}
|
||||||
|
/>
|
||||||
<DesktopBuildsNotice isRoomEncrypted={isRoomEncrypted} kind={WarningKind.Files} />
|
<DesktopBuildsNotice isRoomEncrypted={isRoomEncrypted} kind={WarningKind.Files} />
|
||||||
<TimelinePanel
|
<TimelinePanel
|
||||||
manageReadReceipts={false}
|
manageReadReceipts={false}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Copyright 2016 - 2021 The Matrix.org Foundation C.I.C.
|
Copyright 2016 - 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
@ -16,6 +16,7 @@ limitations under the License.
|
|||||||
|
|
||||||
import React, { createRef, KeyboardEvent, ReactNode, SyntheticEvent, TransitionEvent } from 'react';
|
import React, { createRef, KeyboardEvent, ReactNode, SyntheticEvent, TransitionEvent } from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
|
import classNames from 'classnames';
|
||||||
import { Room } from 'matrix-js-sdk/src/models/room';
|
import { Room } from 'matrix-js-sdk/src/models/room';
|
||||||
import { EventType } from 'matrix-js-sdk/src/@types/event';
|
import { EventType } from 'matrix-js-sdk/src/@types/event';
|
||||||
import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
|
import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
|
||||||
@ -1018,11 +1019,15 @@ export default class MessagePanel extends React.Component<IProps, IState> {
|
|||||||
/>;
|
/>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const classes = classNames(this.props.className, {
|
||||||
|
"mx_MessagePanel_narrow": this.context.narrow,
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<ScrollPanel
|
<ScrollPanel
|
||||||
ref={this.scrollPanel}
|
ref={this.scrollPanel}
|
||||||
className={this.props.className}
|
className={classes}
|
||||||
onScroll={this.props.onScroll}
|
onScroll={this.props.onScroll}
|
||||||
onUserScroll={this.props.onUserScroll}
|
onUserScroll={this.props.onUserScroll}
|
||||||
onFillRequest={this.props.onFillRequest}
|
onFillRequest={this.props.onFillRequest}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Copyright 2016, 2019, 2021 The Matrix.org Foundation C.I.C.
|
Copyright 2016 - 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
@ -25,17 +25,37 @@ import TimelinePanel from "./TimelinePanel";
|
|||||||
import Spinner from "../views/elements/Spinner";
|
import Spinner from "../views/elements/Spinner";
|
||||||
import { Layout } from "../../settings/enums/Layout";
|
import { Layout } from "../../settings/enums/Layout";
|
||||||
import RoomContext, { TimelineRenderingType } from "../../contexts/RoomContext";
|
import RoomContext, { TimelineRenderingType } from "../../contexts/RoomContext";
|
||||||
|
import Measured from "../views/elements/Measured";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
onClose(): void;
|
onClose(): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
narrow: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Component which shows the global notification list using a TimelinePanel
|
* Component which shows the global notification list using a TimelinePanel
|
||||||
*/
|
*/
|
||||||
@replaceableComponent("structures.NotificationPanel")
|
@replaceableComponent("structures.NotificationPanel")
|
||||||
export default class NotificationPanel extends React.PureComponent<IProps> {
|
export default class NotificationPanel extends React.PureComponent<IProps, IState> {
|
||||||
static contextType = RoomContext;
|
static contextType = RoomContext;
|
||||||
|
|
||||||
|
private card = React.createRef<HTMLDivElement>();
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
narrow: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private onMeasurement = (narrow: boolean): void => {
|
||||||
|
this.setState({ narrow });
|
||||||
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const emptyState = (<div className="mx_RightPanel_empty mx_NotificationPanel_empty">
|
const emptyState = (<div className="mx_RightPanel_empty mx_NotificationPanel_empty">
|
||||||
<h2>{ _t("You're all caught up") }</h2>
|
<h2>{ _t("You're all caught up") }</h2>
|
||||||
@ -65,8 +85,13 @@ export default class NotificationPanel extends React.PureComponent<IProps> {
|
|||||||
return <RoomContext.Provider value={{
|
return <RoomContext.Provider value={{
|
||||||
...this.context,
|
...this.context,
|
||||||
timelineRenderingType: TimelineRenderingType.Notification,
|
timelineRenderingType: TimelineRenderingType.Notification,
|
||||||
|
narrow: this.state.narrow,
|
||||||
}}>
|
}}>
|
||||||
<BaseCard className="mx_NotificationPanel" onClose={this.props.onClose} withoutScrollContainer>
|
<BaseCard className="mx_NotificationPanel" onClose={this.props.onClose} withoutScrollContainer>
|
||||||
|
<Measured
|
||||||
|
sensor={this.card.current}
|
||||||
|
onMeasurement={this.onMeasurement}
|
||||||
|
/>
|
||||||
{ content }
|
{ content }
|
||||||
</BaseCard>
|
</BaseCard>
|
||||||
</RoomContext.Provider>;
|
</RoomContext.Provider>;
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
Copyright 2017 Vector Creations Ltd
|
Copyright 2017 Vector Creations Ltd
|
||||||
Copyright 2018, 2019 New Vector Ltd
|
Copyright 2018, 2019 New Vector Ltd
|
||||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
Copyright 2019 - 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
@ -104,6 +104,7 @@ import { ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload";
|
|||||||
import { JoinRoomPayload } from "../../dispatcher/payloads/JoinRoomPayload";
|
import { JoinRoomPayload } from "../../dispatcher/payloads/JoinRoomPayload";
|
||||||
import { DoAfterSyncPreparedPayload } from '../../dispatcher/payloads/DoAfterSyncPreparedPayload';
|
import { DoAfterSyncPreparedPayload } from '../../dispatcher/payloads/DoAfterSyncPreparedPayload';
|
||||||
import FileDropTarget from './FileDropTarget';
|
import FileDropTarget from './FileDropTarget';
|
||||||
|
import Measured from '../views/elements/Measured';
|
||||||
|
|
||||||
const DEBUG = false;
|
const DEBUG = false;
|
||||||
let debuglog = function(msg: string) {};
|
let debuglog = function(msg: string) {};
|
||||||
@ -211,6 +212,7 @@ export interface IRoomState {
|
|||||||
timelineRenderingType: TimelineRenderingType;
|
timelineRenderingType: TimelineRenderingType;
|
||||||
threadId?: string;
|
threadId?: string;
|
||||||
liveTimeline?: EventTimeline;
|
liveTimeline?: EventTimeline;
|
||||||
|
narrow: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@replaceableComponent("structures.RoomView")
|
@replaceableComponent("structures.RoomView")
|
||||||
@ -226,6 +228,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||||||
private roomView = createRef<HTMLElement>();
|
private roomView = createRef<HTMLElement>();
|
||||||
private searchResultsPanel = createRef<ScrollPanel>();
|
private searchResultsPanel = createRef<ScrollPanel>();
|
||||||
private messagePanel: TimelinePanel;
|
private messagePanel: TimelinePanel;
|
||||||
|
private roomViewBody = createRef<HTMLDivElement>();
|
||||||
|
|
||||||
static contextType = MatrixClientContext;
|
static contextType = MatrixClientContext;
|
||||||
|
|
||||||
@ -271,6 +274,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||||||
mainSplitContentType: MainSplitContentType.Timeline,
|
mainSplitContentType: MainSplitContentType.Timeline,
|
||||||
timelineRenderingType: TimelineRenderingType.Room,
|
timelineRenderingType: TimelineRenderingType.Room,
|
||||||
liveTimeline: undefined,
|
liveTimeline: undefined,
|
||||||
|
narrow: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.dispatcherRef = dis.register(this.onAction);
|
this.dispatcherRef = dis.register(this.onAction);
|
||||||
@ -1730,6 +1734,10 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||||||
TimelineRenderingType.Room,
|
TimelineRenderingType.Room,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
private onMeasurement = (narrow: boolean): void => {
|
||||||
|
this.setState({ narrow });
|
||||||
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
if (!this.state.room) {
|
if (!this.state.room) {
|
||||||
const loading = !this.state.matrixClientIsReady || this.state.roomLoading || this.state.peekLoading;
|
const loading = !this.state.matrixClientIsReady || this.state.roomLoading || this.state.peekLoading;
|
||||||
@ -2084,6 +2092,10 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||||||
|
|
||||||
// Decide what to show in the main split
|
// Decide what to show in the main split
|
||||||
let mainSplitBody = <React.Fragment>
|
let mainSplitBody = <React.Fragment>
|
||||||
|
<Measured
|
||||||
|
sensor={this.roomViewBody.current}
|
||||||
|
onMeasurement={this.onMeasurement}
|
||||||
|
/>
|
||||||
{ auxPanel }
|
{ auxPanel }
|
||||||
<div className={timelineClasses}>
|
<div className={timelineClasses}>
|
||||||
<FileDropTarget parent={this.roomView.current} onFileDrop={this.onFileDrop} />
|
<FileDropTarget parent={this.roomView.current} onFileDrop={this.onFileDrop} />
|
||||||
@ -2148,7 +2160,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||||||
excludedRightPanelPhaseButtons={excludedRightPanelPhaseButtons}
|
excludedRightPanelPhaseButtons={excludedRightPanelPhaseButtons}
|
||||||
/>
|
/>
|
||||||
<MainSplit panel={rightPanel} resizeNotifier={this.props.resizeNotifier}>
|
<MainSplit panel={rightPanel} resizeNotifier={this.props.resizeNotifier}>
|
||||||
<div className="mx_RoomView_body" data-layout={this.state.layout}>
|
<div className="mx_RoomView_body" ref={this.roomViewBody} data-layout={this.state.layout}>
|
||||||
{ mainSplitBody }
|
{ mainSplitBody }
|
||||||
</div>
|
</div>
|
||||||
</MainSplit>
|
</MainSplit>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
Copyright 2021 - 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
@ -37,6 +37,7 @@ import RoomContext, { TimelineRenderingType } from '../../contexts/RoomContext';
|
|||||||
import TimelinePanel from './TimelinePanel';
|
import TimelinePanel from './TimelinePanel';
|
||||||
import { Layout } from '../../settings/enums/Layout';
|
import { Layout } from '../../settings/enums/Layout';
|
||||||
import { RoomPermalinkCreator } from '../../utils/permalinks/Permalinks';
|
import { RoomPermalinkCreator } from '../../utils/permalinks/Permalinks';
|
||||||
|
import Measured from '../views/elements/Measured';
|
||||||
|
|
||||||
async function getThreadTimelineSet(
|
async function getThreadTimelineSet(
|
||||||
client: MatrixClient,
|
client: MatrixClient,
|
||||||
@ -213,12 +214,14 @@ const EmptyThread: React.FC<EmptyThreadIProps> = ({ filterOption, showAllThreads
|
|||||||
const ThreadPanel: React.FC<IProps> = ({ roomId, onClose, permalinkCreator }) => {
|
const ThreadPanel: React.FC<IProps> = ({ roomId, onClose, permalinkCreator }) => {
|
||||||
const mxClient = useContext(MatrixClientContext);
|
const mxClient = useContext(MatrixClientContext);
|
||||||
const roomContext = useContext(RoomContext);
|
const roomContext = useContext(RoomContext);
|
||||||
const [filterOption, setFilterOption] = useState<ThreadFilterType>(ThreadFilterType.All);
|
const timelinePanel = useRef<TimelinePanel>();
|
||||||
const ref = useRef<TimelinePanel>();
|
const card = useRef<HTMLDivElement>();
|
||||||
|
|
||||||
|
const [filterOption, setFilterOption] = useState<ThreadFilterType>(ThreadFilterType.All);
|
||||||
const [room, setRoom] = useState(mxClient.getRoom(roomId));
|
const [room, setRoom] = useState(mxClient.getRoom(roomId));
|
||||||
const [threadCount, setThreadCount] = useState<number>(0);
|
const [threadCount, setThreadCount] = useState<number>(0);
|
||||||
const [timelineSet, setTimelineSet] = useState<EventTimelineSet | null>(null);
|
const [timelineSet, setTimelineSet] = useState<EventTimelineSet | null>(null);
|
||||||
|
const [narrow, setNarrow] = useState<boolean>(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setRoom(mxClient.getRoom(roomId));
|
setRoom(mxClient.getRoom(roomId));
|
||||||
@ -257,7 +260,7 @@ const ThreadPanel: React.FC<IProps> = ({ roomId, onClose, permalinkCreator }) =>
|
|||||||
}
|
}
|
||||||
|
|
||||||
function refreshTimeline() {
|
function refreshTimeline() {
|
||||||
if (timelineSet) ref.current.refreshTimeline();
|
if (timelineSet) timelinePanel.current.refreshTimeline();
|
||||||
}
|
}
|
||||||
|
|
||||||
setThreadCount(room.threads.size);
|
setThreadCount(room.threads.size);
|
||||||
@ -278,14 +281,15 @@ const ThreadPanel: React.FC<IProps> = ({ roomId, onClose, permalinkCreator }) =>
|
|||||||
}, [mxClient, room, filterOption]);
|
}, [mxClient, room, filterOption]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (timelineSet) ref.current.refreshTimeline();
|
if (timelineSet) timelinePanel.current.refreshTimeline();
|
||||||
}, [timelineSet, ref]);
|
}, [timelineSet, timelinePanel]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RoomContext.Provider value={{
|
<RoomContext.Provider value={{
|
||||||
...roomContext,
|
...roomContext,
|
||||||
timelineRenderingType: TimelineRenderingType.ThreadsList,
|
timelineRenderingType: TimelineRenderingType.ThreadsList,
|
||||||
showHiddenEventsInTimeline: true,
|
showHiddenEventsInTimeline: true,
|
||||||
|
narrow,
|
||||||
}}>
|
}}>
|
||||||
<BaseCard
|
<BaseCard
|
||||||
header={<ThreadPanelHeader
|
header={<ThreadPanelHeader
|
||||||
@ -296,10 +300,15 @@ const ThreadPanel: React.FC<IProps> = ({ roomId, onClose, permalinkCreator }) =>
|
|||||||
className="mx_ThreadPanel"
|
className="mx_ThreadPanel"
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
withoutScrollContainer={true}
|
withoutScrollContainer={true}
|
||||||
|
ref={card}
|
||||||
>
|
>
|
||||||
|
<Measured
|
||||||
|
sensor={card.current}
|
||||||
|
onMeasurement={setNarrow}
|
||||||
|
/>
|
||||||
{ timelineSet && (
|
{ timelineSet && (
|
||||||
<TimelinePanel
|
<TimelinePanel
|
||||||
ref={ref}
|
ref={timelinePanel}
|
||||||
showReadReceipts={false} // No RR support in thread's MVP
|
showReadReceipts={false} // No RR support in thread's MVP
|
||||||
manageReadReceipts={false} // No RR support in thread's MVP
|
manageReadReceipts={false} // No RR support in thread's MVP
|
||||||
manageReadMarkers={false} // No RM support in thread's MVP
|
manageReadMarkers={false} // No RM support in thread's MVP
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
Copyright 2021 - 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
@ -49,6 +49,7 @@ import { ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload";
|
|||||||
import FileDropTarget from "./FileDropTarget";
|
import FileDropTarget from "./FileDropTarget";
|
||||||
import { getKeyBindingsManager } from "../../KeyBindingsManager";
|
import { getKeyBindingsManager } from "../../KeyBindingsManager";
|
||||||
import { KeyBindingAction } from "../../accessibility/KeyboardShortcuts";
|
import { KeyBindingAction } from "../../accessibility/KeyboardShortcuts";
|
||||||
|
import Measured from '../views/elements/Measured';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
room: Room;
|
room: Room;
|
||||||
@ -60,12 +61,14 @@ interface IProps {
|
|||||||
initialEvent?: MatrixEvent;
|
initialEvent?: MatrixEvent;
|
||||||
isInitialEventHighlighted?: boolean;
|
isInitialEventHighlighted?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
thread?: Thread;
|
thread?: Thread;
|
||||||
lastThreadReply?: MatrixEvent;
|
lastThreadReply?: MatrixEvent;
|
||||||
layout: Layout;
|
layout: Layout;
|
||||||
editState?: EditorStateTransfer;
|
editState?: EditorStateTransfer;
|
||||||
replyToEvent?: MatrixEvent;
|
replyToEvent?: MatrixEvent;
|
||||||
|
narrow: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@replaceableComponent("structures.ThreadView")
|
@replaceableComponent("structures.ThreadView")
|
||||||
@ -74,14 +77,16 @@ export default class ThreadView extends React.Component<IProps, IState> {
|
|||||||
public context!: React.ContextType<typeof RoomContext>;
|
public context!: React.ContextType<typeof RoomContext>;
|
||||||
|
|
||||||
private dispatcherRef: string;
|
private dispatcherRef: string;
|
||||||
private timelinePanelRef = createRef<TimelinePanel>();
|
|
||||||
private cardRef = createRef<HTMLDivElement>();
|
|
||||||
private readonly layoutWatcherRef: string;
|
private readonly layoutWatcherRef: string;
|
||||||
|
private timelinePanel = createRef<TimelinePanel>();
|
||||||
|
private card = createRef<HTMLDivElement>();
|
||||||
|
|
||||||
constructor(props: IProps) {
|
constructor(props: IProps) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
layout: SettingsStore.getValue("layout"),
|
layout: SettingsStore.getValue("layout"),
|
||||||
|
narrow: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.layoutWatcherRef = SettingsStore.watchSetting("layout", null, (...[,,, value]) =>
|
this.layoutWatcherRef = SettingsStore.watchSetting("layout", null, (...[,,, value]) =>
|
||||||
@ -131,7 +136,7 @@ export default class ThreadView extends React.Component<IProps, IState> {
|
|||||||
editState: payload.event ? new EditorStateTransfer(payload.event) : null,
|
editState: payload.event ? new EditorStateTransfer(payload.event) : null,
|
||||||
}, () => {
|
}, () => {
|
||||||
if (payload.event) {
|
if (payload.event) {
|
||||||
this.timelinePanelRef.current?.scrollToEventIfNeeded(payload.event.getId());
|
this.timelinePanel.current?.scrollToEventIfNeeded(payload.event.getId());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
@ -181,7 +186,7 @@ export default class ThreadView extends React.Component<IProps, IState> {
|
|||||||
if (!thread.initialEventsFetched) {
|
if (!thread.initialEventsFetched) {
|
||||||
await thread.fetchInitialEvents();
|
await thread.fetchInitialEvents();
|
||||||
}
|
}
|
||||||
this.timelinePanelRef.current?.refreshTimeline();
|
this.timelinePanel.current?.refreshTimeline();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -209,6 +214,10 @@ export default class ThreadView extends React.Component<IProps, IState> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private onMeasurement = (narrow: boolean): void => {
|
||||||
|
this.setState({ narrow });
|
||||||
|
};
|
||||||
|
|
||||||
private onKeyDown = (ev: KeyboardEvent) => {
|
private onKeyDown = (ev: KeyboardEvent) => {
|
||||||
let handled = false;
|
let handled = false;
|
||||||
|
|
||||||
@ -230,15 +239,6 @@ export default class ThreadView extends React.Component<IProps, IState> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private renderThreadViewHeader = (): JSX.Element => {
|
|
||||||
return <div className="mx_ThreadPanel__header">
|
|
||||||
<span>{ _t("Thread") }</span>
|
|
||||||
<ThreadListContextMenu
|
|
||||||
mxEvent={this.props.mxEvent}
|
|
||||||
permalinkCreator={this.props.permalinkCreator} />
|
|
||||||
</div>;
|
|
||||||
};
|
|
||||||
|
|
||||||
private onPaginationRequest = async (
|
private onPaginationRequest = async (
|
||||||
timelineWindow: TimelineWindow | null,
|
timelineWindow: TimelineWindow | null,
|
||||||
direction = Direction.Backward,
|
direction = Direction.Backward,
|
||||||
@ -284,6 +284,15 @@ export default class ThreadView extends React.Component<IProps, IState> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private renderThreadViewHeader = (): JSX.Element => {
|
||||||
|
return <div className="mx_ThreadPanel__header">
|
||||||
|
<span>{ _t("Thread") }</span>
|
||||||
|
<ThreadListContextMenu
|
||||||
|
mxEvent={this.props.mxEvent}
|
||||||
|
permalinkCreator={this.props.permalinkCreator} />
|
||||||
|
</div>;
|
||||||
|
};
|
||||||
|
|
||||||
public render(): JSX.Element {
|
public render(): JSX.Element {
|
||||||
const highlightedEventId = this.props.isInitialEventHighlighted
|
const highlightedEventId = this.props.isInitialEventHighlighted
|
||||||
? this.props.initialEvent?.getId()
|
? this.props.initialEvent?.getId()
|
||||||
@ -303,20 +312,24 @@ export default class ThreadView extends React.Component<IProps, IState> {
|
|||||||
timelineRenderingType: TimelineRenderingType.Thread,
|
timelineRenderingType: TimelineRenderingType.Thread,
|
||||||
threadId: this.state.thread?.id,
|
threadId: this.state.thread?.id,
|
||||||
liveTimeline: this.state?.thread?.timelineSet?.getLiveTimeline(),
|
liveTimeline: this.state?.thread?.timelineSet?.getLiveTimeline(),
|
||||||
|
narrow: this.state.narrow,
|
||||||
}}>
|
}}>
|
||||||
|
|
||||||
<BaseCard
|
<BaseCard
|
||||||
className="mx_ThreadView mx_ThreadPanel"
|
className="mx_ThreadView mx_ThreadPanel"
|
||||||
onClose={this.props.onClose}
|
onClose={this.props.onClose}
|
||||||
withoutScrollContainer={true}
|
withoutScrollContainer={true}
|
||||||
header={this.renderThreadViewHeader()}
|
header={this.renderThreadViewHeader()}
|
||||||
ref={this.cardRef}
|
ref={this.card}
|
||||||
onKeyDown={this.onKeyDown}
|
onKeyDown={this.onKeyDown}
|
||||||
>
|
>
|
||||||
|
<Measured
|
||||||
|
sensor={this.card.current}
|
||||||
|
onMeasurement={this.onMeasurement}
|
||||||
|
/>
|
||||||
{ this.state.thread && <div className="mx_ThreadView_timelinePanelWrapper">
|
{ this.state.thread && <div className="mx_ThreadView_timelinePanelWrapper">
|
||||||
<FileDropTarget parent={this.cardRef.current} onFileDrop={this.onFileDrop} />
|
<FileDropTarget parent={this.card.current} onFileDrop={this.onFileDrop} />
|
||||||
<TimelinePanel
|
<TimelinePanel
|
||||||
ref={this.timelinePanelRef}
|
ref={this.timelinePanel}
|
||||||
showReadReceipts={false} // Hide the read receipts
|
showReadReceipts={false} // Hide the read receipts
|
||||||
// until homeservers speak threads language
|
// until homeservers speak threads language
|
||||||
manageReadReceipts={true}
|
manageReadReceipts={true}
|
||||||
|
71
src/components/views/elements/Measured.tsx
Normal file
71
src/components/views/elements/Measured.tsx
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import UIStore, { UI_EVENTS } from "../../../stores/UIStore";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
sensor: Element;
|
||||||
|
breakpoint: number;
|
||||||
|
onMeasurement(narrow: boolean): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class Measured extends React.PureComponent<IProps> {
|
||||||
|
private static instanceCount = 0;
|
||||||
|
private readonly instanceId: number;
|
||||||
|
|
||||||
|
static defaultProps = {
|
||||||
|
breakpoint: 500,
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.instanceId = Measured.instanceCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
UIStore.instance.on(`Measured${this.instanceId}`, this.onResize);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps: Readonly<IProps>) {
|
||||||
|
const previous = prevProps.sensor;
|
||||||
|
const current = this.props.sensor;
|
||||||
|
if (previous === current) return;
|
||||||
|
if (previous) {
|
||||||
|
UIStore.instance.stopTrackingElementDimensions(`Measured${this.instanceId}`);
|
||||||
|
}
|
||||||
|
if (current) {
|
||||||
|
UIStore.instance.trackElementDimensions(`Measured${this.instanceId}`,
|
||||||
|
this.props.sensor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
UIStore.instance.off(`Measured${this.instanceId}`, this.onResize);
|
||||||
|
UIStore.instance.stopTrackingElementDimensions(`Measured${this.instanceId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
private onResize = (type: UI_EVENTS, entry: ResizeObserverEntry) => {
|
||||||
|
if (type !== UI_EVENTS.Resize) return;
|
||||||
|
this.props.onMeasurement(entry.contentRect.width <= this.props.breakpoint);
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
Copyright 2021 - 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
@ -42,6 +42,7 @@ import UploadBar from '../../structures/UploadBar';
|
|||||||
import SettingsStore from '../../../settings/SettingsStore';
|
import SettingsStore from '../../../settings/SettingsStore';
|
||||||
import JumpToBottomButton from '../rooms/JumpToBottomButton';
|
import JumpToBottomButton from '../rooms/JumpToBottomButton';
|
||||||
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
|
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
|
||||||
|
import Measured from '../elements/Measured';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
room: Room;
|
room: Room;
|
||||||
@ -55,6 +56,7 @@ interface IProps {
|
|||||||
showComposer?: boolean;
|
showComposer?: boolean;
|
||||||
composerRelation?: IEventRelation;
|
composerRelation?: IEventRelation;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
thread?: Thread;
|
thread?: Thread;
|
||||||
editState?: EditorStateTransfer;
|
editState?: EditorStateTransfer;
|
||||||
@ -63,6 +65,7 @@ interface IState {
|
|||||||
isInitialEventHighlighted?: boolean;
|
isInitialEventHighlighted?: boolean;
|
||||||
layout: Layout;
|
layout: Layout;
|
||||||
atEndOfLiveTimeline: boolean;
|
atEndOfLiveTimeline: boolean;
|
||||||
|
narrow: boolean;
|
||||||
|
|
||||||
// settings:
|
// settings:
|
||||||
showReadReceipts?: boolean;
|
showReadReceipts?: boolean;
|
||||||
@ -74,7 +77,8 @@ export default class TimelineCard extends React.Component<IProps, IState> {
|
|||||||
|
|
||||||
private dispatcherRef: string;
|
private dispatcherRef: string;
|
||||||
private layoutWatcherRef: string;
|
private layoutWatcherRef: string;
|
||||||
private timelinePanelRef: React.RefObject<TimelinePanel> = React.createRef();
|
private timelinePanel = React.createRef<TimelinePanel>();
|
||||||
|
private card = React.createRef<HTMLDivElement>();
|
||||||
private roomStoreToken: EventSubscription;
|
private roomStoreToken: EventSubscription;
|
||||||
private readReceiptsSettingWatcher: string;
|
private readReceiptsSettingWatcher: string;
|
||||||
|
|
||||||
@ -84,6 +88,7 @@ export default class TimelineCard extends React.Component<IProps, IState> {
|
|||||||
showReadReceipts: SettingsStore.getValue("showReadReceipts", props.room.roomId),
|
showReadReceipts: SettingsStore.getValue("showReadReceipts", props.room.roomId),
|
||||||
layout: SettingsStore.getValue("layout"),
|
layout: SettingsStore.getValue("layout"),
|
||||||
atEndOfLiveTimeline: true,
|
atEndOfLiveTimeline: true,
|
||||||
|
narrow: false,
|
||||||
};
|
};
|
||||||
this.readReceiptsSettingWatcher = null;
|
this.readReceiptsSettingWatcher = null;
|
||||||
}
|
}
|
||||||
@ -134,7 +139,7 @@ export default class TimelineCard extends React.Component<IProps, IState> {
|
|||||||
editState: payload.event ? new EditorStateTransfer(payload.event) : null,
|
editState: payload.event ? new EditorStateTransfer(payload.event) : null,
|
||||||
}, () => {
|
}, () => {
|
||||||
if (payload.event) {
|
if (payload.event) {
|
||||||
this.timelinePanelRef.current?.scrollToEventIfNeeded(payload.event.getId());
|
this.timelinePanel.current?.scrollToEventIfNeeded(payload.event.getId());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
@ -157,7 +162,7 @@ export default class TimelineCard extends React.Component<IProps, IState> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
private onScroll = (): void => {
|
private onScroll = (): void => {
|
||||||
const timelinePanel = this.timelinePanelRef.current;
|
const timelinePanel = this.timelinePanel.current;
|
||||||
if (!timelinePanel) return;
|
if (!timelinePanel) return;
|
||||||
if (timelinePanel.isAtEndOfLiveTimeline()) {
|
if (timelinePanel.isAtEndOfLiveTimeline()) {
|
||||||
this.setState({
|
this.setState({
|
||||||
@ -170,6 +175,10 @@ export default class TimelineCard extends React.Component<IProps, IState> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private onMeasurement = (narrow: boolean): void => {
|
||||||
|
this.setState({ narrow });
|
||||||
|
};
|
||||||
|
|
||||||
private jumpToLiveTimeline = () => {
|
private jumpToLiveTimeline = () => {
|
||||||
if (this.state.initialEventId && this.state.isInitialEventHighlighted) {
|
if (this.state.initialEventId && this.state.isInitialEventHighlighted) {
|
||||||
// If we were viewing a highlighted event, firing view_room without
|
// If we were viewing a highlighted event, firing view_room without
|
||||||
@ -181,7 +190,7 @@ export default class TimelineCard extends React.Component<IProps, IState> {
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// Otherwise we have to jump manually
|
// Otherwise we have to jump manually
|
||||||
this.timelinePanelRef.current?.jumpToLiveTimeline();
|
this.timelinePanel.current?.jumpToLiveTimeline();
|
||||||
dis.fire(Action.FocusSendMessageComposer);
|
dis.fire(Action.FocusSendMessageComposer);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -210,22 +219,30 @@ export default class TimelineCard extends React.Component<IProps, IState> {
|
|||||||
/>);
|
/>);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isUploading = ContentMessages.sharedInstance().getCurrentUploads(this.props.composerRelation).length > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RoomContext.Provider value={{
|
<RoomContext.Provider value={{
|
||||||
...this.context,
|
...this.context,
|
||||||
timelineRenderingType: this.props.timelineRenderingType ?? this.context.timelineRenderingType,
|
timelineRenderingType: this.props.timelineRenderingType ?? this.context.timelineRenderingType,
|
||||||
liveTimeline: this.props.timelineSet.getLiveTimeline(),
|
liveTimeline: this.props.timelineSet.getLiveTimeline(),
|
||||||
|
narrow: this.state.narrow,
|
||||||
}}>
|
}}>
|
||||||
<BaseCard
|
<BaseCard
|
||||||
className={this.props.classNames}
|
className={this.props.classNames}
|
||||||
onClose={this.props.onClose}
|
onClose={this.props.onClose}
|
||||||
withoutScrollContainer={true}
|
withoutScrollContainer={true}
|
||||||
header={this.renderTimelineCardHeader()}
|
header={this.renderTimelineCardHeader()}
|
||||||
|
ref={this.card}
|
||||||
>
|
>
|
||||||
|
<Measured
|
||||||
|
sensor={this.card.current}
|
||||||
|
onMeasurement={this.onMeasurement}
|
||||||
|
/>
|
||||||
<div className="mx_TimelineCard_timeline">
|
<div className="mx_TimelineCard_timeline">
|
||||||
{ jumpToBottom }
|
{ jumpToBottom }
|
||||||
<TimelinePanel
|
<TimelinePanel
|
||||||
ref={this.timelinePanelRef}
|
ref={this.timelinePanel}
|
||||||
showReadReceipts={this.state.showReadReceipts}
|
showReadReceipts={this.state.showReadReceipts}
|
||||||
manageReadReceipts={true}
|
manageReadReceipts={true}
|
||||||
manageReadMarkers={false} // No RM support in the TimelineCard
|
manageReadMarkers={false} // No RM support in the TimelineCard
|
||||||
@ -249,7 +266,7 @@ export default class TimelineCard extends React.Component<IProps, IState> {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{ ContentMessages.sharedInstance().getCurrentUploads(this.props.composerRelation).length > 0 && (
|
{ isUploading && (
|
||||||
<UploadBar room={this.props.room} relation={this.props.composerRelation} />
|
<UploadBar room={this.props.room} relation={this.props.composerRelation} />
|
||||||
) }
|
) }
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Copyright 2015-2021 The Matrix.org Foundation C.I.C.
|
Copyright 2015 - 2022 The Matrix.org Foundation C.I.C.
|
||||||
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
@ -677,6 +677,12 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||||||
<p className="mx_ThreadSummaryIcon">{ _t("From a thread") }</p>
|
<p className="mx_ThreadSummaryIcon">{ _t("From a thread") }</p>
|
||||||
);
|
);
|
||||||
} else if (this.state.threadReplyCount && this.props.mxEvent.isThreadRoot) {
|
} else if (this.state.threadReplyCount && this.props.mxEvent.isThreadRoot) {
|
||||||
|
let count: string | number = this.state.threadReplyCount;
|
||||||
|
if (!this.context.narrow) {
|
||||||
|
count = _t("%(count)s reply", {
|
||||||
|
count: this.state.threadReplyCount,
|
||||||
|
});
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<CardContext.Consumer>
|
<CardContext.Consumer>
|
||||||
{ context =>
|
{ context =>
|
||||||
@ -687,9 +693,7 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span className="mx_ThreadInfo_threads-amount">
|
<span className="mx_ThreadInfo_threads-amount">
|
||||||
{ _t("%(count)s reply", {
|
{ count }
|
||||||
count: this.state.threadReplyCount,
|
|
||||||
}) }
|
|
||||||
</span>
|
</span>
|
||||||
{ this.renderThreadLastMessagePreview() }
|
{ this.renderThreadLastMessagePreview() }
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Copyright 2015-2021 The Matrix.org Foundation C.I.C.
|
Copyright 2015 - 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
@ -54,7 +54,6 @@ import { ButtonEvent } from '../elements/AccessibleButton';
|
|||||||
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
|
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
|
||||||
|
|
||||||
let instanceCount = 0;
|
let instanceCount = 0;
|
||||||
const NARROW_MODE_BREAKPOINT = 500;
|
|
||||||
|
|
||||||
interface ISendButtonProps {
|
interface ISendButtonProps {
|
||||||
onClick: (ev: ButtonEvent) => void;
|
onClick: (ev: ButtonEvent) => void;
|
||||||
@ -88,7 +87,6 @@ interface IState {
|
|||||||
haveRecording: boolean;
|
haveRecording: boolean;
|
||||||
recordingTimeLeftSeconds?: number;
|
recordingTimeLeftSeconds?: number;
|
||||||
me?: RoomMember;
|
me?: RoomMember;
|
||||||
narrowMode?: boolean;
|
|
||||||
isMenuOpen: boolean;
|
isMenuOpen: boolean;
|
||||||
isStickerPickerOpen: boolean;
|
isStickerPickerOpen: boolean;
|
||||||
showStickersButton: boolean;
|
showStickersButton: boolean;
|
||||||
@ -165,10 +163,9 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
|||||||
|
|
||||||
private onResize = (type: UI_EVENTS, entry: ResizeObserverEntry) => {
|
private onResize = (type: UI_EVENTS, entry: ResizeObserverEntry) => {
|
||||||
if (type === UI_EVENTS.Resize) {
|
if (type === UI_EVENTS.Resize) {
|
||||||
const narrowMode = entry.contentRect.width <= NARROW_MODE_BREAKPOINT;
|
const { narrow } = this.context;
|
||||||
this.setState({
|
this.setState({
|
||||||
narrowMode,
|
isMenuOpen: !narrow ? false : this.state.isMenuOpen,
|
||||||
isMenuOpen: !narrowMode ? false : this.state.isMenuOpen,
|
|
||||||
isStickerPickerOpen: false,
|
isStickerPickerOpen: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -476,11 +473,10 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
|||||||
isMenuOpen={this.state.isMenuOpen}
|
isMenuOpen={this.state.isMenuOpen}
|
||||||
isStickerPickerOpen={this.state.isStickerPickerOpen}
|
isStickerPickerOpen={this.state.isStickerPickerOpen}
|
||||||
menuPosition={menuPosition}
|
menuPosition={menuPosition}
|
||||||
narrowMode={this.state.narrowMode}
|
|
||||||
relation={this.props.relation}
|
relation={this.props.relation}
|
||||||
onRecordStartEndClick={() => {
|
onRecordStartEndClick={() => {
|
||||||
this.voiceRecordingButton.current?.onRecordStartEndClick();
|
this.voiceRecordingButton.current?.onRecordStartEndClick();
|
||||||
if (this.state.narrowMode) {
|
if (this.context.narrow) {
|
||||||
this.toggleButtonMenu();
|
this.toggleButtonMenu();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
@ -44,7 +44,6 @@ interface IProps {
|
|||||||
isMenuOpen: boolean;
|
isMenuOpen: boolean;
|
||||||
isStickerPickerOpen: boolean;
|
isStickerPickerOpen: boolean;
|
||||||
menuPosition: AboveLeftOf;
|
menuPosition: AboveLeftOf;
|
||||||
narrowMode?: boolean;
|
|
||||||
onRecordStartEndClick: () => void;
|
onRecordStartEndClick: () => void;
|
||||||
relation?: IEventRelation;
|
relation?: IEventRelation;
|
||||||
setStickerPickerOpen: (isStickerPickerOpen: boolean) => void;
|
setStickerPickerOpen: (isStickerPickerOpen: boolean) => void;
|
||||||
@ -58,7 +57,7 @@ export const OverflowMenuContext = createContext<OverflowMenuCloser | null>(null
|
|||||||
|
|
||||||
const MessageComposerButtons: React.FC<IProps> = (props: IProps) => {
|
const MessageComposerButtons: React.FC<IProps> = (props: IProps) => {
|
||||||
const matrixClient: MatrixClient = useContext(MatrixClientContext);
|
const matrixClient: MatrixClient = useContext(MatrixClientContext);
|
||||||
const { room, roomId } = useContext(RoomContext);
|
const { room, roomId, narrow } = useContext(RoomContext);
|
||||||
|
|
||||||
if (props.haveRecording) {
|
if (props.haveRecording) {
|
||||||
return null;
|
return null;
|
||||||
@ -66,14 +65,14 @@ const MessageComposerButtons: React.FC<IProps> = (props: IProps) => {
|
|||||||
|
|
||||||
let mainButtons: ReactElement[];
|
let mainButtons: ReactElement[];
|
||||||
let moreButtons: ReactElement[];
|
let moreButtons: ReactElement[];
|
||||||
if (props.narrowMode) {
|
if (narrow) {
|
||||||
mainButtons = [
|
mainButtons = [
|
||||||
emojiButton(props),
|
emojiButton(props),
|
||||||
];
|
];
|
||||||
moreButtons = [
|
moreButtons = [
|
||||||
uploadButton(props, roomId),
|
uploadButton(props, roomId),
|
||||||
showStickersButton(props),
|
showStickersButton(props),
|
||||||
voiceRecordingButton(props),
|
voiceRecordingButton(props, narrow),
|
||||||
pollButton(room, props.relation),
|
pollButton(room, props.relation),
|
||||||
showLocationButton(props, room, roomId, matrixClient),
|
showLocationButton(props, room, roomId, matrixClient),
|
||||||
];
|
];
|
||||||
@ -84,7 +83,7 @@ const MessageComposerButtons: React.FC<IProps> = (props: IProps) => {
|
|||||||
];
|
];
|
||||||
moreButtons = [
|
moreButtons = [
|
||||||
showStickersButton(props),
|
showStickersButton(props),
|
||||||
voiceRecordingButton(props),
|
voiceRecordingButton(props, narrow),
|
||||||
pollButton(room, props.relation),
|
pollButton(room, props.relation),
|
||||||
showLocationButton(props, room, roomId, matrixClient),
|
showLocationButton(props, room, roomId, matrixClient),
|
||||||
];
|
];
|
||||||
@ -260,10 +259,10 @@ function showStickersButton(props: IProps): ReactElement {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function voiceRecordingButton(props: IProps): ReactElement {
|
function voiceRecordingButton(props: IProps, narrow: boolean): ReactElement {
|
||||||
// XXX: recording UI does not work well in narrow mode, so hide for now
|
// XXX: recording UI does not work well in narrow mode, so hide for now
|
||||||
return (
|
return (
|
||||||
props.narrowMode
|
narrow
|
||||||
? null
|
? null
|
||||||
: <CollapsibleButton
|
: <CollapsibleButton
|
||||||
key="voice_message_send"
|
key="voice_message_send"
|
||||||
|
@ -64,6 +64,7 @@ const RoomContext = createContext<IRoomState>({
|
|||||||
timelineRenderingType: TimelineRenderingType.Room,
|
timelineRenderingType: TimelineRenderingType.Room,
|
||||||
threadId: undefined,
|
threadId: undefined,
|
||||||
liveTimeline: undefined,
|
liveTimeline: undefined,
|
||||||
|
narrow: false,
|
||||||
});
|
});
|
||||||
RoomContext.displayName = "RoomContext";
|
RoomContext.displayName = "RoomContext";
|
||||||
export default RoomContext;
|
export default RoomContext;
|
||||||
|
@ -38,11 +38,11 @@ describe("MessageComposerButtons", () => {
|
|||||||
const buttons = wrapAndRender(
|
const buttons = wrapAndRender(
|
||||||
<MessageComposerButtons
|
<MessageComposerButtons
|
||||||
isMenuOpen={false}
|
isMenuOpen={false}
|
||||||
narrowMode={false}
|
|
||||||
showLocationButton={true}
|
showLocationButton={true}
|
||||||
showStickersButton={true}
|
showStickersButton={true}
|
||||||
toggleButtonMenu={() => {}}
|
toggleButtonMenu={() => {}}
|
||||||
/>,
|
/>,
|
||||||
|
false,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(buttonLabels(buttons)).toEqual([
|
expect(buttonLabels(buttons)).toEqual([
|
||||||
@ -56,11 +56,11 @@ describe("MessageComposerButtons", () => {
|
|||||||
const buttons = wrapAndRender(
|
const buttons = wrapAndRender(
|
||||||
<MessageComposerButtons
|
<MessageComposerButtons
|
||||||
isMenuOpen={true}
|
isMenuOpen={true}
|
||||||
narrowMode={false}
|
|
||||||
showLocationButton={true}
|
showLocationButton={true}
|
||||||
showStickersButton={true}
|
showStickersButton={true}
|
||||||
toggleButtonMenu={() => {}}
|
toggleButtonMenu={() => {}}
|
||||||
/>,
|
/>,
|
||||||
|
false,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(buttonLabels(buttons)).toEqual([
|
expect(buttonLabels(buttons)).toEqual([
|
||||||
@ -80,11 +80,11 @@ describe("MessageComposerButtons", () => {
|
|||||||
const buttons = wrapAndRender(
|
const buttons = wrapAndRender(
|
||||||
<MessageComposerButtons
|
<MessageComposerButtons
|
||||||
isMenuOpen={false}
|
isMenuOpen={false}
|
||||||
narrowMode={true}
|
|
||||||
showLocationButton={true}
|
showLocationButton={true}
|
||||||
showStickersButton={true}
|
showStickersButton={true}
|
||||||
toggleButtonMenu={() => {}}
|
toggleButtonMenu={() => {}}
|
||||||
/>,
|
/>,
|
||||||
|
true,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(buttonLabels(buttons)).toEqual([
|
expect(buttonLabels(buttons)).toEqual([
|
||||||
@ -97,11 +97,11 @@ describe("MessageComposerButtons", () => {
|
|||||||
const buttons = wrapAndRender(
|
const buttons = wrapAndRender(
|
||||||
<MessageComposerButtons
|
<MessageComposerButtons
|
||||||
isMenuOpen={true}
|
isMenuOpen={true}
|
||||||
narrowMode={true}
|
|
||||||
showLocationButton={true}
|
showLocationButton={true}
|
||||||
showStickersButton={true}
|
showStickersButton={true}
|
||||||
toggleButtonMenu={() => {}}
|
toggleButtonMenu={() => {}}
|
||||||
/>,
|
/>,
|
||||||
|
true,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(buttonLabels(buttons)).toEqual([
|
expect(buttonLabels(buttons)).toEqual([
|
||||||
@ -117,7 +117,7 @@ describe("MessageComposerButtons", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function wrapAndRender(component: React.ReactElement): ReactWrapper {
|
function wrapAndRender(component: React.ReactElement, narrow: boolean): ReactWrapper {
|
||||||
const mockClient = MatrixClientPeg.matrixClient = createTestClient();
|
const mockClient = MatrixClientPeg.matrixClient = createTestClient();
|
||||||
const roomId = "myroomid";
|
const roomId = "myroomid";
|
||||||
const mockRoom: any = {
|
const mockRoom: any = {
|
||||||
@ -128,7 +128,7 @@ function wrapAndRender(component: React.ReactElement): ReactWrapper {
|
|||||||
return new RoomMember(roomId, userId);
|
return new RoomMember(roomId, userId);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const roomState = createRoomState(mockRoom);
|
const roomState = createRoomState(mockRoom, narrow);
|
||||||
|
|
||||||
return mount(
|
return mount(
|
||||||
<MatrixClientContext.Provider value={mockClient}>
|
<MatrixClientContext.Provider value={mockClient}>
|
||||||
@ -139,7 +139,7 @@ function wrapAndRender(component: React.ReactElement): ReactWrapper {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function createRoomState(room: Room): IRoomState {
|
function createRoomState(room: Room, narrow: boolean): IRoomState {
|
||||||
return {
|
return {
|
||||||
room: room,
|
room: room,
|
||||||
roomId: room.roomId,
|
roomId: room.roomId,
|
||||||
@ -176,6 +176,7 @@ function createRoomState(room: Room): IRoomState {
|
|||||||
matrixClientIsReady: false,
|
matrixClientIsReady: false,
|
||||||
timelineRenderingType: TimelineRenderingType.Room,
|
timelineRenderingType: TimelineRenderingType.Room,
|
||||||
liveTimeline: undefined,
|
liveTimeline: undefined,
|
||||||
|
narrow,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user