mirror of
https://github.com/element-hq/element-web
synced 2024-11-27 03:36:07 +03:00
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
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue