Allow bubble layout in Thread View (#7478)

This commit is contained in:
Michael Telatynski 2022-01-12 09:02:30 +00:00 committed by GitHub
parent 038a6bc204
commit f5465b37a9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 144 additions and 99 deletions

View file

@ -180,7 +180,7 @@ limitations under the License.
border-top-left-radius: var(--cornerRadius); border-top-left-radius: var(--cornerRadius);
border-top-right-radius: var(--cornerRadius); border-top-right-radius: var(--cornerRadius);
> a { > a, .mx_MessageTimestamp {
position: absolute; position: absolute;
padding: 4px 8px; padding: 4px 8px;
bottom: 0; bottom: 0;
@ -375,7 +375,8 @@ limitations under the License.
margin-left: 9px; margin-left: 9px;
} }
.mx_EventTile_line > a { .mx_EventTile_line > a,
.mx_EventTile_line .mx_MessageTimestamp {
// Align timestamps with those of normal bubble tiles // Align timestamps with those of normal bubble tiles
right: auto; right: auto;
top: -11px; top: -11px;

View file

@ -841,24 +841,6 @@ $left-gutter: 64px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
.mx_EventTile_senderDetails {
display: flex;
align-items: center;
gap: calc(6px + $selected-message-border-width);
a {
flex: 1;
min-width: none;
max-width: 100%;
display: flex;
align-items: center;
.mx_SenderProfile {
flex: 1;
}
}
}
.mx_ThreadView_List { .mx_ThreadView_List {
flex: 1; flex: 1;
overflow: scroll; overflow: scroll;
@ -869,7 +851,6 @@ $left-gutter: 64px;
} }
.mx_EventTile { .mx_EventTile {
width: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding-top: 0; padding-top: 0;
@ -880,11 +861,7 @@ $left-gutter: 64px;
} }
.mx_MessageTimestamp { .mx_MessageTimestamp {
left: auto; font-size: $font-10px;
right: 2px !important;
top: 1px !important;
font-size: 1rem;
text-align: right;
} }
.mx_ReactionsRow { .mx_ReactionsRow {
@ -896,16 +873,63 @@ $left-gutter: 64px;
} }
} }
.mx_EventTile_content, .mx_EventTile[data-layout=bubble] {
.mx_RedactedBody,
.mx_ReplyChain_wrapper {
margin-left: 36px; margin-left: 36px;
margin-right: 50px; margin-right: 36px;
&[data-self=true] {
align-items: flex-end;
.mx_EventTile_line.mx_EventTile_mediaLine {
margin: 0 -13px 0 0; // align with normal messages
padding: 0 !important;
.mx_MFileBody ~ .mx_MessageTimestamp {
bottom: calc($font-14px + 4px); // above the Decrypt/Download text line
}
}
}
}
.mx_EventTile[data-layout=group] {
width: 100%;
.mx_EventTile_content, .mx_EventTile_content,
.mx_RedactedBody, .mx_RedactedBody,
.mx_MImageBody { .mx_ReplyChain_wrapper {
margin: 0; margin-left: 36px;
margin-right: 50px;
.mx_EventTile_content,
.mx_RedactedBody,
.mx_MImageBody {
margin: 0;
}
}
.mx_MessageTimestamp {
left: auto;
right: 2px !important;
top: 1px !important;
text-align: right;
}
.mx_EventTile_senderDetails {
display: flex;
align-items: center;
gap: calc(6px + $selected-message-border-width);
a {
flex: 1;
min-width: none;
max-width: 100%;
display: flex;
align-items: center;
.mx_SenderProfile {
flex: 1;
}
}
} }
} }

View file

@ -18,6 +18,7 @@ import React from 'react';
import { IEventRelation, MatrixEvent, Room } from 'matrix-js-sdk/src'; import { IEventRelation, MatrixEvent, Room } from 'matrix-js-sdk/src';
import { Thread, ThreadEvent } from 'matrix-js-sdk/src/models/thread'; import { Thread, ThreadEvent } from 'matrix-js-sdk/src/models/thread';
import { RelationType } from 'matrix-js-sdk/src/@types/event'; import { RelationType } from 'matrix-js-sdk/src/@types/event';
import classNames from "classnames";
import BaseCard from "../views/right_panel/BaseCard"; import BaseCard from "../views/right_panel/BaseCard";
import { RightPanelPhases } from "../../stores/right-panel/RightPanelStorePhases"; import { RightPanelPhases } from "../../stores/right-panel/RightPanelStorePhases";
@ -40,6 +41,7 @@ import UploadBar from './UploadBar';
import { _t } from '../../languageHandler'; import { _t } from '../../languageHandler';
import ThreadListContextMenu from '../views/context_menus/ThreadListContextMenu'; import ThreadListContextMenu from '../views/context_menus/ThreadListContextMenu';
import RightPanelStore from '../../stores/right-panel/RightPanelStore'; import RightPanelStore from '../../stores/right-panel/RightPanelStore';
import SettingsStore from "../../settings/SettingsStore";
interface IProps { interface IProps {
room: Room; room: Room;
@ -53,6 +55,7 @@ interface IProps {
} }
interface IState { interface IState {
thread?: Thread; thread?: Thread;
layout: Layout;
editState?: EditorStateTransfer; editState?: EditorStateTransfer;
replyToEvent?: MatrixEvent; replyToEvent?: MatrixEvent;
} }
@ -63,10 +66,17 @@ export default class ThreadView extends React.Component<IProps, IState> {
private dispatcherRef: string; private dispatcherRef: string;
private timelinePanelRef: React.RefObject<TimelinePanel> = React.createRef(); private timelinePanelRef: React.RefObject<TimelinePanel> = React.createRef();
private readonly layoutWatcherRef: string;
constructor(props: IProps) { constructor(props: IProps) {
super(props); super(props);
this.state = {}; this.state = {
layout: SettingsStore.getValue("layout"),
};
this.layoutWatcherRef = SettingsStore.watchSetting("layout", null, (...[,,, value]) =>
this.setState({ layout: value as Layout }),
);
} }
public componentDidMount(): void { public componentDidMount(): void {
@ -82,6 +92,7 @@ export default class ThreadView extends React.Component<IProps, IState> {
dis.unregister(this.dispatcherRef); dis.unregister(this.dispatcherRef);
const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId()); const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId());
room.removeListener(ThreadEvent.New, this.onNewThread); room.removeListener(ThreadEvent.New, this.onNewThread);
SettingsStore.unwatchSetting(this.layoutWatcherRef);
} }
public componentDidUpdate(prevProps) { public componentDidUpdate(prevProps) {
@ -192,6 +203,12 @@ export default class ThreadView extends React.Component<IProps, IState> {
event_id: this.state.thread?.id, event_id: this.state.thread?.id,
}; };
const messagePanelClassNames = classNames(
"mx_RoomView_messagePanel",
{
"mx_GroupLayout": this.state.layout === Layout.Group,
});
return ( return (
<RoomContext.Provider value={{ <RoomContext.Provider value={{
...this.context, ...this.context,
@ -216,11 +233,12 @@ export default class ThreadView extends React.Component<IProps, IState> {
timelineSet={this.state?.thread?.timelineSet} timelineSet={this.state?.thread?.timelineSet}
showUrlPreview={true} showUrlPreview={true}
tileShape={TileShape.Thread} tileShape={TileShape.Thread}
layout={Layout.Group} // ThreadView doesn't support IRC layout at this time
layout={this.state.layout === Layout.Bubble ? Layout.Bubble : Layout.Group}
hideThreadedMessages={false} hideThreadedMessages={false}
hidden={false} hidden={false}
showReactions={true} showReactions={true}
className="mx_RoomView_messagePanel mx_GroupLayout" className={messagePanelClassNames}
permalinkCreator={this.props.permalinkCreator} permalinkCreator={this.props.permalinkCreator}
membersLoaded={true} membersLoaded={true}
editState={this.state.editState} editState={this.state.editState}

View file

@ -23,7 +23,7 @@ import { Relations } from "matrix-js-sdk/src/models/relations";
import { RoomMember } from "matrix-js-sdk/src/models/room-member"; import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import { Thread, ThreadEvent } from 'matrix-js-sdk/src/models/thread'; import { Thread, ThreadEvent } from 'matrix-js-sdk/src/models/thread';
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
import { NotificationCountType } from 'matrix-js-sdk/src/models/room'; import { NotificationCountType, Room } from 'matrix-js-sdk/src/models/room';
import { POLL_START_EVENT_TYPE } from "matrix-js-sdk/src/@types/polls"; import { POLL_START_EVENT_TYPE } from "matrix-js-sdk/src/@types/polls";
import ReplyChain from "../elements/ReplyChain"; import ReplyChain from "../elements/ReplyChain";
@ -129,16 +129,15 @@ for (const evType of ALL_RULE_TYPES) {
stateEventTileTypes[evType] = 'messages.TextualEvent'; stateEventTileTypes[evType] = 'messages.TextualEvent';
} }
export function getHandlerTile(ev) { export function getHandlerTile(ev: MatrixEvent): string {
const type = ev.getType(); const type = ev.getType();
// don't show verification requests we're not involved in, // don't show verification requests we're not involved in,
// not even when showing hidden events // not even when showing hidden events
if (type === "m.room.message") { if (type === EventType.RoomMessage) {
const content = ev.getContent(); const content = ev.getContent();
if (content && content.msgtype === MsgType.KeyVerificationRequest) { if (content && content.msgtype === MsgType.KeyVerificationRequest) {
const client = MatrixClientPeg.get(); const me = MatrixClientPeg.get()?.getUserId();
const me = client && client.getUserId();
if (ev.getSender() !== me && content.to !== me) { if (ev.getSender() !== me && content.to !== me) {
return undefined; return undefined;
} else { } else {
@ -148,20 +147,19 @@ export function getHandlerTile(ev) {
} }
// these events are sent by both parties during verification, but we only want to render one // these events are sent by both parties during verification, but we only want to render one
// tile once the verification concludes, so filter out the one from the other party. // tile once the verification concludes, so filter out the one from the other party.
if (type === "m.key.verification.done") { if (type === EventType.KeyVerificationDone) {
const client = MatrixClientPeg.get(); const me = MatrixClientPeg.get()?.getUserId();
const me = client && client.getUserId();
if (ev.getSender() !== me) { if (ev.getSender() !== me) {
return undefined; return undefined;
} }
} }
// sometimes MKeyVerificationConclusion declines to render. Jankily decline to render and // sometimes MKeyVerificationConclusion declines to render. Jankily decline to render and
// fall back to showing hidden events, if we're viewing hidden events // fall back to showing hidden events, if we're viewing hidden events
// XXX: This is extremely a hack. Possibly these components should have an interface for // XXX: This is extremely a hack. Possibly these components should have an interface for
// declining to render? // declining to render?
if (type === "m.key.verification.cancel" || type === "m.key.verification.done") { if (type === EventType.KeyVerificationCancel || type === EventType.KeyVerificationDone) {
if (!MKeyVerificationConclusion.shouldRender(ev, ev.request)) { if (!MKeyVerificationConclusion.shouldRender(ev, ev.verificationRequest)) {
return; return;
} }
} }
@ -375,8 +373,9 @@ export default class EventTile extends React.Component<IProps, IState> {
}; };
static contextType = MatrixClientContext; static contextType = MatrixClientContext;
public context!: React.ContextType<typeof MatrixClientContext>;
constructor(props, context) { constructor(props: IProps, context: React.ContextType<typeof MatrixClientContext>) {
super(props, context); super(props, context);
this.state = { this.state = {
@ -424,7 +423,7 @@ export default class EventTile extends React.Component<IProps, IState> {
// Quickly check to see if the event was sent by us. If it wasn't, it won't qualify for // Quickly check to see if the event was sent by us. If it wasn't, it won't qualify for
// special read receipts. // special read receipts.
const myUserId = MatrixClientPeg.get().getUserId(); const myUserId = this.context.getUserId();
if (this.props.mxEvent.getSender() !== myUserId) return false; if (this.props.mxEvent.getSender() !== myUserId) return false;
// Finally, determine if the type is relevant to the user. This notably excludes state // Finally, determine if the type is relevant to the user. This notably excludes state
@ -455,7 +454,7 @@ export default class EventTile extends React.Component<IProps, IState> {
// If anyone has read the event besides us, we don't want to show a sent receipt. // If anyone has read the event besides us, we don't want to show a sent receipt.
const receipts = this.props.readReceipts || []; const receipts = this.props.readReceipts || [];
const myUserId = MatrixClientPeg.get().getUserId(); const myUserId = this.context.getUserId();
if (receipts.some(r => r.userId !== myUserId)) return false; if (receipts.some(r => r.userId !== myUserId)) return false;
// Finally, we should show a receipt. // Finally, we should show a receipt.
@ -511,7 +510,7 @@ export default class EventTile extends React.Component<IProps, IState> {
room?.on(ThreadEvent.New, this.onNewThread); room?.on(ThreadEvent.New, this.onNewThread);
} }
private setupNotificationListener = (thread): void => { private setupNotificationListener = (thread: Thread): void => {
const room = this.context.getRoom(this.props.mxEvent.getRoomId()); const room = this.context.getRoom(this.props.mxEvent.getRoomId());
const notifications = RoomNotificationStateStore.instance.getThreadsRoomState(room); const notifications = RoomNotificationStateStore.instance.getThreadsRoomState(room);
@ -537,7 +536,7 @@ export default class EventTile extends React.Component<IProps, IState> {
}); });
}; };
private updateThread = (thread) => { private updateThread = (thread: Thread) => {
if (thread !== this.state.thread) { if (thread !== this.state.thread) {
if (this.threadState) { if (this.threadState) {
this.threadState.off(NotificationStateEvents.Update, this.onThreadStateUpdate); this.threadState.off(NotificationStateEvents.Update, this.onThreadStateUpdate);
@ -554,7 +553,7 @@ export default class EventTile extends React.Component<IProps, IState> {
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event // TODO: [REACT-WARNING] Replace with appropriate lifecycle event
// eslint-disable-next-line // eslint-disable-next-line
UNSAFE_componentWillReceiveProps(nextProps) { UNSAFE_componentWillReceiveProps(nextProps: IProps) {
// re-check the sender verification as outgoing events progress through // re-check the sender verification as outgoing events progress through
// the send process. // the send process.
if (nextProps.eventSendStatus !== this.props.eventSendStatus) { if (nextProps.eventSendStatus !== this.props.eventSendStatus) {
@ -562,7 +561,7 @@ export default class EventTile extends React.Component<IProps, IState> {
} }
} }
shouldComponentUpdate(nextProps, nextState, nextContext) { shouldComponentUpdate(nextProps: IProps, nextState: IState): boolean {
if (objectHasDiff(this.state, nextState)) { if (objectHasDiff(this.state, nextState)) {
return true; return true;
} }
@ -592,7 +591,7 @@ export default class EventTile extends React.Component<IProps, IState> {
} }
} }
componentDidUpdate(prevProps, prevState, snapshot) { componentDidUpdate(prevProps: IProps, prevState: IState, snapshot) {
// If we're not listening for receipts and expect to be, register a listener. // If we're not listening for receipts and expect to be, register a listener.
if (!this.isListeningForReceipts && (this.shouldShowSentReceipt || this.shouldShowSendingReceipt)) { if (!this.isListeningForReceipts && (this.shouldShowSentReceipt || this.shouldShowSendingReceipt)) {
this.context.on("Room.receipt", this.onRoomReceipt); this.context.on("Room.receipt", this.onRoomReceipt);
@ -619,7 +618,7 @@ export default class EventTile extends React.Component<IProps, IState> {
* We currently have no reliable way to discover than an event is a thread * We currently have no reliable way to discover than an event is a thread
* when we are at the sync stage * when we are at the sync stage
*/ */
const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId()); const room = this.context.getRoom(this.props.mxEvent.getRoomId());
const thread = room?.threads.get(this.props.mxEvent.getId()); const thread = room?.threads.get(this.props.mxEvent.getId());
if (!thread || thread.length === 0) { if (!thread || thread.length === 0) {
@ -692,9 +691,9 @@ export default class EventTile extends React.Component<IProps, IState> {
); );
} }
private onRoomReceipt = (ev, room) => { private onRoomReceipt = (ev: MatrixEvent, room: Room): void => {
// ignore events for other rooms // ignore events for other rooms
const tileRoom = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId()); const tileRoom = this.context.getRoom(this.props.mxEvent.getRoomId());
if (room !== tileRoom) return; if (room !== tileRoom) return;
if (!this.shouldShowSentReceipt && !this.shouldShowSendingReceipt && !this.isListeningForReceipts) { if (!this.shouldShowSentReceipt && !this.shouldShowSendingReceipt && !this.isListeningForReceipts) {
@ -722,19 +721,19 @@ export default class EventTile extends React.Component<IProps, IState> {
this.forceUpdate(); this.forceUpdate();
}; };
private onDeviceVerificationChanged = (userId, device) => { private onDeviceVerificationChanged = (userId: string, device: string): void => {
if (userId === this.props.mxEvent.getSender()) { if (userId === this.props.mxEvent.getSender()) {
this.verifyEvent(this.props.mxEvent); this.verifyEvent(this.props.mxEvent);
} }
}; };
private onUserVerificationChanged = (userId, _trustStatus) => { private onUserVerificationChanged = (userId: string, _trustStatus: string): void => {
if (userId === this.props.mxEvent.getSender()) { if (userId === this.props.mxEvent.getSender()) {
this.verifyEvent(this.props.mxEvent); this.verifyEvent(this.props.mxEvent);
} }
}; };
private async verifyEvent(mxEvent) { private async verifyEvent(mxEvent: MatrixEvent): Promise<void> {
if (!mxEvent.isEncrypted()) { if (!mxEvent.isEncrypted()) {
return; return;
} }
@ -788,7 +787,7 @@ export default class EventTile extends React.Component<IProps, IState> {
}, this.props.onHeightChanged); // Decryption may have caused a change in size }, this.props.onHeightChanged); // Decryption may have caused a change in size
} }
private propsEqual(objA, objB) { private propsEqual(objA: IProps, objB: IProps): boolean {
const keysA = Object.keys(objA); const keysA = Object.keys(objA);
const keysB = Object.keys(objB); const keysB = Object.keys(objB);
@ -836,7 +835,7 @@ export default class EventTile extends React.Component<IProps, IState> {
return true; return true;
} }
shouldHighlight() { private shouldHighlight(): boolean {
if (this.props.forExport) return false; if (this.props.forExport) return false;
const actions = this.context.getPushActionsForEvent(this.props.mxEvent.replacingEvent() || this.props.mxEvent); const actions = this.context.getPushActionsForEvent(this.props.mxEvent.replacingEvent() || this.props.mxEvent);
if (!actions || !actions.tweaks) { return false; } if (!actions || !actions.tweaks) { return false; }
@ -849,13 +848,13 @@ export default class EventTile extends React.Component<IProps, IState> {
return actions.tweaks.highlight; return actions.tweaks.highlight;
} }
toggleAllReadAvatars = () => { private toggleAllReadAvatars = () => {
this.setState({ this.setState({
allReadAvatars: !this.state.allReadAvatars, allReadAvatars: !this.state.allReadAvatars,
}); });
}; };
getReadAvatars() { private getReadAvatars() {
if (this.shouldShowSentReceipt || this.shouldShowSendingReceipt) { if (this.shouldShowSentReceipt || this.shouldShowSendingReceipt) {
return <SentReceipt messageState={this.props.mxEvent.getAssociatedStatus()} />; return <SentReceipt messageState={this.props.mxEvent.getAssociatedStatus()} />;
} }
@ -928,7 +927,8 @@ export default class EventTile extends React.Component<IProps, IState> {
/>, />,
); );
} }
let remText;
let remText: JSX.Element;
if (!this.state.allReadAvatars) { if (!this.state.allReadAvatars) {
const remainder = receipts.length - MAX_READ_AVATARS; const remainder = receipts.length - MAX_READ_AVATARS;
if (remainder > 0) { if (remainder > 0) {
@ -950,7 +950,7 @@ export default class EventTile extends React.Component<IProps, IState> {
); );
} }
onSenderProfileClick = () => { private onSenderProfileClick = () => {
if (!this.props.timelineRenderingType) return; if (!this.props.timelineRenderingType) return;
dis.dispatch<ComposerInsertPayload>({ dis.dispatch<ComposerInsertPayload>({
action: Action.ComposerInsert, action: Action.ComposerInsert,
@ -959,7 +959,7 @@ export default class EventTile extends React.Component<IProps, IState> {
}); });
}; };
onRequestKeysClick = () => { private onRequestKeysClick = () => {
this.setState({ this.setState({
// Indicate in the UI that the keys have been requested (this is expected to // Indicate in the UI that the keys have been requested (this is expected to
// be reset if the component is mounted in the future). // be reset if the component is mounted in the future).
@ -972,7 +972,7 @@ export default class EventTile extends React.Component<IProps, IState> {
this.context.cancelAndResendEventRoomKeyRequest(this.props.mxEvent); this.context.cancelAndResendEventRoomKeyRequest(this.props.mxEvent);
}; };
onPermalinkClicked = e => { private onPermalinkClicked = e => {
// This allows the permalink to be opened in a new tab/window or copied as // This allows the permalink to be opened in a new tab/window or copied as
// matrix.to, but also for it to enable routing within Element when clicked. // matrix.to, but also for it to enable routing within Element when clicked.
e.preventDefault(); e.preventDefault();
@ -1027,17 +1027,16 @@ export default class EventTile extends React.Component<IProps, IState> {
return null; return null;
} }
onActionBarFocusChange = focused => { private onActionBarFocusChange = (actionBarFocused: boolean) => {
this.setState({ this.setState({ actionBarFocused });
actionBarFocused: focused,
});
}; };
// TODO: Types // TODO: Types
getTile: () => any | null = () => this.tile.current; private getTile: () => any | null = () => this.tile.current;
getReplyChain = () => this.replyChain.current; private getReplyChain = () => this.replyChain.current;
getReactions = () => { private getReactions = () => {
if ( if (
!this.props.showReactions || !this.props.showReactions ||
!this.props.getRelationsForEvent !this.props.getRelationsForEvent
@ -1063,6 +1062,7 @@ export default class EventTile extends React.Component<IProps, IState> {
isQuoteExpanded: expanded, isQuoteExpanded: expanded,
}); });
}; };
render() { render() {
const msgtype = this.props.mxEvent.getContent().msgtype; const msgtype = this.props.mxEvent.getContent().msgtype;
const eventType = this.props.mxEvent.getType() as EventType; const eventType = this.props.mxEvent.getType() as EventType;
@ -1099,6 +1099,11 @@ export default class EventTile extends React.Component<IProps, IState> {
const isRedacted = isMessageEvent(this.props.mxEvent) && this.props.isRedacted; const isRedacted = isMessageEvent(this.props.mxEvent) && this.props.isRedacted;
const isEncryptionFailure = this.props.mxEvent.isDecryptionFailure(); const isEncryptionFailure = this.props.mxEvent.isDecryptionFailure();
let isContinuation = this.props.continuation;
if (this.props.tileShape && this.props.layout !== Layout.Bubble) {
isContinuation = false;
}
const isEditing = !!this.props.editState; const isEditing = !!this.props.editState;
const classes = classNames({ const classes = classNames({
mx_EventTile_bubbleContainer: isBubbleMessage, mx_EventTile_bubbleContainer: isBubbleMessage,
@ -1111,10 +1116,7 @@ export default class EventTile extends React.Component<IProps, IState> {
mx_EventTile_sending: !isEditing && isSending, mx_EventTile_sending: !isEditing && isSending,
mx_EventTile_highlight: this.props.tileShape === TileShape.Notif ? false : this.shouldHighlight(), mx_EventTile_highlight: this.props.tileShape === TileShape.Notif ? false : this.shouldHighlight(),
mx_EventTile_selected: this.props.isSelectedEvent, mx_EventTile_selected: this.props.isSelectedEvent,
mx_EventTile_continuation: ( mx_EventTile_continuation: isContinuation || eventType === EventType.CallInvite,
(this.props.tileShape ? '' : this.props.continuation) ||
eventType === EventType.CallInvite
),
mx_EventTile_last: this.props.last, mx_EventTile_last: this.props.last,
mx_EventTile_lastInSection: this.props.lastInSection, mx_EventTile_lastInSection: this.props.lastInSection,
mx_EventTile_contextual: this.props.contextual, mx_EventTile_contextual: this.props.contextual,
@ -1226,7 +1228,7 @@ export default class EventTile extends React.Component<IProps, IState> {
|| this.state.hover || this.state.hover
|| this.state.actionBarFocused); || this.state.actionBarFocused);
const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId()); const room = this.context.getRoom(this.props.mxEvent.getRoomId());
const thread = room?.findThreadForEvent?.(this.props.mxEvent); const thread = room?.findThreadForEvent?.(this.props.mxEvent);
// Thread panel shows the timestamp of the last reply in that thread // Thread panel shows the timestamp of the last reply in that thread
@ -1312,20 +1314,22 @@ export default class EventTile extends React.Component<IProps, IState> {
msgOption = readAvatars; msgOption = readAvatars;
} }
const replyChain = haveTileForEvent(this.props.mxEvent) && const replyChain = haveTileForEvent(this.props.mxEvent) && ReplyChain.hasReply(this.props.mxEvent)
ReplyChain.hasReply(this.props.mxEvent) ? ( ? <ReplyChain
<ReplyChain parentEv={this.props.mxEvent}
parentEv={this.props.mxEvent} onHeightChanged={this.props.onHeightChanged}
onHeightChanged={this.props.onHeightChanged} ref={this.replyChain}
ref={this.replyChain} forExport={this.props.forExport}
forExport={this.props.forExport} permalinkCreator={this.props.permalinkCreator}
permalinkCreator={this.props.permalinkCreator} layout={this.props.layout}
layout={this.props.layout} alwaysShowTimestamps={this.props.alwaysShowTimestamps || this.state.hover}
alwaysShowTimestamps={this.props.alwaysShowTimestamps || this.state.hover} isQuoteExpanded={isQuoteExpanded}
isQuoteExpanded={isQuoteExpanded} setQuoteExpanded={this.setQuoteExpanded}
setQuoteExpanded={this.setQuoteExpanded} getRelationsForEvent={this.props.getRelationsForEvent}
getRelationsForEvent={this.props.getRelationsForEvent} />
/>) : null; : null;
const isOwnEvent = this.props.mxEvent?.sender?.userId === this.context.getUserId();
switch (this.props.tileShape) { switch (this.props.tileShape) {
case TileShape.Notif: { case TileShape.Notif: {
@ -1372,6 +1376,8 @@ export default class EventTile extends React.Component<IProps, IState> {
"aria-atomic": true, "aria-atomic": true,
"data-scroll-tokens": scrollToken, "data-scroll-tokens": scrollToken,
"data-has-reply": !!replyChain, "data-has-reply": !!replyChain,
"data-layout": this.props.layout,
"data-self": isOwnEvent,
"onMouseEnter": () => this.setState({ hover: true }), "onMouseEnter": () => this.setState({ hover: true }),
"onMouseLeave": () => this.setState({ hover: false }), "onMouseLeave": () => this.setState({ hover: false }),
}, [ }, [
@ -1407,8 +1413,6 @@ export default class EventTile extends React.Component<IProps, IState> {
]); ]);
} }
case TileShape.ThreadPanel: { case TileShape.ThreadPanel: {
const isOwnEvent = this.props.mxEvent?.sender?.userId === MatrixClientPeg.get().getUserId();
// tab-index=-1 to allow it to be focusable but do not add tab stop for it, primarily for screen readers // tab-index=-1 to allow it to be focusable but do not add tab stop for it, primarily for screen readers
return ( return (
React.createElement(this.props.as || "li", { React.createElement(this.props.as || "li", {
@ -1491,8 +1495,6 @@ export default class EventTile extends React.Component<IProps, IState> {
} }
default: { default: {
const isOwnEvent = this.props.mxEvent?.sender?.userId === MatrixClientPeg.get().getUserId();
// tab-index=-1 to allow it to be focusable but do not add tab stop for it, primarily for screen readers // tab-index=-1 to allow it to be focusable but do not add tab stop for it, primarily for screen readers
return ( return (
React.createElement(this.props.as || "li", { React.createElement(this.props.as || "li", {
@ -1554,7 +1556,7 @@ function isMessageEvent(ev: MatrixEvent): boolean {
return (messageTypes.includes(ev.getType())); return (messageTypes.includes(ev.getType()));
} }
export function haveTileForEvent(e: MatrixEvent, showHiddenEvents?: boolean) { export function haveTileForEvent(e: MatrixEvent, showHiddenEvents?: boolean): boolean {
// Only messages have a tile (black-rectangle) if redacted // Only messages have a tile (black-rectangle) if redacted
if (e.isRedacted() && !isMessageEvent(e)) return false; if (e.isRedacted() && !isMessageEvent(e)) return false;