Allow maintaining a different right panel width for thread panels (#11064)
* Move Room context threadId our of RoomView state * Allow maintaining a different right panel width for thread panels * Fix types * Fix types * Add tests * Increase coverage * Increase coverage * Add comments * Update src/components/structures/MainSplit.tsx Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> --------- Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
This commit is contained in:
parent
cb2b1718ff
commit
7236e48765
9 changed files with 190 additions and 7 deletions
|
@ -26,9 +26,24 @@ interface IProps {
|
||||||
collapsedRhs?: boolean;
|
collapsedRhs?: boolean;
|
||||||
panel?: JSX.Element;
|
panel?: JSX.Element;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
|
/**
|
||||||
|
* A unique identifier for this panel split.
|
||||||
|
*
|
||||||
|
* This is appended to the key used to store the panel size in localStorage, allowing the widths of different
|
||||||
|
* panels to be stored.
|
||||||
|
*/
|
||||||
|
sizeKey?: string;
|
||||||
|
/**
|
||||||
|
* The size to use for the panel component if one isn't persisted in storage. Defaults to 350.
|
||||||
|
*/
|
||||||
|
defaultSize: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class MainSplit extends React.Component<IProps> {
|
export default class MainSplit extends React.Component<IProps> {
|
||||||
|
public static defaultProps = {
|
||||||
|
defaultSize: 350,
|
||||||
|
};
|
||||||
|
|
||||||
private onResizeStart = (): void => {
|
private onResizeStart = (): void => {
|
||||||
this.props.resizeNotifier.startResizing();
|
this.props.resizeNotifier.startResizing();
|
||||||
};
|
};
|
||||||
|
@ -37,6 +52,14 @@ export default class MainSplit extends React.Component<IProps> {
|
||||||
this.props.resizeNotifier.notifyRightHandleResized();
|
this.props.resizeNotifier.notifyRightHandleResized();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private get sizeSettingStorageKey(): string {
|
||||||
|
let key = "mx_rhs_size";
|
||||||
|
if (!!this.props.sizeKey) {
|
||||||
|
key += `_${this.props.sizeKey}`;
|
||||||
|
}
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
private onResizeStop = (
|
private onResizeStop = (
|
||||||
event: MouseEvent | TouchEvent,
|
event: MouseEvent | TouchEvent,
|
||||||
direction: Direction,
|
direction: Direction,
|
||||||
|
@ -44,14 +67,17 @@ export default class MainSplit extends React.Component<IProps> {
|
||||||
delta: NumberSize,
|
delta: NumberSize,
|
||||||
): void => {
|
): void => {
|
||||||
this.props.resizeNotifier.stopResizing();
|
this.props.resizeNotifier.stopResizing();
|
||||||
window.localStorage.setItem("mx_rhs_size", (this.loadSidePanelSize().width + delta.width).toString());
|
window.localStorage.setItem(
|
||||||
|
this.sizeSettingStorageKey,
|
||||||
|
(this.loadSidePanelSize().width + delta.width).toString(),
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
private loadSidePanelSize(): { height: string | number; width: number } {
|
private loadSidePanelSize(): { height: string | number; width: number } {
|
||||||
let rhsSize = parseInt(window.localStorage.getItem("mx_rhs_size")!, 10);
|
let rhsSize = parseInt(window.localStorage.getItem(this.sizeSettingStorageKey)!, 10);
|
||||||
|
|
||||||
if (isNaN(rhsSize)) {
|
if (isNaN(rhsSize)) {
|
||||||
rhsSize = 350;
|
rhsSize = this.props.defaultSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -70,6 +96,7 @@ export default class MainSplit extends React.Component<IProps> {
|
||||||
if (hasResizer) {
|
if (hasResizer) {
|
||||||
children = (
|
children = (
|
||||||
<Resizable
|
<Resizable
|
||||||
|
key={this.props.sizeKey}
|
||||||
defaultSize={this.loadSidePanelSize()}
|
defaultSize={this.loadSidePanelSize()}
|
||||||
minWidth={264}
|
minWidth={264}
|
||||||
maxWidth="50%"
|
maxWidth="50%"
|
||||||
|
|
|
@ -179,6 +179,11 @@ export interface IRoomState {
|
||||||
showApps: boolean;
|
showApps: boolean;
|
||||||
isPeeking: boolean;
|
isPeeking: boolean;
|
||||||
showRightPanel: boolean;
|
showRightPanel: boolean;
|
||||||
|
/**
|
||||||
|
* Whether the right panel shown is either of ThreadPanel or ThreadView.
|
||||||
|
* Always false when `showRightPanel` is false.
|
||||||
|
*/
|
||||||
|
threadRightPanel: boolean;
|
||||||
// error object, as from the matrix client/server API
|
// error object, as from the matrix client/server API
|
||||||
// If we failed to load information about the room,
|
// If we failed to load information about the room,
|
||||||
// store the error here.
|
// store the error here.
|
||||||
|
@ -223,7 +228,6 @@ export interface IRoomState {
|
||||||
wasContextSwitch?: boolean;
|
wasContextSwitch?: boolean;
|
||||||
editState?: EditorStateTransfer;
|
editState?: EditorStateTransfer;
|
||||||
timelineRenderingType: TimelineRenderingType;
|
timelineRenderingType: TimelineRenderingType;
|
||||||
threadId?: string;
|
|
||||||
liveTimeline?: EventTimeline;
|
liveTimeline?: EventTimeline;
|
||||||
narrow: boolean;
|
narrow: boolean;
|
||||||
msc3946ProcessDynamicPredecessor: boolean;
|
msc3946ProcessDynamicPredecessor: boolean;
|
||||||
|
@ -402,6 +406,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||||
showApps: false,
|
showApps: false,
|
||||||
isPeeking: false,
|
isPeeking: false,
|
||||||
showRightPanel: false,
|
showRightPanel: false,
|
||||||
|
threadRightPanel: false,
|
||||||
joining: false,
|
joining: false,
|
||||||
showTopUnreadMessagesBar: false,
|
showTopUnreadMessagesBar: false,
|
||||||
statusBarVisible: false,
|
statusBarVisible: false,
|
||||||
|
@ -623,6 +628,11 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||||
mainSplitContentType: room ? this.getMainSplitContentType(room) : undefined,
|
mainSplitContentType: room ? this.getMainSplitContentType(room) : undefined,
|
||||||
initialEventId: undefined, // default to clearing this, will get set later in the method if needed
|
initialEventId: undefined, // default to clearing this, will get set later in the method if needed
|
||||||
showRightPanel: roomId ? this.context.rightPanelStore.isOpenForRoom(roomId) : false,
|
showRightPanel: roomId ? this.context.rightPanelStore.isOpenForRoom(roomId) : false,
|
||||||
|
threadRightPanel: roomId
|
||||||
|
? [RightPanelPhases.ThreadView, RightPanelPhases.ThreadPanel].includes(
|
||||||
|
this.context.rightPanelStore.currentCardForRoom(roomId).phase!,
|
||||||
|
)
|
||||||
|
: false,
|
||||||
activeCall: roomId ? CallStore.instance.getActiveCall(roomId) : null,
|
activeCall: roomId ? CallStore.instance.getActiveCall(roomId) : null,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1011,8 +1021,14 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private onRightPanelStoreUpdate = (): void => {
|
private onRightPanelStoreUpdate = (): void => {
|
||||||
|
const { roomId } = this.state;
|
||||||
this.setState({
|
this.setState({
|
||||||
showRightPanel: this.state.roomId ? this.context.rightPanelStore.isOpenForRoom(this.state.roomId) : false,
|
showRightPanel: roomId ? this.context.rightPanelStore.isOpenForRoom(roomId) : false,
|
||||||
|
threadRightPanel: roomId
|
||||||
|
? [RightPanelPhases.ThreadView, RightPanelPhases.ThreadPanel].includes(
|
||||||
|
this.context.rightPanelStore.currentCardForRoom(roomId).phase!,
|
||||||
|
)
|
||||||
|
: false,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -2439,7 +2455,14 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||||
viewingCall={viewingCall}
|
viewingCall={viewingCall}
|
||||||
activeCall={this.state.activeCall}
|
activeCall={this.state.activeCall}
|
||||||
/>
|
/>
|
||||||
<MainSplit panel={rightPanel} resizeNotifier={this.props.resizeNotifier}>
|
<MainSplit
|
||||||
|
panel={rightPanel}
|
||||||
|
resizeNotifier={this.props.resizeNotifier}
|
||||||
|
// Override defaults when a thread is being shown to allow persisting a separate
|
||||||
|
// right panel width for thread panels as they tend to want to be wider.
|
||||||
|
sizeKey={this.state.threadRightPanel ? "thread" : undefined}
|
||||||
|
defaultSize={this.state.threadRightPanel ? 500 : undefined}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
className={mainSplitContentClasses}
|
className={mainSplitContentClasses}
|
||||||
ref={this.roomViewBody}
|
ref={this.roomViewBody}
|
||||||
|
|
|
@ -29,7 +29,11 @@ export enum TimelineRenderingType {
|
||||||
Pinned = "Pinned",
|
Pinned = "Pinned",
|
||||||
}
|
}
|
||||||
|
|
||||||
const RoomContext = createContext<IRoomState>({
|
const RoomContext = createContext<
|
||||||
|
IRoomState & {
|
||||||
|
threadId?: string;
|
||||||
|
}
|
||||||
|
>({
|
||||||
roomLoading: true,
|
roomLoading: true,
|
||||||
peekLoading: false,
|
peekLoading: false,
|
||||||
shouldPeek: true,
|
shouldPeek: true,
|
||||||
|
@ -39,6 +43,7 @@ const RoomContext = createContext<IRoomState>({
|
||||||
showApps: false,
|
showApps: false,
|
||||||
isPeeking: false,
|
isPeeking: false,
|
||||||
showRightPanel: true,
|
showRightPanel: true,
|
||||||
|
threadRightPanel: false,
|
||||||
joining: false,
|
joining: false,
|
||||||
showTopUnreadMessagesBar: false,
|
showTopUnreadMessagesBar: false,
|
||||||
statusBarVisible: false,
|
statusBarVisible: false,
|
||||||
|
|
63
test/components/structures/MainSplit-test.tsx
Normal file
63
test/components/structures/MainSplit-test.tsx
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
/*
|
||||||
|
Copyright 2023 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 { render } from "@testing-library/react";
|
||||||
|
|
||||||
|
import MainSplit from "../../../src/components/structures/MainSplit";
|
||||||
|
import ResizeNotifier from "../../../src/utils/ResizeNotifier";
|
||||||
|
|
||||||
|
describe("<MainSplit/>", () => {
|
||||||
|
const resizeNotifier = new ResizeNotifier();
|
||||||
|
const children = (
|
||||||
|
<div>
|
||||||
|
Child<span>Foo</span>Bar
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
const panel = <div>Right panel</div>;
|
||||||
|
|
||||||
|
it("renders", () => {
|
||||||
|
const { asFragment, container } = render(
|
||||||
|
<MainSplit resizeNotifier={resizeNotifier} children={children} panel={panel} />,
|
||||||
|
);
|
||||||
|
expect(asFragment()).toMatchSnapshot();
|
||||||
|
// Assert it matches the default width of 350
|
||||||
|
expect(container.querySelector<HTMLElement>(".mx_RightPanel_ResizeWrapper")!.style.width).toBe("350px");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("respects defaultSize prop", () => {
|
||||||
|
const { asFragment, container } = render(
|
||||||
|
<MainSplit resizeNotifier={resizeNotifier} children={children} panel={panel} defaultSize={500} />,
|
||||||
|
);
|
||||||
|
expect(asFragment()).toMatchSnapshot();
|
||||||
|
// Assert it matches the default width of 350
|
||||||
|
expect(container.querySelector<HTMLElement>(".mx_RightPanel_ResizeWrapper")!.style.width).toBe("500px");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("prefers size stashed in LocalStorage to the defaultSize prop", () => {
|
||||||
|
localStorage.setItem("mx_rhs_size_thread", "333");
|
||||||
|
const { container } = render(
|
||||||
|
<MainSplit
|
||||||
|
resizeNotifier={resizeNotifier}
|
||||||
|
children={children}
|
||||||
|
panel={panel}
|
||||||
|
sizeKey="thread"
|
||||||
|
defaultSize={400}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
expect(container.querySelector<HTMLElement>(".mx_RightPanel_ResizeWrapper")!.style.width).toBe("333px");
|
||||||
|
});
|
||||||
|
});
|
|
@ -225,6 +225,7 @@ describe("RoomView", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("updates url preview visibility on encryption state change", async () => {
|
it("updates url preview visibility on encryption state change", async () => {
|
||||||
|
room.getMyMembership = jest.fn().mockReturnValue("join");
|
||||||
// we should be starting unencrypted
|
// we should be starting unencrypted
|
||||||
expect(cli.isCryptoEnabled()).toEqual(false);
|
expect(cli.isCryptoEnabled()).toEqual(false);
|
||||||
expect(cli.isRoomEncrypted(room.roomId)).toEqual(false);
|
expect(cli.isRoomEncrypted(room.roomId)).toEqual(false);
|
||||||
|
|
|
@ -0,0 +1,61 @@
|
||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`<MainSplit/> renders 1`] = `
|
||||||
|
<DocumentFragment>
|
||||||
|
<div
|
||||||
|
class="mx_MainSplit"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
Child
|
||||||
|
<span>
|
||||||
|
Foo
|
||||||
|
</span>
|
||||||
|
Bar
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_RightPanel_ResizeWrapper"
|
||||||
|
style="position: relative; user-select: auto; width: 350px; height: 100%; max-width: 50%; min-width: 264px; box-sizing: border-box; flex-shrink: 0;"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
Right panel
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="mx_ResizeHandle--horizontal"
|
||||||
|
style="position: absolute; user-select: none; width: 10px; height: 100%; top: 0px; left: -5px; cursor: col-resize;"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DocumentFragment>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`<MainSplit/> respects defaultSize prop 1`] = `
|
||||||
|
<DocumentFragment>
|
||||||
|
<div
|
||||||
|
class="mx_MainSplit"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
Child
|
||||||
|
<span>
|
||||||
|
Foo
|
||||||
|
</span>
|
||||||
|
Bar
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_RightPanel_ResizeWrapper"
|
||||||
|
style="position: relative; user-select: auto; width: 500px; height: 100%; max-width: 50%; min-width: 264px; box-sizing: border-box; flex-shrink: 0;"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
Right panel
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="mx_ResizeHandle--horizontal"
|
||||||
|
style="position: absolute; user-select: none; width: 10px; height: 100%; top: 0px; left: -5px; cursor: col-resize;"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DocumentFragment>
|
||||||
|
`;
|
|
@ -57,6 +57,7 @@ describe("<SendMessageComposer/>", () => {
|
||||||
showApps: false,
|
showApps: false,
|
||||||
isPeeking: false,
|
isPeeking: false,
|
||||||
showRightPanel: true,
|
showRightPanel: true,
|
||||||
|
threadRightPanel: false,
|
||||||
joining: false,
|
joining: false,
|
||||||
atEndOfLiveTimeline: true,
|
atEndOfLiveTimeline: true,
|
||||||
showTopUnreadMessagesBar: false,
|
showTopUnreadMessagesBar: false,
|
||||||
|
|
|
@ -61,6 +61,7 @@ export function getRoomContext(room: Room, override: Partial<IRoomState>): IRoom
|
||||||
showApps: false,
|
showApps: false,
|
||||||
isPeeking: false,
|
isPeeking: false,
|
||||||
showRightPanel: true,
|
showRightPanel: true,
|
||||||
|
threadRightPanel: false,
|
||||||
joining: false,
|
joining: false,
|
||||||
atEndOfLiveTimeline: true,
|
atEndOfLiveTimeline: true,
|
||||||
showTopUnreadMessagesBar: false,
|
showTopUnreadMessagesBar: false,
|
||||||
|
|
|
@ -237,6 +237,7 @@ export function createTestClient(): MatrixClient {
|
||||||
searchUserDirectory: jest.fn().mockResolvedValue({ limited: false, results: [] }),
|
searchUserDirectory: jest.fn().mockResolvedValue({ limited: false, results: [] }),
|
||||||
setDeviceVerified: jest.fn(),
|
setDeviceVerified: jest.fn(),
|
||||||
joinRoom: jest.fn(),
|
joinRoom: jest.fn(),
|
||||||
|
getSyncStateData: jest.fn(),
|
||||||
} as unknown as MatrixClient;
|
} as unknown as MatrixClient;
|
||||||
|
|
||||||
client.reEmitter = new ReEmitter(client);
|
client.reEmitter = new ReEmitter(client);
|
||||||
|
|
Loading…
Reference in a new issue