Create room threads list view (#6904)
Implement https://github.com/vector-im/element-web/issues/18957 following requirements: * Create a new right panel view to list all the threads in a given room. * Change ThreadView previous phase to be ThreadPanel rather than RoomSummary * Implement local filters for My and All threads In addition: * Create a new TileShape for proper rendering requirements (hiding typing indicator) * Create new timelineRenderingType for proper rendering requirements
This commit is contained in:
parent
6bb47ec710
commit
562a880c7d
17 changed files with 623 additions and 121 deletions
|
@ -203,6 +203,7 @@
|
||||||
@import "./views/right_panel/_UserInfo.scss";
|
@import "./views/right_panel/_UserInfo.scss";
|
||||||
@import "./views/right_panel/_VerificationPanel.scss";
|
@import "./views/right_panel/_VerificationPanel.scss";
|
||||||
@import "./views/right_panel/_WidgetCard.scss";
|
@import "./views/right_panel/_WidgetCard.scss";
|
||||||
|
@import "./views/right_panel/_ThreadPanel.scss";
|
||||||
@import "./views/room_settings/_AliasSettings.scss";
|
@import "./views/room_settings/_AliasSettings.scss";
|
||||||
@import "./views/rooms/_AppsDrawer.scss";
|
@import "./views/rooms/_AppsDrawer.scss";
|
||||||
@import "./views/rooms/_Autocomplete.scss";
|
@import "./views/rooms/_Autocomplete.scss";
|
||||||
|
|
|
@ -79,6 +79,10 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_RightPanel_threadsButton::before {
|
||||||
|
mask-image: url('$(res)/img/element-icons/room/thread.svg');
|
||||||
|
}
|
||||||
|
|
||||||
.mx_RightPanel_notifsButton::before {
|
.mx_RightPanel_notifsButton::before {
|
||||||
mask-image: url('$(res)/img/element-icons/notifications.svg');
|
mask-image: url('$(res)/img/element-icons/notifications.svg');
|
||||||
mask-position: center;
|
mask-position: center;
|
||||||
|
|
152
res/css/views/right_panel/_ThreadPanel.scss
Normal file
152
res/css/views/right_panel/_ThreadPanel.scss
Normal file
|
@ -0,0 +1,152 @@
|
||||||
|
/*
|
||||||
|
Copyright 2021 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
.mx_ThreadPanel {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.mx_BaseCard_header {
|
||||||
|
padding: 6px 0;
|
||||||
|
|
||||||
|
.mx_BaseCard_close {
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_AccessibleButton.mx_BaseCard_back {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__header {
|
||||||
|
width: calc(100% - 40px);
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
span:first-of-type {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 18px;
|
||||||
|
color: $secondary-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_AccessibleButton {
|
||||||
|
font-size: 12px;
|
||||||
|
color: $secondary-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_ContextualMenu_wrapper {
|
||||||
|
// It's added here due to some weird error if I pass it directly in the style, even though it's a numeric value, so it's being passed 0 instead.
|
||||||
|
// The error: react_devtools_backend.js:2526 Warning: `NaN` is an invalid value for the `top` css style property.
|
||||||
|
top: 43px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_ContextualMenu {
|
||||||
|
position: initial;
|
||||||
|
span:first-of-type {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 12px;
|
||||||
|
color: $primary-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
font-size: 12px;
|
||||||
|
color: $secondary-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_ThreadPanel_Header_FilterOptionItem {
|
||||||
|
display: flex;
|
||||||
|
flex-grow: 1;
|
||||||
|
justify-content: space-between;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: visible;
|
||||||
|
width: 100%;
|
||||||
|
padding: 20px;
|
||||||
|
padding-left: 30px;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: $event-selected-color;
|
||||||
|
}
|
||||||
|
&[aria-selected="true"] {
|
||||||
|
&::before {
|
||||||
|
content: "";
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
grid-column: 1;
|
||||||
|
grid-row: 1;
|
||||||
|
mask-image: url("$(res)/img/feather-customised/check.svg");
|
||||||
|
mask-size: 100%;
|
||||||
|
mask-repeat: no-repeat;
|
||||||
|
position: absolute;
|
||||||
|
top: 22px;
|
||||||
|
left: 10px;
|
||||||
|
background-color: $primary-content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_RoomView_messageListWrapper {
|
||||||
|
background-color: $background;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding-top: 8px;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_ScrollPanel {
|
||||||
|
.mx_RoomView_MessageList {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_EventTile, .mx_EventListSummary {
|
||||||
|
// Account for scrollbar when hovering
|
||||||
|
width: calc(100% - 3px);
|
||||||
|
margin: 0 2px;
|
||||||
|
|
||||||
|
.mx_MessageTimestamp {
|
||||||
|
// We need to add !important here due to some enormous selectors overriding it anyways
|
||||||
|
// See: _EventTile.scss:241
|
||||||
|
left: unset !important;
|
||||||
|
right: 0 !important;
|
||||||
|
top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_EventTile_line.mx_EventTile_line {
|
||||||
|
position: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_ThreadInfo {
|
||||||
|
position: relative;
|
||||||
|
padding-right: 11px;
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
bottom: -16px;
|
||||||
|
height: 1px;
|
||||||
|
width: 100%;
|
||||||
|
border-bottom: 1px solid $message-action-bar-border-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_DateSeparator {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
1
res/img/element-icons/room/thread.svg
Normal file
1
res/img/element-icons/room/thread.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path fill="#17191C" fill-rule="evenodd" d="M2 5a3 3 0 0 1 3-3h14a3 3 0 0 1 3 3v12a3 3 0 0 1-3 3H7.667a1 1 0 0 0-.6.2L3.6 22.8A1 1 0 0 1 2 22V5Zm3 4a1 1 0 0 1 1-1h12a1 1 0 1 1 0 2H6a1 1 0 0 1-1-1Zm1 3a1 1 0 1 0 0 2h6a1 1 0 1 0 0-2H6Z" clip-rule="evenodd"/></svg>
|
After Width: | Height: | Size: 357 B |
|
@ -26,7 +26,7 @@ import shouldHideEvent from '../../shouldHideEvent';
|
||||||
import { wantsDateSeparator } from '../../DateUtils';
|
import { wantsDateSeparator } from '../../DateUtils';
|
||||||
import { MatrixClientPeg } from '../../MatrixClientPeg';
|
import { MatrixClientPeg } from '../../MatrixClientPeg';
|
||||||
import SettingsStore from '../../settings/SettingsStore';
|
import SettingsStore from '../../settings/SettingsStore';
|
||||||
import RoomContext from "../../contexts/RoomContext";
|
import RoomContext, { TimelineRenderingType } from "../../contexts/RoomContext";
|
||||||
import { Layout } from "../../settings/Layout";
|
import { Layout } from "../../settings/Layout";
|
||||||
import { _t } from "../../languageHandler";
|
import { _t } from "../../languageHandler";
|
||||||
import EventTile, { haveTileForEvent, IReadReceiptProps, TileShape } from "../views/rooms/EventTile";
|
import EventTile, { haveTileForEvent, IReadReceiptProps, TileShape } from "../views/rooms/EventTile";
|
||||||
|
@ -66,7 +66,9 @@ export function shouldFormContinuation(
|
||||||
prevEvent: MatrixEvent,
|
prevEvent: MatrixEvent,
|
||||||
mxEvent: MatrixEvent,
|
mxEvent: MatrixEvent,
|
||||||
showHiddenEvents: boolean,
|
showHiddenEvents: boolean,
|
||||||
|
timelineRenderingType?: TimelineRenderingType,
|
||||||
): boolean {
|
): boolean {
|
||||||
|
if (timelineRenderingType === TimelineRenderingType.ThreadsList) return false;
|
||||||
// sanity check inputs
|
// sanity check inputs
|
||||||
if (!prevEvent || !prevEvent.sender || !mxEvent.sender) return false;
|
if (!prevEvent || !prevEvent.sender || !mxEvent.sender) return false;
|
||||||
// check if within the max continuation period
|
// check if within the max continuation period
|
||||||
|
@ -722,7 +724,7 @@ export default class MessagePanel extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
// is this a continuation of the previous message?
|
// is this a continuation of the previous message?
|
||||||
const continuation = !wantsDateSeparator &&
|
const continuation = !wantsDateSeparator &&
|
||||||
shouldFormContinuation(prevEvent, mxEv, this.showHiddenEvents);
|
shouldFormContinuation(prevEvent, mxEv, this.showHiddenEvents, this.context.timelineRenderingType);
|
||||||
|
|
||||||
const eventId = mxEv.getId();
|
const eventId = mxEv.getId();
|
||||||
const highlight = (eventId === this.props.highlightedEventId);
|
const highlight = (eventId === this.props.highlightedEventId);
|
||||||
|
@ -794,6 +796,9 @@ export default class MessagePanel extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
public wantsDateSeparator(prevEvent: MatrixEvent, nextEventDate: Date): boolean {
|
public wantsDateSeparator(prevEvent: MatrixEvent, nextEventDate: Date): boolean {
|
||||||
|
if (this.context.timelineRenderingType === TimelineRenderingType.ThreadsList) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if (prevEvent == null) {
|
if (prevEvent == null) {
|
||||||
// first event in the panel: depends if we could back-paginate from
|
// first event in the panel: depends if we could back-paginate from
|
||||||
// here.
|
// here.
|
||||||
|
|
|
@ -53,7 +53,7 @@ import { throttle } from 'lodash';
|
||||||
import SpaceStore from "../../stores/SpaceStore";
|
import SpaceStore from "../../stores/SpaceStore";
|
||||||
import { RoomPermalinkCreator } from '../../utils/permalinks/Permalinks';
|
import { RoomPermalinkCreator } from '../../utils/permalinks/Permalinks';
|
||||||
import { E2EStatus } from '../../utils/ShieldUtils';
|
import { E2EStatus } from '../../utils/ShieldUtils';
|
||||||
import { SetRightPanelPhasePayload } from '../../dispatcher/payloads/SetRightPanelPhasePayload';
|
import { dispatchShowThreadsPanelEvent } from '../../dispatcher/dispatch-actions/threads';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
room?: Room; // if showing panels for a given room, this is set
|
room?: Room; // if showing panels for a given room, this is set
|
||||||
|
@ -199,10 +199,7 @@ export default class RightPanel extends React.Component<IProps, IState> {
|
||||||
const isChangingRoom = payload.action === 'view_room' && payload.room_id !== this.props.room.roomId;
|
const isChangingRoom = payload.action === 'view_room' && payload.room_id !== this.props.room.roomId;
|
||||||
const isViewingThread = this.state.phase === RightPanelPhases.ThreadView;
|
const isViewingThread = this.state.phase === RightPanelPhases.ThreadView;
|
||||||
if (isChangingRoom && isViewingThread) {
|
if (isChangingRoom && isViewingThread) {
|
||||||
dis.dispatch<SetRightPanelPhasePayload>({
|
dispatchShowThreadsPanelEvent();
|
||||||
action: Action.SetRightPanelPhase,
|
|
||||||
phase: RightPanelPhases.ThreadPanel,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (payload.action === Action.AfterRightPanelPhaseChange) {
|
if (payload.action === Action.AfterRightPanelPhaseChange) {
|
||||||
|
|
|
@ -14,17 +14,26 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React, { useContext, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { MatrixEvent, Room } from 'matrix-js-sdk/src';
|
import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
|
||||||
import { Thread, ThreadEvent } from 'matrix-js-sdk/src/models/thread';
|
import { Thread, ThreadEvent } from 'matrix-js-sdk/src/models/thread';
|
||||||
|
import { EventTimelineSet } from 'matrix-js-sdk/src/models/event-timeline-set';
|
||||||
|
import { Room } from 'matrix-js-sdk/src/models/room';
|
||||||
|
|
||||||
import BaseCard from "../views/right_panel/BaseCard";
|
import BaseCard from "../views/right_panel/BaseCard";
|
||||||
import { RightPanelPhases } from "../../stores/RightPanelStorePhases";
|
import { RightPanelPhases } from "../../stores/RightPanelStorePhases";
|
||||||
import { replaceableComponent } from "../../utils/replaceableComponent";
|
|
||||||
import { MatrixClientPeg } from '../../MatrixClientPeg';
|
|
||||||
|
|
||||||
import ResizeNotifier from '../../utils/ResizeNotifier';
|
import ResizeNotifier from '../../utils/ResizeNotifier';
|
||||||
import EventTile from '../views/rooms/EventTile';
|
import EventTile, { TileShape } from '../views/rooms/EventTile';
|
||||||
|
import MatrixClientContext from '../../contexts/MatrixClientContext';
|
||||||
|
import { _t } from '../../languageHandler';
|
||||||
|
import { ContextMenuButton } from '../../accessibility/context_menu/ContextMenuButton';
|
||||||
|
import ContextMenu, { useContextMenu } from './ContextMenu';
|
||||||
|
import RoomContext, { TimelineRenderingType } from '../../contexts/RoomContext';
|
||||||
|
import TimelinePanel from './TimelinePanel';
|
||||||
|
import { Layout } from '../../settings/Layout';
|
||||||
|
import { useEventEmitter } from '../../hooks/useEventEmitter';
|
||||||
|
import AccessibleButton from '../views/elements/AccessibleButton';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
roomId: string;
|
roomId: string;
|
||||||
|
@ -32,62 +41,199 @@ interface IProps {
|
||||||
resizeNotifier: ResizeNotifier;
|
resizeNotifier: ResizeNotifier;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
export const ThreadPanelItem: React.FC<{ event: MatrixEvent }> = ({ event }) => {
|
||||||
threads?: Thread[];
|
return <EventTile
|
||||||
|
key={event.getId()}
|
||||||
|
mxEvent={event}
|
||||||
|
enableFlair={false}
|
||||||
|
showReadReceipts={false}
|
||||||
|
as="div"
|
||||||
|
tileShape={TileShape.Thread}
|
||||||
|
alwaysShowTimestamps={true}
|
||||||
|
/>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export enum ThreadFilterType {
|
||||||
|
"My",
|
||||||
|
"All"
|
||||||
}
|
}
|
||||||
|
|
||||||
@replaceableComponent("structures.ThreadView")
|
type ThreadPanelHeaderOption = {
|
||||||
export default class ThreadPanel extends React.Component<IProps, IState> {
|
label: string;
|
||||||
private room: Room;
|
description: string;
|
||||||
|
key: ThreadFilterType;
|
||||||
|
};
|
||||||
|
|
||||||
constructor(props: IProps) {
|
const useFilteredThreadsTimelinePanel = ({
|
||||||
super(props);
|
threads,
|
||||||
this.room = MatrixClientPeg.get().getRoom(this.props.roomId);
|
room,
|
||||||
}
|
filterOption,
|
||||||
|
userId,
|
||||||
|
updateTimeline,
|
||||||
|
}: {
|
||||||
|
threads: Set<Thread>;
|
||||||
|
room: Room;
|
||||||
|
userId: string;
|
||||||
|
filterOption: ThreadFilterType;
|
||||||
|
updateTimeline: () => void;
|
||||||
|
}) => {
|
||||||
|
const timelineSet = useMemo(() => new EventTimelineSet(room, {
|
||||||
|
unstableClientRelationAggregation: true,
|
||||||
|
timelineSupport: true,
|
||||||
|
}), [room]);
|
||||||
|
|
||||||
public componentDidMount(): void {
|
useEffect(() => {
|
||||||
this.room.on(ThreadEvent.Update, this.onThreadEventReceived);
|
let filteredThreads = Array.from(threads);
|
||||||
this.room.on(ThreadEvent.Ready, this.onThreadEventReceived);
|
if (filterOption === ThreadFilterType.My) {
|
||||||
}
|
filteredThreads = filteredThreads.filter(thread => {
|
||||||
|
return thread.rootEvent.getSender() === userId;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// NOTE: Temporarily reverse the list until https://github.com/vector-im/element-web/issues/19393 gets properly resolved
|
||||||
|
// The proper list order should be top-to-bottom, like in social-media newsfeeds.
|
||||||
|
filteredThreads.reverse().forEach(thread => {
|
||||||
|
const event = thread.rootEvent;
|
||||||
|
if (timelineSet.findEventById(event.getId()) || event.status !== null) return;
|
||||||
|
timelineSet.addEventToTimeline(
|
||||||
|
event,
|
||||||
|
timelineSet.getLiveTimeline(),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [room, timelineSet]);
|
||||||
|
|
||||||
public componentWillUnmount(): void {
|
useEventEmitter(room, ThreadEvent.Update, (thread) => {
|
||||||
this.room.removeListener(ThreadEvent.Update, this.onThreadEventReceived);
|
const event = thread.rootEvent;
|
||||||
this.room.removeListener(ThreadEvent.Ready, this.onThreadEventReceived);
|
if (
|
||||||
}
|
// If that's a reply and not an event
|
||||||
|
event !== thread.replyToEvent &&
|
||||||
|
timelineSet.findEventById(event.getId()) ||
|
||||||
|
event.status !== null
|
||||||
|
) return;
|
||||||
|
if (event !== thread.events[thread.events.length - 1]) {
|
||||||
|
timelineSet.removeEvent(thread.events[thread.events.length - 1]);
|
||||||
|
timelineSet.removeEvent(event);
|
||||||
|
}
|
||||||
|
timelineSet.addEventToTimeline(
|
||||||
|
event,
|
||||||
|
timelineSet.getLiveTimeline(),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
updateTimeline();
|
||||||
|
});
|
||||||
|
|
||||||
private onThreadEventReceived = () => this.updateThreads();
|
return timelineSet;
|
||||||
|
};
|
||||||
|
|
||||||
private updateThreads = (callback?: () => void): void => {
|
export const ThreadPanelHeaderFilterOptionItem = ({
|
||||||
this.setState({
|
label,
|
||||||
threads: this.room.getThreads(),
|
description,
|
||||||
}, callback);
|
onClick,
|
||||||
};
|
isSelected,
|
||||||
|
}: ThreadPanelHeaderOption & {
|
||||||
|
onClick: () => void;
|
||||||
|
isSelected: boolean;
|
||||||
|
}) => {
|
||||||
|
return <AccessibleButton
|
||||||
|
aria-selected={isSelected}
|
||||||
|
className="mx_ThreadPanel_Header_FilterOptionItem"
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
<span>{ label }</span>
|
||||||
|
<span>{ description }</span>
|
||||||
|
</AccessibleButton>;
|
||||||
|
};
|
||||||
|
|
||||||
private renderEventTile(event: MatrixEvent): JSX.Element {
|
export const ThreadPanelHeader = ({ filterOption, setFilterOption }: {
|
||||||
return <EventTile
|
filterOption: ThreadFilterType;
|
||||||
key={event.getId()}
|
setFilterOption: (filterOption: ThreadFilterType) => void;
|
||||||
mxEvent={event}
|
}) => {
|
||||||
enableFlair={false}
|
const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu<HTMLElement>();
|
||||||
showReadReceipts={false}
|
const options: readonly ThreadPanelHeaderOption[] = [
|
||||||
as="div"
|
{
|
||||||
/>;
|
label: _t("My threads"),
|
||||||
}
|
description: _t("Shows all threads you’ve participated in"),
|
||||||
|
key: ThreadFilterType.My,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: _t("All threads"),
|
||||||
|
description: _t('Shows all threads from current room'),
|
||||||
|
key: ThreadFilterType.All,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
public render(): JSX.Element {
|
const value = options.find(option => option.key === filterOption);
|
||||||
return (
|
const contextMenuOptions = options.map(opt => <ThreadPanelHeaderFilterOptionItem
|
||||||
|
key={opt.key}
|
||||||
|
label={opt.label}
|
||||||
|
description={opt.description}
|
||||||
|
onClick={() => {
|
||||||
|
setFilterOption(opt.key);
|
||||||
|
closeMenu();
|
||||||
|
}}
|
||||||
|
isSelected={opt === value}
|
||||||
|
/>);
|
||||||
|
const contextMenu = menuDisplayed ? <ContextMenu top={0} right={25} onFinished={closeMenu} managed={false}>
|
||||||
|
{ contextMenuOptions }
|
||||||
|
</ContextMenu> : null;
|
||||||
|
return <div className="mx_ThreadPanel__header">
|
||||||
|
<span>{ _t("Threads") }</span>
|
||||||
|
<ContextMenuButton inputRef={button} isExpanded={menuDisplayed} onClick={() => menuDisplayed ? closeMenu() : openMenu()}>
|
||||||
|
{ `${_t('Show:')} ${value.label}` }
|
||||||
|
</ContextMenuButton>
|
||||||
|
{ contextMenu }
|
||||||
|
</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ThreadPanel: React.FC<IProps> = ({ roomId, onClose }) => {
|
||||||
|
const mxClient = useContext(MatrixClientContext);
|
||||||
|
const roomContext = useContext(RoomContext);
|
||||||
|
const room = mxClient.getRoom(roomId);
|
||||||
|
const [filterOption, setFilterOption] = useState<ThreadFilterType>(ThreadFilterType.All);
|
||||||
|
const ref = useRef<TimelinePanel>();
|
||||||
|
|
||||||
|
const filteredTimelineSet = useFilteredThreadsTimelinePanel({
|
||||||
|
threads: room.threads,
|
||||||
|
room,
|
||||||
|
filterOption,
|
||||||
|
userId: mxClient.getUserId(),
|
||||||
|
updateTimeline: () => ref.current?.refreshTimeline(),
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RoomContext.Provider value={{
|
||||||
|
...roomContext,
|
||||||
|
timelineRenderingType: TimelineRenderingType.ThreadsList,
|
||||||
|
liveTimeline: filteredTimelineSet.getLiveTimeline(),
|
||||||
|
showHiddenEventsInTimeline: true,
|
||||||
|
}}>
|
||||||
<BaseCard
|
<BaseCard
|
||||||
|
header={<ThreadPanelHeader filterOption={filterOption} setFilterOption={setFilterOption} />}
|
||||||
className="mx_ThreadPanel"
|
className="mx_ThreadPanel"
|
||||||
onClose={this.props.onClose}
|
onClose={onClose}
|
||||||
previousPhase={RightPanelPhases.RoomSummary}
|
previousPhase={RightPanelPhases.RoomSummary}
|
||||||
>
|
>
|
||||||
{
|
<TimelinePanel
|
||||||
this.state?.threads.map((thread: Thread) => {
|
ref={ref}
|
||||||
if (thread.ready) {
|
showReadReceipts={false} // No RR support in thread's MVP
|
||||||
return this.renderEventTile(thread.rootEvent);
|
manageReadReceipts={false} // No RR support in thread's MVP
|
||||||
}
|
manageReadMarkers={false} // No RM support in thread's MVP
|
||||||
})
|
sendReadReceiptOnLoad={false} // No RR support in thread's MVP
|
||||||
}
|
timelineSet={filteredTimelineSet}
|
||||||
|
showUrlPreview={true}
|
||||||
|
empty={<div>empty</div>}
|
||||||
|
alwaysShowTimestamps={true}
|
||||||
|
layout={Layout.Group}
|
||||||
|
hideThreadedMessages={false}
|
||||||
|
hidden={false}
|
||||||
|
showReactions={true}
|
||||||
|
className="mx_RoomView_messagePanel mx_GroupLayout"
|
||||||
|
membersLoaded={true}
|
||||||
|
tileShape={TileShape.ThreadPanel}
|
||||||
|
/>
|
||||||
</BaseCard>
|
</BaseCard>
|
||||||
);
|
</RoomContext.Provider>
|
||||||
}
|
);
|
||||||
}
|
};
|
||||||
|
export default ThreadPanel;
|
||||||
|
|
|
@ -156,7 +156,7 @@ export default class ThreadView extends React.Component<IProps, IState> {
|
||||||
<BaseCard
|
<BaseCard
|
||||||
className="mx_ThreadView"
|
className="mx_ThreadView"
|
||||||
onClose={this.props.onClose}
|
onClose={this.props.onClose}
|
||||||
previousPhase={RightPanelPhases.RoomSummary}
|
previousPhase={RightPanelPhases.ThreadPanel}
|
||||||
withoutScrollContainer={true}
|
withoutScrollContainer={true}
|
||||||
>
|
>
|
||||||
{ this.state.thread && (
|
{ this.state.thread && (
|
||||||
|
|
|
@ -31,6 +31,8 @@ import RightPanelStore from "../../../stores/RightPanelStore";
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
import { useSettingValue } from "../../../hooks/useSettings";
|
import { useSettingValue } from "../../../hooks/useSettings";
|
||||||
import { useReadPinnedEvents, usePinnedEvents } from './PinnedMessagesCard';
|
import { useReadPinnedEvents, usePinnedEvents } from './PinnedMessagesCard';
|
||||||
|
import { dispatchShowThreadsPanelEvent } from "../../../dispatcher/dispatch-actions/threads";
|
||||||
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
|
|
||||||
const ROOM_INFO_PHASES = [
|
const ROOM_INFO_PHASES = [
|
||||||
RightPanelPhases.RoomSummary,
|
RightPanelPhases.RoomSummary,
|
||||||
|
@ -122,6 +124,13 @@ export default class RoomHeaderButtons extends HeaderButtons<IProps> {
|
||||||
isHighlighted={this.isPhase(RightPanelPhases.PinnedMessages)}
|
isHighlighted={this.isPhase(RightPanelPhases.PinnedMessages)}
|
||||||
onClick={this.onPinnedMessagesClicked}
|
onClick={this.onPinnedMessagesClicked}
|
||||||
/>
|
/>
|
||||||
|
{ SettingsStore.getValue("feature_thread") && <HeaderButton
|
||||||
|
name="threadsButton"
|
||||||
|
title={_t("Threads")}
|
||||||
|
onClick={dispatchShowThreadsPanelEvent}
|
||||||
|
isHighlighted={this.isPhase(RightPanelPhases.ThreadPanel)}
|
||||||
|
analytics={['Right Panel', 'Threads List Button', 'click']}
|
||||||
|
/> }
|
||||||
<HeaderButton
|
<HeaderButton
|
||||||
name="notifsButton"
|
name="notifsButton"
|
||||||
title={_t('Notifications')}
|
title={_t('Notifications')}
|
||||||
|
|
|
@ -48,6 +48,7 @@ import { Container, MAX_PINNED, WidgetLayoutStore } from "../../../stores/widget
|
||||||
import RoomName from "../elements/RoomName";
|
import RoomName from "../elements/RoomName";
|
||||||
import UIStore from "../../../stores/UIStore";
|
import UIStore from "../../../stores/UIStore";
|
||||||
import ExportDialog from "../dialogs/ExportDialog";
|
import ExportDialog from "../dialogs/ExportDialog";
|
||||||
|
import { dispatchShowThreadsPanelEvent } from "../../../dispatcher/dispatch-actions/threads";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
room: Room;
|
room: Room;
|
||||||
|
@ -221,13 +222,6 @@ const onRoomFilesClick = () => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const onRoomThreadsClick = () => {
|
|
||||||
defaultDispatcher.dispatch<SetRightPanelPhasePayload>({
|
|
||||||
action: Action.SetRightPanelPhase,
|
|
||||||
phase: RightPanelPhases.ThreadPanel,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const onRoomSettingsClick = () => {
|
const onRoomSettingsClick = () => {
|
||||||
defaultDispatcher.dispatch({ action: "open_room_settings" });
|
defaultDispatcher.dispatch({ action: "open_room_settings" });
|
||||||
};
|
};
|
||||||
|
@ -291,7 +285,7 @@ const RoomSummaryCard: React.FC<IProps> = ({ room, onClose }) => {
|
||||||
{ _t("Export chat") }
|
{ _t("Export chat") }
|
||||||
</Button>
|
</Button>
|
||||||
{ SettingsStore.getValue("feature_thread") && (
|
{ SettingsStore.getValue("feature_thread") && (
|
||||||
<Button className="mx_RoomSummaryCard_icon_threads" onClick={onRoomThreadsClick}>
|
<Button className="mx_RoomSummaryCard_icon_threads" onClick={dispatchShowThreadsPanelEvent}>
|
||||||
{ _t("Show threads") }
|
{ _t("Show threads") }
|
||||||
</Button>
|
</Button>
|
||||||
) }
|
) }
|
||||||
|
|
|
@ -56,9 +56,9 @@ import ReadReceiptMarker from "./ReadReceiptMarker";
|
||||||
import MessageActionBar from "../messages/MessageActionBar";
|
import MessageActionBar from "../messages/MessageActionBar";
|
||||||
import ReactionsRow from '../messages/ReactionsRow';
|
import ReactionsRow from '../messages/ReactionsRow';
|
||||||
import { getEventDisplayInfo } from '../../../utils/EventUtils';
|
import { getEventDisplayInfo } from '../../../utils/EventUtils';
|
||||||
import { RightPanelPhases } from "../../../stores/RightPanelStorePhases";
|
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import MKeyVerificationConclusion from "../messages/MKeyVerificationConclusion";
|
import MKeyVerificationConclusion from "../messages/MKeyVerificationConclusion";
|
||||||
|
import { dispatchShowThreadEvent } from '../../../dispatcher/dispatch-actions/threads';
|
||||||
|
|
||||||
const eventTileTypes = {
|
const eventTileTypes = {
|
||||||
[EventType.RoomMessage]: 'messages.MessageEvent',
|
[EventType.RoomMessage]: 'messages.MessageEvent',
|
||||||
|
@ -193,6 +193,7 @@ export enum TileShape {
|
||||||
FileGrid = "file_grid",
|
FileGrid = "file_grid",
|
||||||
Pinned = "pinned",
|
Pinned = "pinned",
|
||||||
Thread = "thread",
|
Thread = "thread",
|
||||||
|
ThreadPanel = "thread_list"
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
|
@ -511,6 +512,10 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||||
if (this.props.showReactions) {
|
if (this.props.showReactions) {
|
||||||
this.props.mxEvent.removeListener("Event.relationsCreated", this.onReactionsCreated);
|
this.props.mxEvent.removeListener("Event.relationsCreated", this.onReactionsCreated);
|
||||||
}
|
}
|
||||||
|
if (SettingsStore.getValue("feature_thread")) {
|
||||||
|
this.props.mxEvent.off(ThreadEvent.Ready, this.updateThread);
|
||||||
|
this.props.mxEvent.off(ThreadEvent.Update, this.updateThread);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps, prevState, snapshot) {
|
componentDidUpdate(prevProps, prevState, snapshot) {
|
||||||
|
@ -541,13 +546,7 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||||
<div
|
<div
|
||||||
className="mx_ThreadInfo"
|
className="mx_ThreadInfo"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
dis.dispatch({
|
dispatchShowThreadEvent(this.props.mxEvent);
|
||||||
action: Action.SetRightPanelPhase,
|
|
||||||
phase: RightPanelPhases.ThreadView,
|
|
||||||
refireParams: {
|
|
||||||
event: this.props.mxEvent,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span className="mx_EventListSummary_avatars">
|
<span className="mx_EventListSummary_avatars">
|
||||||
|
|
|
@ -155,58 +155,55 @@ export default class RoomHeader extends React.Component<IProps> {
|
||||||
/>;
|
/>;
|
||||||
}
|
}
|
||||||
|
|
||||||
let forgetButton;
|
const buttons: JSX.Element[] = [];
|
||||||
if (this.props.onForgetClick) {
|
|
||||||
forgetButton =
|
|
||||||
<AccessibleTooltipButton
|
|
||||||
className="mx_RoomHeader_button mx_RoomHeader_forgetButton"
|
|
||||||
onClick={this.props.onForgetClick}
|
|
||||||
title={_t("Forget room")} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
let appsButton;
|
|
||||||
if (this.props.onAppsClick) {
|
|
||||||
appsButton =
|
|
||||||
<AccessibleTooltipButton
|
|
||||||
className={classNames("mx_RoomHeader_button mx_RoomHeader_appsButton", {
|
|
||||||
mx_RoomHeader_appsButton_highlight: this.props.appsShown,
|
|
||||||
})}
|
|
||||||
onClick={this.props.onAppsClick}
|
|
||||||
title={this.props.appsShown ? _t("Hide Widgets") : _t("Show Widgets")} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
let searchButton;
|
|
||||||
if (this.props.onSearchClick && this.props.inRoom) {
|
|
||||||
searchButton =
|
|
||||||
<AccessibleTooltipButton
|
|
||||||
className="mx_RoomHeader_button mx_RoomHeader_searchButton"
|
|
||||||
onClick={this.props.onSearchClick}
|
|
||||||
title={_t("Search")} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
let voiceCallButton;
|
|
||||||
let videoCallButton;
|
|
||||||
if (this.props.inRoom && SettingsStore.getValue("showCallButtonsInComposer")) {
|
if (this.props.inRoom && SettingsStore.getValue("showCallButtonsInComposer")) {
|
||||||
voiceCallButton =
|
const voiceCallButton = <AccessibleTooltipButton
|
||||||
<AccessibleTooltipButton
|
className="mx_RoomHeader_button mx_RoomHeader_voiceCallButton"
|
||||||
className="mx_RoomHeader_button mx_RoomHeader_voiceCallButton"
|
onClick={() => this.props.onCallPlaced(PlaceCallType.Voice)}
|
||||||
onClick={() => this.props.onCallPlaced(PlaceCallType.Voice)}
|
title={_t("Voice call")}
|
||||||
title={_t("Voice call")} />;
|
/>;
|
||||||
videoCallButton =
|
const videoCallButton = <AccessibleTooltipButton
|
||||||
<AccessibleTooltipButton
|
className="mx_RoomHeader_button mx_RoomHeader_videoCallButton"
|
||||||
className="mx_RoomHeader_button mx_RoomHeader_videoCallButton"
|
onClick={(ev: React.MouseEvent<Element>) => ev.shiftKey ?
|
||||||
onClick={(ev: React.MouseEvent<Element>) => ev.shiftKey ?
|
this.displayInfoDialogAboutScreensharing() : this.props.onCallPlaced(PlaceCallType.Video)}
|
||||||
this.displayInfoDialogAboutScreensharing() : this.props.onCallPlaced(PlaceCallType.Video)}
|
title={_t("Video call")}
|
||||||
title={_t("Video call")} />;
|
/>;
|
||||||
|
buttons.push(voiceCallButton, videoCallButton);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.props.onForgetClick) {
|
||||||
|
const forgetButton = <AccessibleTooltipButton
|
||||||
|
className="mx_RoomHeader_button mx_RoomHeader_forgetButton"
|
||||||
|
onClick={this.props.onForgetClick}
|
||||||
|
title={_t("Forget room")}
|
||||||
|
/>;
|
||||||
|
buttons.push(forgetButton);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.props.onAppsClick) {
|
||||||
|
const appsButton = <AccessibleTooltipButton
|
||||||
|
className={classNames("mx_RoomHeader_button mx_RoomHeader_appsButton", {
|
||||||
|
mx_RoomHeader_appsButton_highlight: this.props.appsShown,
|
||||||
|
})}
|
||||||
|
onClick={this.props.onAppsClick}
|
||||||
|
title={this.props.appsShown ? _t("Hide Widgets") : _t("Show Widgets")}
|
||||||
|
/>;
|
||||||
|
buttons.push(appsButton);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.props.onSearchClick && this.props.inRoom) {
|
||||||
|
const searchButton = <AccessibleTooltipButton
|
||||||
|
className="mx_RoomHeader_button mx_RoomHeader_searchButton"
|
||||||
|
onClick={this.props.onSearchClick}
|
||||||
|
title={_t("Search")}
|
||||||
|
/>;
|
||||||
|
buttons.push(searchButton);
|
||||||
}
|
}
|
||||||
|
|
||||||
const rightRow =
|
const rightRow =
|
||||||
<div className="mx_RoomHeader_buttons">
|
<div className="mx_RoomHeader_buttons">
|
||||||
{ videoCallButton }
|
{ buttons }
|
||||||
{ voiceCallButton }
|
|
||||||
{ forgetButton }
|
|
||||||
{ appsButton }
|
|
||||||
{ searchButton }
|
|
||||||
</div>;
|
</div>;
|
||||||
|
|
||||||
const e2eIcon = this.props.e2eStatus ? <E2EIcon status={this.props.e2eStatus} /> : undefined;
|
const e2eIcon = this.props.e2eStatus ? <E2EIcon status={this.props.e2eStatus} /> : undefined;
|
||||||
|
|
|
@ -21,9 +21,10 @@ import { Layout } from "../settings/Layout";
|
||||||
|
|
||||||
export enum TimelineRenderingType {
|
export enum TimelineRenderingType {
|
||||||
Room,
|
Room,
|
||||||
|
Thread,
|
||||||
|
ThreadsList,
|
||||||
File,
|
File,
|
||||||
Notification,
|
Notification,
|
||||||
Thread
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const RoomContext = createContext<IRoomState>({
|
const RoomContext = createContext<IRoomState>({
|
||||||
|
|
38
src/dispatcher/dispatch-actions/threads.ts
Normal file
38
src/dispatcher/dispatch-actions/threads.ts
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
/*
|
||||||
|
Copyright 2021 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 { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||||
|
import { RightPanelPhases } from "../../stores/RightPanelStorePhases";
|
||||||
|
import { Action } from "../actions";
|
||||||
|
import dis from '../dispatcher';
|
||||||
|
import { SetRightPanelPhasePayload } from "../payloads/SetRightPanelPhasePayload";
|
||||||
|
|
||||||
|
export const dispatchShowThreadEvent = (event: MatrixEvent) => {
|
||||||
|
dis.dispatch({
|
||||||
|
action: Action.SetRightPanelPhase,
|
||||||
|
phase: RightPanelPhases.ThreadView,
|
||||||
|
refireParams: {
|
||||||
|
event,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const dispatchShowThreadsPanelEvent = () => {
|
||||||
|
dis.dispatch<SetRightPanelPhasePayload>({
|
||||||
|
action: Action.SetRightPanelPhase,
|
||||||
|
phase: RightPanelPhases.ThreadPanel,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
|
@ -1831,6 +1831,7 @@
|
||||||
"Nothing pinned, yet": "Nothing pinned, yet",
|
"Nothing pinned, yet": "Nothing pinned, yet",
|
||||||
"If you have permissions, open the menu on any message and select <b>Pin</b> to stick them here.": "If you have permissions, open the menu on any message and select <b>Pin</b> to stick them here.",
|
"If you have permissions, open the menu on any message and select <b>Pin</b> to stick them here.": "If you have permissions, open the menu on any message and select <b>Pin</b> to stick them here.",
|
||||||
"Pinned messages": "Pinned messages",
|
"Pinned messages": "Pinned messages",
|
||||||
|
"Threads": "Threads",
|
||||||
"Room Info": "Room Info",
|
"Room Info": "Room Info",
|
||||||
"You can only pin up to %(count)s widgets|other": "You can only pin up to %(count)s widgets",
|
"You can only pin up to %(count)s widgets|other": "You can only pin up to %(count)s widgets",
|
||||||
"Unpin a widget to view it in this panel": "Unpin a widget to view it in this panel",
|
"Unpin a widget to view it in this panel": "Unpin a widget to view it in this panel",
|
||||||
|
@ -2974,6 +2975,11 @@
|
||||||
"You can add more later too, including already existing ones.": "You can add more later too, including already existing ones.",
|
"You can add more later too, including already existing ones.": "You can add more later too, including already existing ones.",
|
||||||
"What projects are you working on?": "What projects are you working on?",
|
"What projects are you working on?": "What projects are you working on?",
|
||||||
"We'll create rooms for each of them. You can add more later too, including already existing ones.": "We'll create rooms for each of them. You can add more later too, including already existing ones.",
|
"We'll create rooms for each of them. You can add more later too, including already existing ones.": "We'll create rooms for each of them. You can add more later too, including already existing ones.",
|
||||||
|
"My threads": "My threads",
|
||||||
|
"Shows all threads you’ve participated in": "Shows all threads you’ve participated in",
|
||||||
|
"All threads": "All threads",
|
||||||
|
"Shows all threads from current room": "Shows all threads from current room",
|
||||||
|
"Show:": "Show:",
|
||||||
"Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.": "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.",
|
"Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.": "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.",
|
||||||
"Tried to load a specific point in this room's timeline, but was unable to find it.": "Tried to load a specific point in this room's timeline, but was unable to find it.",
|
"Tried to load a specific point in this room's timeline, but was unable to find it.": "Tried to load a specific point in this room's timeline, but was unable to find it.",
|
||||||
"Failed to load timeline position": "Failed to load timeline position",
|
"Failed to load timeline position": "Failed to load timeline position",
|
||||||
|
|
81
test/components/structures/ThreadPanel-test.tsx
Normal file
81
test/components/structures/ThreadPanel-test.tsx
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
/*
|
||||||
|
Copyright 2021 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 { shallow, mount, configure } from "enzyme";
|
||||||
|
import '../../skinned-sdk';
|
||||||
|
import Adapter from "@wojtekmaj/enzyme-adapter-react-17";
|
||||||
|
|
||||||
|
import {
|
||||||
|
ThreadFilterType,
|
||||||
|
ThreadPanelHeader,
|
||||||
|
ThreadPanelHeaderFilterOptionItem,
|
||||||
|
} from '../../../src/components/structures/ThreadPanel';
|
||||||
|
import { ContextMenuButton } from '../../../src/accessibility/context_menu/ContextMenuButton';
|
||||||
|
import ContextMenu from '../../../src/components/structures/ContextMenu';
|
||||||
|
import { _t } from '../../../src/languageHandler';
|
||||||
|
|
||||||
|
configure({ adapter: new Adapter() });
|
||||||
|
|
||||||
|
describe('ThreadPanel', () => {
|
||||||
|
describe('Header', () => {
|
||||||
|
it('expect that All filter for ThreadPanelHeader properly renders Show: All threads', () => {
|
||||||
|
const wrapper = shallow(
|
||||||
|
<ThreadPanelHeader
|
||||||
|
filterOption={ThreadFilterType.All}
|
||||||
|
setFilterOption={() => undefined} />,
|
||||||
|
);
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('expect that My filter for ThreadPanelHeader properly renders Show: My threads', () => {
|
||||||
|
const wrapper = shallow(
|
||||||
|
<ThreadPanelHeader
|
||||||
|
filterOption={ThreadFilterType.My}
|
||||||
|
setFilterOption={() => undefined} />,
|
||||||
|
);
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('expect that ThreadPanelHeader properly opens a context menu when clicked on the button', () => {
|
||||||
|
const wrapper = mount(
|
||||||
|
<ThreadPanelHeader
|
||||||
|
filterOption={ThreadFilterType.All}
|
||||||
|
setFilterOption={() => undefined} />,
|
||||||
|
);
|
||||||
|
const found = wrapper.find(ContextMenuButton);
|
||||||
|
expect(found).not.toBe(undefined);
|
||||||
|
expect(found).not.toBe(null);
|
||||||
|
expect(wrapper.exists(ContextMenu)).toEqual(false);
|
||||||
|
found.simulate('click');
|
||||||
|
expect(wrapper.exists(ContextMenu)).toEqual(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('expect that ThreadPanelHeader has the correct option selected in the context menu', () => {
|
||||||
|
const wrapper = mount(
|
||||||
|
<ThreadPanelHeader
|
||||||
|
filterOption={ThreadFilterType.All}
|
||||||
|
setFilterOption={() => undefined} />,
|
||||||
|
);
|
||||||
|
wrapper.find(ContextMenuButton).simulate('click');
|
||||||
|
const found = wrapper.find(ThreadPanelHeaderFilterOptionItem);
|
||||||
|
expect(found.length).toEqual(2);
|
||||||
|
const foundButton = found.find('[aria-selected=true]').first();
|
||||||
|
expect(foundButton.text()).toEqual(`${_t("All threads")}${_t('Shows all threads from current room')}`);
|
||||||
|
expect(foundButton).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,71 @@
|
||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`ThreadPanel Header expect that All filter for ThreadPanelHeader properly renders Show: All threads 1`] = `
|
||||||
|
<div
|
||||||
|
className="mx_ThreadPanel__header"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
Threads
|
||||||
|
</span>
|
||||||
|
<ContextMenuButton
|
||||||
|
inputRef={
|
||||||
|
Object {
|
||||||
|
"current": null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
isExpanded={false}
|
||||||
|
onClick={[Function]}
|
||||||
|
>
|
||||||
|
Show: All threads
|
||||||
|
</ContextMenuButton>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`ThreadPanel Header expect that My filter for ThreadPanelHeader properly renders Show: My threads 1`] = `
|
||||||
|
<div
|
||||||
|
className="mx_ThreadPanel__header"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
Threads
|
||||||
|
</span>
|
||||||
|
<ContextMenuButton
|
||||||
|
inputRef={
|
||||||
|
Object {
|
||||||
|
"current": null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
isExpanded={false}
|
||||||
|
onClick={[Function]}
|
||||||
|
>
|
||||||
|
Show: My threads
|
||||||
|
</ContextMenuButton>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`ThreadPanel Header expect that ThreadPanelHeader has the correct option selected in the context menu 1`] = `
|
||||||
|
<AccessibleButton
|
||||||
|
aria-selected={true}
|
||||||
|
className="mx_ThreadPanel_Header_FilterOptionItem"
|
||||||
|
element="div"
|
||||||
|
onClick={[Function]}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
aria-selected={true}
|
||||||
|
className="mx_AccessibleButton mx_ThreadPanel_Header_FilterOptionItem"
|
||||||
|
onClick={[Function]}
|
||||||
|
onKeyDown={[Function]}
|
||||||
|
onKeyUp={[Function]}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
All threads
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
Shows all threads from current room
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</AccessibleButton>
|
||||||
|
`;
|
Loading…
Add table
Add a link
Reference in a new issue