Allow bubble layout in Thread View (#7478)
This commit is contained in:
parent
038a6bc204
commit
f5465b37a9
4 changed files with 144 additions and 99 deletions
|
@ -23,7 +23,7 @@ import { Relations } from "matrix-js-sdk/src/models/relations";
|
|||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||
import { Thread, ThreadEvent } from 'matrix-js-sdk/src/models/thread';
|
||||
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 ReplyChain from "../elements/ReplyChain";
|
||||
|
@ -129,16 +129,15 @@ for (const evType of ALL_RULE_TYPES) {
|
|||
stateEventTileTypes[evType] = 'messages.TextualEvent';
|
||||
}
|
||||
|
||||
export function getHandlerTile(ev) {
|
||||
export function getHandlerTile(ev: MatrixEvent): string {
|
||||
const type = ev.getType();
|
||||
|
||||
// don't show verification requests we're not involved in,
|
||||
// not even when showing hidden events
|
||||
if (type === "m.room.message") {
|
||||
if (type === EventType.RoomMessage) {
|
||||
const content = ev.getContent();
|
||||
if (content && content.msgtype === MsgType.KeyVerificationRequest) {
|
||||
const client = MatrixClientPeg.get();
|
||||
const me = client && client.getUserId();
|
||||
const me = MatrixClientPeg.get()?.getUserId();
|
||||
if (ev.getSender() !== me && content.to !== me) {
|
||||
return undefined;
|
||||
} 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
|
||||
// tile once the verification concludes, so filter out the one from the other party.
|
||||
if (type === "m.key.verification.done") {
|
||||
const client = MatrixClientPeg.get();
|
||||
const me = client && client.getUserId();
|
||||
if (type === EventType.KeyVerificationDone) {
|
||||
const me = MatrixClientPeg.get()?.getUserId();
|
||||
if (ev.getSender() !== me) {
|
||||
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
|
||||
// XXX: This is extremely a hack. Possibly these components should have an interface for
|
||||
// declining to render?
|
||||
if (type === "m.key.verification.cancel" || type === "m.key.verification.done") {
|
||||
if (!MKeyVerificationConclusion.shouldRender(ev, ev.request)) {
|
||||
if (type === EventType.KeyVerificationCancel || type === EventType.KeyVerificationDone) {
|
||||
if (!MKeyVerificationConclusion.shouldRender(ev, ev.verificationRequest)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
@ -375,8 +373,9 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
};
|
||||
|
||||
static contextType = MatrixClientContext;
|
||||
public context!: React.ContextType<typeof MatrixClientContext>;
|
||||
|
||||
constructor(props, context) {
|
||||
constructor(props: IProps, context: React.ContextType<typeof MatrixClientContext>) {
|
||||
super(props, context);
|
||||
|
||||
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
|
||||
// special read receipts.
|
||||
const myUserId = MatrixClientPeg.get().getUserId();
|
||||
const myUserId = this.context.getUserId();
|
||||
if (this.props.mxEvent.getSender() !== myUserId) return false;
|
||||
|
||||
// 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.
|
||||
const receipts = this.props.readReceipts || [];
|
||||
const myUserId = MatrixClientPeg.get().getUserId();
|
||||
const myUserId = this.context.getUserId();
|
||||
if (receipts.some(r => r.userId !== myUserId)) return false;
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
private setupNotificationListener = (thread): void => {
|
||||
private setupNotificationListener = (thread: Thread): void => {
|
||||
const room = this.context.getRoom(this.props.mxEvent.getRoomId());
|
||||
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 (this.threadState) {
|
||||
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
|
||||
// eslint-disable-next-line
|
||||
UNSAFE_componentWillReceiveProps(nextProps) {
|
||||
UNSAFE_componentWillReceiveProps(nextProps: IProps) {
|
||||
// re-check the sender verification as outgoing events progress through
|
||||
// the send process.
|
||||
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)) {
|
||||
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 (!this.isListeningForReceipts && (this.shouldShowSentReceipt || this.shouldShowSendingReceipt)) {
|
||||
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
|
||||
* 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());
|
||||
|
||||
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
|
||||
const tileRoom = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId());
|
||||
const tileRoom = this.context.getRoom(this.props.mxEvent.getRoomId());
|
||||
if (room !== tileRoom) return;
|
||||
|
||||
if (!this.shouldShowSentReceipt && !this.shouldShowSendingReceipt && !this.isListeningForReceipts) {
|
||||
|
@ -722,19 +721,19 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
this.forceUpdate();
|
||||
};
|
||||
|
||||
private onDeviceVerificationChanged = (userId, device) => {
|
||||
private onDeviceVerificationChanged = (userId: string, device: string): void => {
|
||||
if (userId === this.props.mxEvent.getSender()) {
|
||||
this.verifyEvent(this.props.mxEvent);
|
||||
}
|
||||
};
|
||||
|
||||
private onUserVerificationChanged = (userId, _trustStatus) => {
|
||||
private onUserVerificationChanged = (userId: string, _trustStatus: string): void => {
|
||||
if (userId === this.props.mxEvent.getSender()) {
|
||||
this.verifyEvent(this.props.mxEvent);
|
||||
}
|
||||
};
|
||||
|
||||
private async verifyEvent(mxEvent) {
|
||||
private async verifyEvent(mxEvent: MatrixEvent): Promise<void> {
|
||||
if (!mxEvent.isEncrypted()) {
|
||||
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
|
||||
}
|
||||
|
||||
private propsEqual(objA, objB) {
|
||||
private propsEqual(objA: IProps, objB: IProps): boolean {
|
||||
const keysA = Object.keys(objA);
|
||||
const keysB = Object.keys(objB);
|
||||
|
||||
|
@ -836,7 +835,7 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
return true;
|
||||
}
|
||||
|
||||
shouldHighlight() {
|
||||
private shouldHighlight(): boolean {
|
||||
if (this.props.forExport) return false;
|
||||
const actions = this.context.getPushActionsForEvent(this.props.mxEvent.replacingEvent() || this.props.mxEvent);
|
||||
if (!actions || !actions.tweaks) { return false; }
|
||||
|
@ -849,13 +848,13 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
return actions.tweaks.highlight;
|
||||
}
|
||||
|
||||
toggleAllReadAvatars = () => {
|
||||
private toggleAllReadAvatars = () => {
|
||||
this.setState({
|
||||
allReadAvatars: !this.state.allReadAvatars,
|
||||
});
|
||||
};
|
||||
|
||||
getReadAvatars() {
|
||||
private getReadAvatars() {
|
||||
if (this.shouldShowSentReceipt || this.shouldShowSendingReceipt) {
|
||||
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) {
|
||||
const remainder = receipts.length - MAX_READ_AVATARS;
|
||||
if (remainder > 0) {
|
||||
|
@ -950,7 +950,7 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
);
|
||||
}
|
||||
|
||||
onSenderProfileClick = () => {
|
||||
private onSenderProfileClick = () => {
|
||||
if (!this.props.timelineRenderingType) return;
|
||||
dis.dispatch<ComposerInsertPayload>({
|
||||
action: Action.ComposerInsert,
|
||||
|
@ -959,7 +959,7 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
});
|
||||
};
|
||||
|
||||
onRequestKeysClick = () => {
|
||||
private onRequestKeysClick = () => {
|
||||
this.setState({
|
||||
// Indicate in the UI that the keys have been requested (this is expected to
|
||||
// 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);
|
||||
};
|
||||
|
||||
onPermalinkClicked = e => {
|
||||
private onPermalinkClicked = e => {
|
||||
// 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.
|
||||
e.preventDefault();
|
||||
|
@ -1027,17 +1027,16 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
return null;
|
||||
}
|
||||
|
||||
onActionBarFocusChange = focused => {
|
||||
this.setState({
|
||||
actionBarFocused: focused,
|
||||
});
|
||||
private onActionBarFocusChange = (actionBarFocused: boolean) => {
|
||||
this.setState({ actionBarFocused });
|
||||
};
|
||||
|
||||
// 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 (
|
||||
!this.props.showReactions ||
|
||||
!this.props.getRelationsForEvent
|
||||
|
@ -1063,6 +1062,7 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
isQuoteExpanded: expanded,
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const msgtype = this.props.mxEvent.getContent().msgtype;
|
||||
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 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 classes = classNames({
|
||||
mx_EventTile_bubbleContainer: isBubbleMessage,
|
||||
|
@ -1111,10 +1116,7 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
mx_EventTile_sending: !isEditing && isSending,
|
||||
mx_EventTile_highlight: this.props.tileShape === TileShape.Notif ? false : this.shouldHighlight(),
|
||||
mx_EventTile_selected: this.props.isSelectedEvent,
|
||||
mx_EventTile_continuation: (
|
||||
(this.props.tileShape ? '' : this.props.continuation) ||
|
||||
eventType === EventType.CallInvite
|
||||
),
|
||||
mx_EventTile_continuation: isContinuation || eventType === EventType.CallInvite,
|
||||
mx_EventTile_last: this.props.last,
|
||||
mx_EventTile_lastInSection: this.props.lastInSection,
|
||||
mx_EventTile_contextual: this.props.contextual,
|
||||
|
@ -1226,7 +1228,7 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
|| this.state.hover
|
||||
|| 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);
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
const replyChain = haveTileForEvent(this.props.mxEvent) &&
|
||||
ReplyChain.hasReply(this.props.mxEvent) ? (
|
||||
<ReplyChain
|
||||
parentEv={this.props.mxEvent}
|
||||
onHeightChanged={this.props.onHeightChanged}
|
||||
ref={this.replyChain}
|
||||
forExport={this.props.forExport}
|
||||
permalinkCreator={this.props.permalinkCreator}
|
||||
layout={this.props.layout}
|
||||
alwaysShowTimestamps={this.props.alwaysShowTimestamps || this.state.hover}
|
||||
isQuoteExpanded={isQuoteExpanded}
|
||||
setQuoteExpanded={this.setQuoteExpanded}
|
||||
getRelationsForEvent={this.props.getRelationsForEvent}
|
||||
/>) : null;
|
||||
const replyChain = haveTileForEvent(this.props.mxEvent) && ReplyChain.hasReply(this.props.mxEvent)
|
||||
? <ReplyChain
|
||||
parentEv={this.props.mxEvent}
|
||||
onHeightChanged={this.props.onHeightChanged}
|
||||
ref={this.replyChain}
|
||||
forExport={this.props.forExport}
|
||||
permalinkCreator={this.props.permalinkCreator}
|
||||
layout={this.props.layout}
|
||||
alwaysShowTimestamps={this.props.alwaysShowTimestamps || this.state.hover}
|
||||
isQuoteExpanded={isQuoteExpanded}
|
||||
setQuoteExpanded={this.setQuoteExpanded}
|
||||
getRelationsForEvent={this.props.getRelationsForEvent}
|
||||
/>
|
||||
: null;
|
||||
|
||||
const isOwnEvent = this.props.mxEvent?.sender?.userId === this.context.getUserId();
|
||||
|
||||
switch (this.props.tileShape) {
|
||||
case TileShape.Notif: {
|
||||
|
@ -1372,6 +1376,8 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
"aria-atomic": true,
|
||||
"data-scroll-tokens": scrollToken,
|
||||
"data-has-reply": !!replyChain,
|
||||
"data-layout": this.props.layout,
|
||||
"data-self": isOwnEvent,
|
||||
"onMouseEnter": () => this.setState({ hover: true }),
|
||||
"onMouseLeave": () => this.setState({ hover: false }),
|
||||
}, [
|
||||
|
@ -1407,8 +1413,6 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
]);
|
||||
}
|
||||
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
|
||||
return (
|
||||
React.createElement(this.props.as || "li", {
|
||||
|
@ -1491,8 +1495,6 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
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
|
||||
return (
|
||||
React.createElement(this.props.as || "li", {
|
||||
|
@ -1554,7 +1556,7 @@ function isMessageEvent(ev: MatrixEvent): boolean {
|
|||
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
|
||||
if (e.isRedacted() && !isMessageEvent(e)) return false;
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue