Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into t3chguy/cr/72

# Conflicts:
#	src/components/views/rooms/RoomHeader.tsx
#	test/components/views/rooms/__snapshots__/RoomHeader-test.tsx.snap
This commit is contained in:
Michael Telatynski 2023-09-19 10:36:29 +01:00
commit c839123b83
No known key found for this signature in database
GPG key ID: A2B008A5F49F5D0D
123 changed files with 6527 additions and 6069 deletions

View file

@ -282,9 +282,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
};
private showPlaceholder(): void {
// escape single quotes
const placeholder = this.props.placeholder?.replace(/'/g, "\\'");
this.editorRef.current?.style.setProperty("--placeholder", `'${placeholder}'`);
this.editorRef.current?.style.setProperty("--placeholder", `'${CSS.escape(this.props.placeholder ?? "")}'`);
this.editorRef.current?.classList.add("mx_BasicMessageComposer_inputEmpty");
}

View file

@ -18,17 +18,17 @@ limitations under the License.
import React, { createRef, forwardRef, MouseEvent, ReactNode, useRef } from "react";
import classNames from "classnames";
import {
EventType,
MsgType,
RelationType,
EventStatus,
EventType,
MatrixEvent,
MatrixEventEvent,
RoomMember,
MsgType,
NotificationCountType,
Relations,
RelationType,
Room,
RoomEvent,
Relations,
RoomMember,
Thread,
ThreadEvent,
} from "matrix-js-sdk/src/matrix";
@ -36,6 +36,7 @@ import { logger } from "matrix-js-sdk/src/logger";
import { CallErrorCode } from "matrix-js-sdk/src/webrtc/call";
import { CryptoEvent } from "matrix-js-sdk/src/crypto";
import { UserTrustLevel } from "matrix-js-sdk/src/crypto/CrossSigning";
import { EventShieldColour, EventShieldReason } from "matrix-js-sdk/src/crypto-api";
import ReplyChain from "../elements/ReplyChain";
import { _t } from "../../../languageHandler";
@ -44,7 +45,6 @@ import { Layout } from "../../../settings/enums/Layout";
import { formatTime } from "../../../DateUtils";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { DecryptionFailureBody } from "../messages/DecryptionFailureBody";
import { E2EState } from "./E2EIcon";
import RoomAvatar from "../avatars/RoomAvatar";
import MessageContextMenu from "../context_menus/MessageContextMenu";
import { aboveRightOf } from "../../structures/ContextMenu";
@ -236,8 +236,19 @@ export interface EventTileProps {
interface IState {
// Whether the action bar is focused.
actionBarFocused: boolean;
// Whether the event's sender has been verified.
verified: string | null;
/**
* E2EE shield we should show for decryption problems.
*
* Note this will be `EventShieldColour.NONE` for all unencrypted events, **including those in encrypted rooms**.
*/
shieldColour: EventShieldColour;
/**
* Reason code for the E2EE shield. `null` if `shieldColour` is `EventShieldColour.NONE`
*/
shieldReason: EventShieldReason | null;
// The Relations model from the JS SDK for reactions to `mxEvent`
reactions?: Relations | null | undefined;
@ -299,9 +310,10 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
this.state = {
// Whether the action bar is focused.
actionBarFocused: false,
// Whether the event's sender has been verified. `null` if no attempt has yet been made to verify
// (including if the event is not encrypted).
verified: null,
shieldColour: EventShieldColour.NONE,
shieldReason: null,
// The Relations model from the JS SDK for reactions to `mxEvent`
reactions: this.getReactions(),
@ -437,8 +449,9 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
}
public componentDidUpdate(prevProps: Readonly<EventTileProps>, prevState: Readonly<IState>): void {
// If the verification state changed, the height might have changed
if (prevState.verified !== this.state.verified && this.props.onHeightChanged) {
// If the shield state changed, the height might have changed.
// XXX: does the shield *actually* cause a change in height? Not sure.
if (prevState.shieldColour !== this.state.shieldColour && this.props.onHeightChanged) {
this.props.onHeightChanged();
}
// If we're not listening for receipts and expect to be, register a listener.
@ -576,65 +589,33 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
this.verifyEvent();
};
private async verifyEvent(): Promise<void> {
private verifyEvent(): void {
this.doVerifyEvent().catch((e) => {
const event = this.props.mxEvent;
logger.error("Error getting encryption info on event", e, event);
});
}
private async doVerifyEvent(): Promise<void> {
// if the event was edited, show the verification info for the edit, not
// the original
const mxEvent = this.props.mxEvent.replacingEvent() ?? this.props.mxEvent;
if (!mxEvent.isEncrypted() || mxEvent.isRedacted()) {
this.setState({ verified: null });
this.setState({ shieldColour: EventShieldColour.NONE, shieldReason: null });
return;
}
const encryptionInfo = MatrixClientPeg.safeGet().getEventEncryptionInfo(mxEvent);
const senderId = mxEvent.getSender();
if (!senderId) {
// something definitely wrong is going on here
this.setState({ verified: E2EState.Warning });
return;
}
const userTrust = MatrixClientPeg.safeGet().checkUserTrust(senderId);
if (encryptionInfo.mismatchedSender) {
// something definitely wrong is going on here
this.setState({ verified: E2EState.Warning });
return;
}
if (!userTrust.isCrossSigningVerified()) {
// If the message is unauthenticated, then display a grey
// shield, otherwise if the user isn't cross-signed then
// nothing's needed
this.setState({ verified: encryptionInfo.authenticated ? E2EState.Normal : E2EState.Unauthenticated });
return;
}
const eventSenderTrust =
senderId &&
encryptionInfo.sender &&
(await MatrixClientPeg.safeGet()
.getCrypto()
?.getDeviceVerificationStatus(senderId, encryptionInfo.sender.deviceId));
const encryptionInfo =
(await MatrixClientPeg.safeGet().getCrypto()?.getEncryptionInfoForEvent(mxEvent)) ?? null;
if (this.unmounted) return;
if (!eventSenderTrust) {
this.setState({ verified: E2EState.Unknown });
if (encryptionInfo === null) {
// likely a decryption error
this.setState({ shieldColour: EventShieldColour.NONE, shieldReason: null });
return;
}
if (!eventSenderTrust.isVerified()) {
this.setState({ verified: E2EState.Warning });
return;
}
if (!encryptionInfo.authenticated) {
this.setState({ verified: E2EState.Unauthenticated });
return;
}
this.setState({ verified: E2EState.Verified });
this.setState({ shieldColour: encryptionInfo.shieldColour, shieldReason: encryptionInfo.shieldReason });
}
private propsEqual(objA: EventTileProps, objB: EventTileProps): boolean {
@ -751,18 +732,42 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
return <E2ePadlockDecryptionFailure />;
}
// event is encrypted and not redacted, display padlock corresponding to whether or not it is verified
if (ev.isEncrypted() && !ev.isRedacted()) {
if (this.state.verified === E2EState.Normal) {
return null; // no icon if we've not even cross-signed the user
} else if (this.state.verified === E2EState.Verified) {
return null; // no icon for verified
} else if (this.state.verified === E2EState.Unauthenticated) {
return <E2ePadlockUnauthenticated />;
} else if (this.state.verified === E2EState.Unknown) {
return <E2ePadlockUnknown />;
if (this.state.shieldColour !== EventShieldColour.NONE) {
let shieldReasonMessage: string;
switch (this.state.shieldReason) {
case null:
case EventShieldReason.UNKNOWN:
shieldReasonMessage = _t("Unknown error");
break;
case EventShieldReason.UNVERIFIED_IDENTITY:
shieldReasonMessage = _t("Encrypted by an unverified user.");
break;
case EventShieldReason.UNSIGNED_DEVICE:
shieldReasonMessage = _t("Encrypted by a device not verified by its owner.");
break;
case EventShieldReason.UNKNOWN_DEVICE:
shieldReasonMessage = _t("Encrypted by an unknown or deleted device.");
break;
case EventShieldReason.AUTHENTICITY_NOT_GUARANTEED:
shieldReasonMessage = _t(
"The authenticity of this encrypted message can't be guaranteed on this device.",
);
break;
case EventShieldReason.MISMATCHED_SENDER_KEY:
shieldReasonMessage = _t("Encrypted by an unverified session");
break;
}
if (this.state.shieldColour === EventShieldColour.GREY) {
return <E2ePadlock icon={E2ePadlockIcon.Normal} title={shieldReasonMessage} />;
} else {
return <E2ePadlockUnverified />;
// red, by elimination
return <E2ePadlock icon={E2ePadlockIcon.Warning} title={shieldReasonMessage} />;
}
}
@ -781,8 +786,10 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
if (ev.isRedacted()) {
return null; // we expect this to be unencrypted
}
// if the event is not encrypted, but it's an e2e room, show the open padlock
return <E2ePadlockUnencrypted />;
if (!ev.isEncrypted()) {
// if the event is not encrypted, but it's an e2e room, show a warning
return <E2ePadlockUnencrypted />;
}
}
// no padlock needed
@ -1460,28 +1467,10 @@ const SafeEventTile = forwardRef<UnwrappedEventTile, EventTileProps>((props, ref
});
export default SafeEventTile;
function E2ePadlockUnverified(props: Omit<IE2ePadlockProps, "title" | "icon">): JSX.Element {
return <E2ePadlock title={_t("Encrypted by an unverified session")} icon={E2ePadlockIcon.Warning} {...props} />;
}
function E2ePadlockUnencrypted(props: Omit<IE2ePadlockProps, "title" | "icon">): JSX.Element {
return <E2ePadlock title={_t("Unencrypted")} icon={E2ePadlockIcon.Warning} {...props} />;
}
function E2ePadlockUnknown(props: Omit<IE2ePadlockProps, "title" | "icon">): JSX.Element {
return <E2ePadlock title={_t("Encrypted by a deleted session")} icon={E2ePadlockIcon.Normal} {...props} />;
}
function E2ePadlockUnauthenticated(props: Omit<IE2ePadlockProps, "title" | "icon">): JSX.Element {
return (
<E2ePadlock
title={_t("The authenticity of this encrypted message can't be guaranteed on this device.")}
icon={E2ePadlockIcon.Normal}
{...props}
/>
);
}
function E2ePadlockDecryptionFailure(props: Omit<IE2ePadlockProps, "title" | "icon">): JSX.Element {
return (
<E2ePadlock
@ -1493,8 +1482,13 @@ function E2ePadlockDecryptionFailure(props: Omit<IE2ePadlockProps, "title" | "ic
}
enum E2ePadlockIcon {
/** grey shield */
Normal = "normal",
/** red shield with (!) */
Warning = "warning",
/** key in grey circle */
DecryptionFailure = "decryption_failure",
}

View file

@ -30,15 +30,19 @@ const HistoryTile: React.FC = () => {
let subtitle: string | undefined;
if (historyState == "invited") {
subtitle = _t("You don't have permission to view messages from before you were invited.");
subtitle = _t("timeline|no_permission_messages_before_invite");
} else if (historyState == "joined") {
subtitle = _t("You don't have permission to view messages from before you joined.");
subtitle = _t("timeline|no_permission_messages_before_join");
} else if (encryptionState) {
subtitle = _t("Encrypted messages before this point are unavailable.");
subtitle = _t("timeline|encrypted_historical_messages_unavailable");
}
return (
<EventTileBubble className="mx_HistoryTile" title={_t("You can't see earlier messages")} subtitle={subtitle} />
<EventTileBubble
className="mx_HistoryTile"
title={_t("timeline|historical_messages_unavailable")}
subtitle={subtitle}
/>
);
};

View file

@ -27,7 +27,6 @@ import { EventType, JoinRule, type Room } from "matrix-js-sdk/src/matrix";
import { useRoomName } from "../../../hooks/useRoomName";
import { RightPanelPhases } from "../../../stores/right-panel/RightPanelStorePhases";
import RightPanelStore from "../../../stores/right-panel/RightPanelStore";
import { useTopic } from "../../../hooks/room/useTopic";
import { useAccountData } from "../../../hooks/useAccountData";
import { useMatrixClientContext } from "../../../contexts/MatrixClientContext";
@ -47,6 +46,7 @@ import FacePile from "../elements/FacePile";
import { useRoomState } from "../../../hooks/useRoomState";
import RoomAvatar from "../avatars/RoomAvatar";
import { formatCount } from "../../../utils/FormattingUtils";
import RightPanelStore from "../../../stores/right-panel/RightPanelStore";
/**
* A helper to transform a notification color to the what the Compound Icon Button
@ -62,16 +62,6 @@ function notificationColorToIndicator(color: NotificationColor): React.Component
}
}
/**
* A helper to show or hide the right panel
*/
function showOrHidePanel(phase: RightPanelPhases): void {
const rightPanel = RightPanelStore.instance;
rightPanel.isOpen && rightPanel.currentCard.phase === phase
? rightPanel.togglePanel(null)
: rightPanel.setCard({ phase });
}
export default function RoomHeader({ room }: { room: Room }): JSX.Element {
const client = useMatrixClientContext();
@ -108,6 +98,8 @@ export default function RoomHeader({ room }: { room: Room }): JSX.Element {
}, [room, directRoomsList]);
const e2eStatus = useEncryptionStatus(client, room);
const notificationsEnabled = useFeatureEnabled("feature_notifications");
return (
<Flex
as="header"
@ -115,7 +107,7 @@ export default function RoomHeader({ room }: { room: Room }): JSX.Element {
gap="var(--cpd-space-3x)"
className="mx_RoomHeader light-panel"
onClick={() => {
showOrHidePanel(RightPanelPhases.RoomSummary);
RightPanelStore.instance.showOrHidePanel(RightPanelPhases.RoomSummary);
}}
>
<RoomAvatar room={room} size="40px" />
@ -197,25 +189,27 @@ export default function RoomHeader({ room }: { room: Room }): JSX.Element {
indicator={notificationColorToIndicator(threadNotifications)}
onClick={(evt) => {
evt.stopPropagation();
showOrHidePanel(RightPanelPhases.ThreadPanel);
RightPanelStore.instance.showOrHidePanel(RightPanelPhases.ThreadPanel);
}}
aria-label={_t("common|threads")}
>
<ThreadsIcon />
</IconButton>
</Tooltip>
<Tooltip label={_t("Notifications")}>
<IconButton
indicator={notificationColorToIndicator(globalNotificationState.color)}
onClick={(evt) => {
evt.stopPropagation();
showOrHidePanel(RightPanelPhases.NotificationPanel);
}}
aria-label={_t("Notifications")}
>
<NotificationsIcon />
</IconButton>
</Tooltip>
{notificationsEnabled && (
<Tooltip label={_t("Notifications")}>
<IconButton
indicator={notificationColorToIndicator(globalNotificationState.color)}
onClick={(evt) => {
evt.stopPropagation();
RightPanelStore.instance.showOrHidePanel(RightPanelPhases.NotificationPanel);
}}
aria-label={_t("Notifications")}
>
<NotificationsIcon />
</IconButton>
</Tooltip>
)}
</Flex>
{!isDirectMessage && (
<BodyText
@ -224,7 +218,7 @@ export default function RoomHeader({ room }: { room: Room }): JSX.Element {
weight="medium"
aria-label={_t("%(count)s members", { count: memberCount })}
onClick={(e: React.MouseEvent) => {
showOrHidePanel(RightPanelPhases.RoomMemberList);
RightPanelStore.instance.showOrHidePanel(RightPanelPhases.RoomMemberList);
e.stopPropagation();
}}
>

View file

@ -90,10 +90,9 @@ export function attachMentions(
replyToEvent: MatrixEvent | undefined,
editedContent: IContent | null = null,
): void {
// If this feature is disabled, do nothing.
if (!SettingsStore.getValue("feature_intentional_mentions")) {
return;
}
// We always attach the mentions even if the home server doesn't yet support
// intentional mentions. This is safe because m.mentions is an additive change
// that should simply be ignored by incapable home servers.
// The mentions property *always* gets included to disable legacy push rules.
const mentions: IMentions = (content["m.mentions"] = {});