Fix thread summary layout for narrow right panel timeline (#7838)
This commit is contained in:
parent
5e76d988ca
commit
d8ac7cf202
15 changed files with 248 additions and 68 deletions
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");
|
||||
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 JumpToBottomButton from '../rooms/JumpToBottomButton';
|
||||
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
|
||||
import Measured from '../elements/Measured';
|
||||
|
||||
interface IProps {
|
||||
room: Room;
|
||||
|
@ -55,6 +56,7 @@ interface IProps {
|
|||
showComposer?: boolean;
|
||||
composerRelation?: IEventRelation;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
thread?: Thread;
|
||||
editState?: EditorStateTransfer;
|
||||
|
@ -63,6 +65,7 @@ interface IState {
|
|||
isInitialEventHighlighted?: boolean;
|
||||
layout: Layout;
|
||||
atEndOfLiveTimeline: boolean;
|
||||
narrow: boolean;
|
||||
|
||||
// settings:
|
||||
showReadReceipts?: boolean;
|
||||
|
@ -74,7 +77,8 @@ export default class TimelineCard extends React.Component<IProps, IState> {
|
|||
|
||||
private dispatcherRef: 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 readReceiptsSettingWatcher: string;
|
||||
|
||||
|
@ -84,6 +88,7 @@ export default class TimelineCard extends React.Component<IProps, IState> {
|
|||
showReadReceipts: SettingsStore.getValue("showReadReceipts", props.room.roomId),
|
||||
layout: SettingsStore.getValue("layout"),
|
||||
atEndOfLiveTimeline: true,
|
||||
narrow: false,
|
||||
};
|
||||
this.readReceiptsSettingWatcher = null;
|
||||
}
|
||||
|
@ -134,7 +139,7 @@ export default class TimelineCard extends React.Component<IProps, IState> {
|
|||
editState: payload.event ? new EditorStateTransfer(payload.event) : null,
|
||||
}, () => {
|
||||
if (payload.event) {
|
||||
this.timelinePanelRef.current?.scrollToEventIfNeeded(payload.event.getId());
|
||||
this.timelinePanel.current?.scrollToEventIfNeeded(payload.event.getId());
|
||||
}
|
||||
});
|
||||
break;
|
||||
|
@ -157,7 +162,7 @@ export default class TimelineCard extends React.Component<IProps, IState> {
|
|||
};
|
||||
|
||||
private onScroll = (): void => {
|
||||
const timelinePanel = this.timelinePanelRef.current;
|
||||
const timelinePanel = this.timelinePanel.current;
|
||||
if (!timelinePanel) return;
|
||||
if (timelinePanel.isAtEndOfLiveTimeline()) {
|
||||
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 = () => {
|
||||
if (this.state.initialEventId && this.state.isInitialEventHighlighted) {
|
||||
// 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 {
|
||||
// Otherwise we have to jump manually
|
||||
this.timelinePanelRef.current?.jumpToLiveTimeline();
|
||||
this.timelinePanel.current?.jumpToLiveTimeline();
|
||||
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 (
|
||||
<RoomContext.Provider value={{
|
||||
...this.context,
|
||||
timelineRenderingType: this.props.timelineRenderingType ?? this.context.timelineRenderingType,
|
||||
liveTimeline: this.props.timelineSet.getLiveTimeline(),
|
||||
narrow: this.state.narrow,
|
||||
}}>
|
||||
<BaseCard
|
||||
className={this.props.classNames}
|
||||
onClose={this.props.onClose}
|
||||
withoutScrollContainer={true}
|
||||
header={this.renderTimelineCardHeader()}
|
||||
ref={this.card}
|
||||
>
|
||||
<Measured
|
||||
sensor={this.card.current}
|
||||
onMeasurement={this.onMeasurement}
|
||||
/>
|
||||
<div className="mx_TimelineCard_timeline">
|
||||
{ jumpToBottom }
|
||||
<TimelinePanel
|
||||
ref={this.timelinePanelRef}
|
||||
ref={this.timelinePanel}
|
||||
showReadReceipts={this.state.showReadReceipts}
|
||||
manageReadReceipts={true}
|
||||
manageReadMarkers={false} // No RM support in the TimelineCard
|
||||
|
@ -249,7 +266,7 @@ export default class TimelineCard extends React.Component<IProps, IState> {
|
|||
/>
|
||||
</div>
|
||||
|
||||
{ ContentMessages.sharedInstance().getCurrentUploads(this.props.composerRelation).length > 0 && (
|
||||
{ isUploading && (
|
||||
<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>
|
||||
|
||||
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>
|
||||
);
|
||||
} 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 (
|
||||
<CardContext.Consumer>
|
||||
{ context =>
|
||||
|
@ -687,9 +693,7 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
}}
|
||||
>
|
||||
<span className="mx_ThreadInfo_threads-amount">
|
||||
{ _t("%(count)s reply", {
|
||||
count: this.state.threadReplyCount,
|
||||
}) }
|
||||
{ count }
|
||||
</span>
|
||||
{ this.renderThreadLastMessagePreview() }
|
||||
</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");
|
||||
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";
|
||||
|
||||
let instanceCount = 0;
|
||||
const NARROW_MODE_BREAKPOINT = 500;
|
||||
|
||||
interface ISendButtonProps {
|
||||
onClick: (ev: ButtonEvent) => void;
|
||||
|
@ -88,7 +87,6 @@ interface IState {
|
|||
haveRecording: boolean;
|
||||
recordingTimeLeftSeconds?: number;
|
||||
me?: RoomMember;
|
||||
narrowMode?: boolean;
|
||||
isMenuOpen: boolean;
|
||||
isStickerPickerOpen: boolean;
|
||||
showStickersButton: boolean;
|
||||
|
@ -165,10 +163,9 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
|||
|
||||
private onResize = (type: UI_EVENTS, entry: ResizeObserverEntry) => {
|
||||
if (type === UI_EVENTS.Resize) {
|
||||
const narrowMode = entry.contentRect.width <= NARROW_MODE_BREAKPOINT;
|
||||
const { narrow } = this.context;
|
||||
this.setState({
|
||||
narrowMode,
|
||||
isMenuOpen: !narrowMode ? false : this.state.isMenuOpen,
|
||||
isMenuOpen: !narrow ? false : this.state.isMenuOpen,
|
||||
isStickerPickerOpen: false,
|
||||
});
|
||||
}
|
||||
|
@ -476,11 +473,10 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
|||
isMenuOpen={this.state.isMenuOpen}
|
||||
isStickerPickerOpen={this.state.isStickerPickerOpen}
|
||||
menuPosition={menuPosition}
|
||||
narrowMode={this.state.narrowMode}
|
||||
relation={this.props.relation}
|
||||
onRecordStartEndClick={() => {
|
||||
this.voiceRecordingButton.current?.onRecordStartEndClick();
|
||||
if (this.state.narrowMode) {
|
||||
if (this.context.narrow) {
|
||||
this.toggleButtonMenu();
|
||||
}
|
||||
}}
|
||||
|
|
|
@ -44,7 +44,6 @@ interface IProps {
|
|||
isMenuOpen: boolean;
|
||||
isStickerPickerOpen: boolean;
|
||||
menuPosition: AboveLeftOf;
|
||||
narrowMode?: boolean;
|
||||
onRecordStartEndClick: () => void;
|
||||
relation?: IEventRelation;
|
||||
setStickerPickerOpen: (isStickerPickerOpen: boolean) => void;
|
||||
|
@ -58,7 +57,7 @@ export const OverflowMenuContext = createContext<OverflowMenuCloser | null>(null
|
|||
|
||||
const MessageComposerButtons: React.FC<IProps> = (props: IProps) => {
|
||||
const matrixClient: MatrixClient = useContext(MatrixClientContext);
|
||||
const { room, roomId } = useContext(RoomContext);
|
||||
const { room, roomId, narrow } = useContext(RoomContext);
|
||||
|
||||
if (props.haveRecording) {
|
||||
return null;
|
||||
|
@ -66,14 +65,14 @@ const MessageComposerButtons: React.FC<IProps> = (props: IProps) => {
|
|||
|
||||
let mainButtons: ReactElement[];
|
||||
let moreButtons: ReactElement[];
|
||||
if (props.narrowMode) {
|
||||
if (narrow) {
|
||||
mainButtons = [
|
||||
emojiButton(props),
|
||||
];
|
||||
moreButtons = [
|
||||
uploadButton(props, roomId),
|
||||
showStickersButton(props),
|
||||
voiceRecordingButton(props),
|
||||
voiceRecordingButton(props, narrow),
|
||||
pollButton(room, props.relation),
|
||||
showLocationButton(props, room, roomId, matrixClient),
|
||||
];
|
||||
|
@ -84,7 +83,7 @@ const MessageComposerButtons: React.FC<IProps> = (props: IProps) => {
|
|||
];
|
||||
moreButtons = [
|
||||
showStickersButton(props),
|
||||
voiceRecordingButton(props),
|
||||
voiceRecordingButton(props, narrow),
|
||||
pollButton(room, props.relation),
|
||||
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
|
||||
return (
|
||||
props.narrowMode
|
||||
narrow
|
||||
? null
|
||||
: <CollapsibleButton
|
||||
key="voice_message_send"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue