diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index 1c9d8e87d9..1adfc0cde3 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -670,3 +670,33 @@ $hover-select-border: 4px; margin-right: 0; } } + + +.mx_ThreadView { + + display: flex; + flex-direction: column; + + .mx_ThreadView_List { + flex: 1; + overflow: scroll; + } + + .mx_EventTile_roomName { + display: none; + } + + .mx_EventTile_line { + padding-left: 0 !important; + order: 10 !important; + } + .mx_EventTile { + width: 100%; + display: flex; + flex-direction: column; + border-bottom: 1px solid #888; + margin-top: 0; + padding-bottom: 5px; + margin-bottom: 5px; + } +} diff --git a/res/img/element-icons/message/thread.svg b/res/img/element-icons/message/thread.svg new file mode 100644 index 0000000000..b4a7cc0066 --- /dev/null +++ b/res/img/element-icons/message/thread.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/components/structures/MessagePanel.tsx b/src/components/structures/MessagePanel.tsx index 1691d90651..15275328a6 100644 --- a/src/components/structures/MessagePanel.tsx +++ b/src/components/structures/MessagePanel.tsx @@ -173,6 +173,8 @@ interface IProps { onUnfillRequest?(backwards: boolean, scrollToken: string): void; getRelationsForEvent?(eventId: string, relationType: string, eventType: string): Relations; + + hideThreadedMessages?: boolean; } interface IState { @@ -443,6 +445,11 @@ export default class MessagePanel extends React.Component { // Always show highlighted event if (this.props.highlightedEventId === mxEv.getId()) return true; + const threadingEnabled = SettingsStore.getValue("feature_threading"); + if (threadingEnabled && mxEv.replyEventId && this.props.hideThreadedMessages === true) { + return false; + } + return !shouldHideEvent(mxEv, this.context); } diff --git a/src/components/structures/ThreadView.tsx b/src/components/structures/ThreadView.tsx index 0f9d499884..f34be87c95 100644 --- a/src/components/structures/ThreadView.tsx +++ b/src/components/structures/ThreadView.tsx @@ -24,9 +24,11 @@ import { replaceableComponent } from "../../utils/replaceableComponent"; import { MatrixClientPeg } from '../../MatrixClientPeg'; import ResizeNotifier from '../../utils/ResizeNotifier'; -import EventTile from '../views/rooms/EventTile'; +import EventTile, { TileShape } from '../views/rooms/EventTile'; import MessageComposer from '../views/rooms/MessageComposer'; import { RoomPermalinkCreator } from '../../utils/permalinks/Permalinks'; +import { Layout } from '../../settings/Layout'; +import TimelinePanel from './TimelinePanel'; interface IProps { roomId: string; @@ -37,6 +39,7 @@ interface IProps { } interface IState { + replyToEvent?: MatrixEvent; } /* @@ -65,6 +68,7 @@ class ThreadView extends React.Component { mxEvent={event} enableFlair={false} showReadReceipts={false} + tileShape={TileShape.FileGrid} as="div" />; } @@ -77,19 +81,24 @@ class ThreadView extends React.Component { className="mx_ThreadView" onClose={this.props.onClose} previousPhase={RightPanelPhases.RoomSummary} + withoutScrollContainer={true} > - { this.renderEventTile(this.props.mxEvent) } - - { thread && ( - thread.eventTimeline.map((event: MatrixEvent) => { - return this.renderEventTile(event); - }) - ) } - + empty} + alwaysShowTimestamps={true} + layout={Layout.Group} + hideThreadedMessages={false} + /> diff --git a/src/components/structures/TimelinePanel.tsx b/src/components/structures/TimelinePanel.tsx index f62676a4fc..e5fa6967dc 100644 --- a/src/components/structures/TimelinePanel.tsx +++ b/src/components/structures/TimelinePanel.tsx @@ -126,6 +126,8 @@ interface IProps { // callback which is called when we wish to paginate the timeline window. onPaginationRequest?(timelineWindow: TimelineWindow, direction: string, size: number): Promise; + + hideThreadedMessages?: boolean; } interface IState { @@ -214,6 +216,7 @@ class TimelinePanel extends React.Component { timelineCap: Number.MAX_VALUE, className: 'mx_RoomView_messagePanel', sendReadReceiptOnLoad: true, + hideThreadedMessages: true, }; private lastRRSentEventId: string = undefined; @@ -1511,6 +1514,7 @@ class TimelinePanel extends React.Component { showReactions={this.props.showReactions} layout={this.props.layout} enableFlair={SettingsStore.getValue(UIFeature.Flair)} + hideThreadedMessages={this.props.hideThreadedMessages} /> ); } diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index 884d004551..30355e55bf 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -55,6 +55,7 @@ import ReadReceiptMarker from "./ReadReceiptMarker"; import MessageActionBar from "../messages/MessageActionBar"; import ReactionsRow from '../messages/ReactionsRow'; import { getEventDisplayInfo } from '../../../utils/EventUtils'; +import { RightPanelPhases } from "../../../stores/RightPanelStorePhases"; const eventTileTypes = { [EventType.RoomMessage]: 'messages.MessageEvent', @@ -299,6 +300,9 @@ interface IProps { // whether or not to display the sender hideSender?: boolean; + + // whether or not to display thread info + showThreadInfo?: boolean; } interface IState { @@ -451,6 +455,7 @@ export default class EventTile extends React.Component { client.on("Room.receipt", this.onRoomReceipt); this.isListeningForReceipts = true; } + this.props.mxEvent.on("Thread.update", this.forceUpdate); } // TODO: [REACT-WARNING] Replace with appropriate lifecycle event @@ -491,6 +496,38 @@ export default class EventTile extends React.Component { } } + private renderThreadInfo(): React.ReactNode { + const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId()); + const thread = room.getThread(this.props.mxEvent.getId()); + if (!thread || this.props.showThreadInfo === false) { + return null; + } + + const avatars = Array.from(thread.participants).map((mxId: string) => { + const member = room.getMember(mxId); + return ; + }); + + return ( +
{ + dis.dispatch({ + action: Action.SetRightPanelPhase, + phase: RightPanelPhases.ThreadView, + refireParams: { + event: this.props.mxEvent, + }, + }); + }} + > + + { avatars } + + { thread.length } { thread.length === 1 ? 'reply' : 'replies' } +
+ ); + } + private onRoomReceipt = (ev, room) => { // ignore events for other rooms const tileRoom = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId()); @@ -1167,6 +1204,7 @@ export default class EventTile extends React.Component { { keyRequestInfo } { actionBar } { this.props.layout === Layout.IRC && (reactionsRow) } + { this.renderThreadInfo() } { this.props.layout !== Layout.IRC && (reactionsRow) } { msgOption } diff --git a/src/components/views/rooms/MessageComposer.tsx b/src/components/views/rooms/MessageComposer.tsx index 8455e9aa11..bd53c9566a 100644 --- a/src/components/views/rooms/MessageComposer.tsx +++ b/src/components/views/rooms/MessageComposer.tsx @@ -183,6 +183,7 @@ interface IProps { resizeNotifier: ResizeNotifier; permalinkCreator: RoomPermalinkCreator; replyToEvent?: MatrixEvent; + showReplyPreview?: boolean; e2eStatus?: E2EStatus; } @@ -201,6 +202,10 @@ export default class MessageComposer extends React.Component { private messageComposerInput: SendMessageComposer; private voiceRecordingButton: VoiceRecordComposerTile; + static defaultProps = { + showReplyPreview: true, + }; + constructor(props) { super(props); VoiceRecordingStore.instance.on(UPDATE_EVENT, this.onVoiceStoreUpdate); @@ -454,7 +459,9 @@ export default class MessageComposer extends React.Component {
{ recordingTooltip }
- + { this.props.showReplyPreview && ( + + ) }
{ controls }