mirror of
https://github.com/element-hq/element-web.git
synced 2025-12-09 01:30:57 +00:00
Compare commits
1 Commits
hs/invite-
...
hs/user-pr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0f16ac7589 |
@@ -323,7 +323,6 @@
|
|||||||
@import "./views/rooms/_RoomKnocksBar.pcss";
|
@import "./views/rooms/_RoomKnocksBar.pcss";
|
||||||
@import "./views/rooms/_RoomPreviewBar.pcss";
|
@import "./views/rooms/_RoomPreviewBar.pcss";
|
||||||
@import "./views/rooms/_RoomPreviewCard.pcss";
|
@import "./views/rooms/_RoomPreviewCard.pcss";
|
||||||
@import "./views/rooms/_RoomPreviewContext.pcss";
|
|
||||||
@import "./views/rooms/_RoomSearchAuxPanel.pcss";
|
@import "./views/rooms/_RoomSearchAuxPanel.pcss";
|
||||||
@import "./views/rooms/_RoomSublist.pcss";
|
@import "./views/rooms/_RoomSublist.pcss";
|
||||||
@import "./views/rooms/_RoomTile.pcss";
|
@import "./views/rooms/_RoomTile.pcss";
|
||||||
|
|||||||
@@ -1,47 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2025 New Vector Ltd.
|
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
|
||||||
Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
.mx_RoomPreviewContext {
|
|
||||||
> li {
|
|
||||||
list-style: none;
|
|
||||||
margin-bottom: var(--cpd-space-2x);
|
|
||||||
}
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_RoomPreviewContext_detailsItem {
|
|
||||||
display: flex;
|
|
||||||
gap: var(--cpd-space-1x);
|
|
||||||
|
|
||||||
svg {
|
|
||||||
width: 1.5em;
|
|
||||||
height: 1.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.safe {
|
|
||||||
color: var(--cpd-color-text-success-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.unknown {
|
|
||||||
color: var(--cpd-color-text-info-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.unsafe {
|
|
||||||
color: var(--cpd-color-text-critical-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: var(--cpd-font-size-body-md);
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
color: var(--cpd-color-text-secondary);
|
|
||||||
margin-top: 2px;
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -12,7 +12,9 @@ import { type MatrixEvent, MsgType } from "matrix-js-sdk/src/matrix";
|
|||||||
|
|
||||||
import DisambiguatedProfile from "./DisambiguatedProfile";
|
import DisambiguatedProfile from "./DisambiguatedProfile";
|
||||||
import { useRoomMemberProfile } from "../../../hooks/room/useRoomMemberProfile";
|
import { useRoomMemberProfile } from "../../../hooks/room/useRoomMemberProfile";
|
||||||
|
import ModuleApi from "../../../modules/Api";
|
||||||
|
import { CustomComponentsApi } from "../../../modules/customComponentApi";
|
||||||
|
import { MessageProfileComponentProps } from "@element-hq/element-web-module-api";
|
||||||
interface IProps {
|
interface IProps {
|
||||||
mxEvent: MatrixEvent;
|
mxEvent: MatrixEvent;
|
||||||
onClick?(): void;
|
onClick?(): void;
|
||||||
@@ -24,17 +26,25 @@ export default function SenderProfile({ mxEvent, onClick, withTooltip }: IProps)
|
|||||||
userId: mxEvent.getSender(),
|
userId: mxEvent.getSender(),
|
||||||
member: mxEvent.sender,
|
member: mxEvent.sender,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (mxEvent.getContent().msgtype === MsgType.Emote) {
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
|
const moduleRenderer = ModuleApi.customComponents.messageProfileRenderer;
|
||||||
|
const renderFn = (moduleProps: MessageProfileComponentProps) => <DisambiguatedProfile
|
||||||
|
fallbackName={moduleProps.mxEvent.sender ?? ""}
|
||||||
|
onClick={moduleProps.onClick}
|
||||||
|
member={moduleProps.member}
|
||||||
|
colored={true}
|
||||||
|
emphasizeDisplayName={true}
|
||||||
|
withTooltip={withTooltip}
|
||||||
|
/>;
|
||||||
|
|
||||||
return mxEvent.getContent().msgtype !== MsgType.Emote ? (
|
const modProps = {
|
||||||
<DisambiguatedProfile
|
onClick,
|
||||||
fallbackName={mxEvent.getSender() ?? ""}
|
mxEvent: CustomComponentsApi.getModuleMatrixEvent(mxEvent)!,
|
||||||
onClick={onClick}
|
member: member || undefined,
|
||||||
member={member}
|
};
|
||||||
colored={true}
|
|
||||||
emphasizeDisplayName={true}
|
return moduleRenderer ? moduleRenderer(modProps, renderFn) : renderFn(modProps);
|
||||||
withTooltip={withTooltip}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<></>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { Flex } from "../../../../shared-components/utils/Flex";
|
|||||||
import PresenceLabel from "../../rooms/PresenceLabel";
|
import PresenceLabel from "../../rooms/PresenceLabel";
|
||||||
import CopyableText from "../../elements/CopyableText";
|
import CopyableText from "../../elements/CopyableText";
|
||||||
import { UserInfoHeaderVerificationView } from "./UserInfoHeaderVerificationView";
|
import { UserInfoHeaderVerificationView } from "./UserInfoHeaderVerificationView";
|
||||||
|
import ModuleApi from "../../../../modules/Api";
|
||||||
|
|
||||||
export interface UserInfoHeaderViewProps {
|
export interface UserInfoHeaderViewProps {
|
||||||
member: Member;
|
member: Member;
|
||||||
@@ -48,6 +49,27 @@ export const UserInfoHeaderView: React.FC<UserInfoHeaderViewProps> = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const moduleRenderer = ModuleApi.customComponents.userInfoRenderer;
|
||||||
|
let usernameSection;
|
||||||
|
if (moduleRenderer && vm.userIdentifier) {
|
||||||
|
usernameSection = moduleRenderer(
|
||||||
|
{
|
||||||
|
userId: vm.userIdentifier,
|
||||||
|
},
|
||||||
|
(props) => <Text size="sm" weight="semibold" className="mx_UserInfo_profile_mxid">
|
||||||
|
<CopyableText getTextToCopy={() => props.userId} border={false}>
|
||||||
|
{props.userId}
|
||||||
|
</CopyableText>
|
||||||
|
</Text>,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
usernameSection = <Text size="sm" weight="semibold" className="mx_UserInfo_profile_mxid">
|
||||||
|
<CopyableText getTextToCopy={() => vm.userIdentifier} border={false}>
|
||||||
|
{vm.userIdentifier}
|
||||||
|
</CopyableText>
|
||||||
|
</Text>;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<div className="mx_UserInfo_avatar">
|
<div className="mx_UserInfo_avatar">
|
||||||
@@ -83,11 +105,7 @@ export const UserInfoHeaderView: React.FC<UserInfoHeaderViewProps> = ({
|
|||||||
</Flex>
|
</Flex>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
<Text size="sm" weight="semibold" className="mx_UserInfo_profile_mxid">
|
{usernameSection}
|
||||||
<CopyableText getTextToCopy={() => vm.userIdentifier} border={false}>
|
|
||||||
{vm.userIdentifier}
|
|
||||||
</CopyableText>
|
|
||||||
</Text>
|
|
||||||
</Flex>
|
</Flex>
|
||||||
{!hideVerificationSection && <UserInfoHeaderVerificationView member={member} devices={devices} />}
|
{!hideVerificationSection && <UserInfoHeaderVerificationView member={member} devices={devices} />}
|
||||||
</Container>
|
</Container>
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ import { ModuleRunner } from "../../../modules/ModuleRunner";
|
|||||||
import { Icon as AskToJoinIcon } from "../../../../res/img/element-icons/ask-to-join.svg";
|
import { Icon as AskToJoinIcon } from "../../../../res/img/element-icons/ask-to-join.svg";
|
||||||
import Field from "../elements/Field";
|
import Field from "../elements/Field";
|
||||||
import ModuleApi from "../../../modules/Api.ts";
|
import ModuleApi from "../../../modules/Api.ts";
|
||||||
import { RoomPreviewContext } from "./RoomPreviewContext.tsx";
|
|
||||||
|
|
||||||
const MemberEventHtmlReasonField = "io.element.html_reason";
|
const MemberEventHtmlReasonField = "io.element.html_reason";
|
||||||
|
|
||||||
@@ -318,7 +317,6 @@ class RoomPreviewBar extends React.Component<IProps, IState> {
|
|||||||
let title: string | undefined;
|
let title: string | undefined;
|
||||||
let subTitle: string | ReactNode[] | undefined;
|
let subTitle: string | ReactNode[] | undefined;
|
||||||
let reasonElement: JSX.Element | undefined;
|
let reasonElement: JSX.Element | undefined;
|
||||||
let inviteContext: JSX.Element | undefined;
|
|
||||||
let primaryActionHandler: (() => void) | undefined;
|
let primaryActionHandler: (() => void) | undefined;
|
||||||
let primaryActionLabel: string | undefined;
|
let primaryActionLabel: string | undefined;
|
||||||
let secondaryActionHandler: (() => void) | undefined;
|
let secondaryActionHandler: (() => void) | undefined;
|
||||||
@@ -559,7 +557,6 @@ class RoomPreviewBar extends React.Component<IProps, IState> {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
inviteContext = <RoomPreviewContext inviterMember={inviteMember} />;
|
|
||||||
|
|
||||||
primaryActionHandler = this.props.onJoinClick;
|
primaryActionHandler = this.props.onJoinClick;
|
||||||
secondaryActionLabel = _t("action|decline");
|
secondaryActionLabel = _t("action|decline");
|
||||||
@@ -739,7 +736,6 @@ class RoomPreviewBar extends React.Component<IProps, IState> {
|
|||||||
{subTitleElements}
|
{subTitleElements}
|
||||||
</div>
|
</div>
|
||||||
{reasonElement}
|
{reasonElement}
|
||||||
{inviteContext}
|
|
||||||
<div
|
<div
|
||||||
className={classNames("mx_RoomPreviewBar_actions", {
|
className={classNames("mx_RoomPreviewBar_actions", {
|
||||||
mx_RoomPreviewBar_fullWidth: messageCase === MessageCase.PromptAskToJoin,
|
mx_RoomPreviewBar_fullWidth: messageCase === MessageCase.PromptAskToJoin,
|
||||||
|
|||||||
@@ -1,329 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2025 New Vector Ltd.
|
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
|
||||||
Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { JoinRule, type RoomMember, type Room, KnownMembership, MatrixError } from "matrix-js-sdk/src/matrix";
|
|
||||||
import React, { type JSX, useEffect, useMemo, useState, type FC } from "react";
|
|
||||||
import { Button, InlineSpinner } from "@vector-im/compound-web";
|
|
||||||
import { CheckCircleIcon, InfoIcon, WarningIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
|
|
||||||
import classNames from "classnames";
|
|
||||||
|
|
||||||
import { formatDuration } from "../../../DateUtils";
|
|
||||||
import { useMatrixClientContext } from "../../../contexts/MatrixClientContext";
|
|
||||||
|
|
||||||
const PRIVATE_JOIN_RULES: JoinRule[] = [JoinRule.Invite, JoinRule.Knock, JoinRule.Restricted];
|
|
||||||
const LONG_TERM_USER_MS = 28 * 24 * 60 * 60 * 1000; // ~a month ago.
|
|
||||||
|
|
||||||
enum InviteScore {
|
|
||||||
Unknown = "unknown",
|
|
||||||
Safe = "safe",
|
|
||||||
Unsafe = "unsafe",
|
|
||||||
}
|
|
||||||
|
|
||||||
function SafetyDetailItem({
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
score,
|
|
||||||
}: {
|
|
||||||
title: string;
|
|
||||||
description?: string;
|
|
||||||
score?: InviteScore;
|
|
||||||
}): JSX.Element {
|
|
||||||
score = score ?? InviteScore.Unknown;
|
|
||||||
return (
|
|
||||||
<li className={classNames("mx_RoomPreviewContext_detailsItem", score)}>
|
|
||||||
{score === InviteScore.Unknown && <InfoIcon />}
|
|
||||||
{score === InviteScore.Safe && <CheckCircleIcon />}
|
|
||||||
{score === InviteScore.Unsafe && <WarningIcon />}
|
|
||||||
<div>
|
|
||||||
{title && <h1>{title}</h1>}
|
|
||||||
{description && <p>{description}</p>}
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function useGetUserSafety(inviterMember: RoomMember | null): {
|
|
||||||
score: InviteScore | null;
|
|
||||||
details: {
|
|
||||||
roomCount?: number;
|
|
||||||
joinedTo?: { title: string; description: string; score: InviteScore };
|
|
||||||
userFirstSeen?: { title: string; description: string; score: InviteScore };
|
|
||||||
userBanned?: string;
|
|
||||||
userKicked?: string;
|
|
||||||
isLocalTrustedServer?: boolean;
|
|
||||||
};
|
|
||||||
} {
|
|
||||||
const client = useMatrixClientContext();
|
|
||||||
const [joinedTo, setJoinedTo] = useState<{ title: string; description: string; score: InviteScore }>();
|
|
||||||
const [roomCount, setRoomCount] = useState<number>();
|
|
||||||
const [isLocalTrustedServer, setIsLocalTrustedServer] = useState<boolean>();
|
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!inviterMember?.userId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const inviterDomain = inviterMember.userId.replace(/^.*?:/, "");
|
|
||||||
if (inviterDomain !== client.getDomain()) {
|
|
||||||
setIsLocalTrustedServer(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
(async () => {
|
|
||||||
// Try auth metadata first for OIDC
|
|
||||||
try {
|
|
||||||
const metadata = await client.getAuthMetadata();
|
|
||||||
const openReg = metadata.prompt_values_supported?.includes("create")
|
|
||||||
setIsLocalTrustedServer(!openReg);
|
|
||||||
} catch {
|
|
||||||
// OIDC not configured, fall through.
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
await client.registerRequest({});
|
|
||||||
setIsLocalTrustedServer(false);
|
|
||||||
} catch (ex) {
|
|
||||||
if (ex instanceof MatrixError && ex.errcode === "M_FORBIDDEN") {
|
|
||||||
// We only accept M_FORBIDDEN for checking if the server is closed, for safety.
|
|
||||||
setIsLocalTrustedServer(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setIsLocalTrustedServer(false);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
setIsLocalTrustedServer(false);
|
|
||||||
};
|
|
||||||
}, [client, inviterMember]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!inviterMember?.userId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
(async () => {
|
|
||||||
let rooms: string[];
|
|
||||||
try {
|
|
||||||
rooms = await client._unstable_getSharedRooms(inviterMember.userId);
|
|
||||||
} catch (ex) {
|
|
||||||
console.warn("getSharedRooms not supported, using slow path", ex);
|
|
||||||
// Could not fetch rooms. We should fallback to the slow path.
|
|
||||||
rooms = client
|
|
||||||
.getRooms()
|
|
||||||
.filter((r) => r.getJoinedMembers().some((m) => m.userId === inviterMember.userId))
|
|
||||||
.map((r) => r.roomId);
|
|
||||||
}
|
|
||||||
const joinedToPrivateSpaces = new Map<string, number>();
|
|
||||||
const joinedToPrivateRooms = new Map<string, number>();
|
|
||||||
const joinedToPublicSpaces = new Map<string, number>();
|
|
||||||
const joinedToPublicRooms = new Map<string, number>();
|
|
||||||
for (const roomId of rooms) {
|
|
||||||
const room = client.getRoom(roomId);
|
|
||||||
if (!room) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (room.isSpaceRoom()) {
|
|
||||||
if (PRIVATE_JOIN_RULES.includes(room.getJoinRule())) {
|
|
||||||
joinedToPrivateSpaces.set(room.name, room.getMembers().length);
|
|
||||||
} else {
|
|
||||||
joinedToPublicSpaces.set(room.name, room.getMembers().length);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (PRIVATE_JOIN_RULES.includes(room.getJoinRule())) {
|
|
||||||
joinedToPrivateRooms.set(room.name, room.getMembers().length);
|
|
||||||
} else {
|
|
||||||
joinedToPublicRooms.set(room.name, room.getMembers().length);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const [roomSet, type] of [
|
|
||||||
[joinedToPrivateSpaces, "private spaces"],
|
|
||||||
[joinedToPrivateRooms, "private rooms"],
|
|
||||||
[joinedToPublicSpaces, "spaces"],
|
|
||||||
[joinedToPublicRooms, "public rooms"],
|
|
||||||
] as [Map<string, number>, string][]) {
|
|
||||||
if (roomSet.size === 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const roomNames = [...roomSet]
|
|
||||||
.sort(([, memberCountA], [, memberCountB]) => memberCountB - memberCountA)
|
|
||||||
.slice(0, 3)
|
|
||||||
.map(([name]) => name)
|
|
||||||
.join(", ");
|
|
||||||
if (roomNames) {
|
|
||||||
setJoinedTo({
|
|
||||||
description: `You share ${roomSet.size} ${type}, including ${roomNames}`,
|
|
||||||
title: `You share ${type}`,
|
|
||||||
score: type === "private spaces" ? InviteScore.Safe : InviteScore.Unknown,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
setJoinedTo({
|
|
||||||
description: `You share ${roomSet.size} ${type}`,
|
|
||||||
title: `You share ${type}`,
|
|
||||||
score: type === "private spaces" ? InviteScore.Safe : InviteScore.Unknown,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
setRoomCount(rooms.filter((r) => r !== inviterMember.roomId).length);
|
|
||||||
})();
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
setRoomCount(undefined);
|
|
||||||
};
|
|
||||||
}, [client, inviterMember]);
|
|
||||||
|
|
||||||
const userBanned = useMemo(() => {
|
|
||||||
if (!inviterMember?.userId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const bannedRooms = client
|
|
||||||
.getRooms()
|
|
||||||
.map<[Room, RoomMember | null]>((r) => [r, r.getMember(inviterMember?.userId)])
|
|
||||||
.filter(([room, member]) => member?.membership === KnownMembership.Ban);
|
|
||||||
if (bannedRooms.length) {
|
|
||||||
const exampleNames = bannedRooms
|
|
||||||
.filter(([room]) => room.normalizedName && room.normalizedName !== room.roomId)
|
|
||||||
.slice(0, 3)
|
|
||||||
.map(([room]) => room.normalizedName)
|
|
||||||
.join(", ");
|
|
||||||
if (exampleNames) {
|
|
||||||
return `User has been banned from ${bannedRooms.length} rooms, including ${exampleNames}`;
|
|
||||||
}
|
|
||||||
return `User has been banned from ${bannedRooms.length} rooms`;
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}, [client, inviterMember]);
|
|
||||||
|
|
||||||
const userKicked = useMemo(() => {
|
|
||||||
if (!inviterMember?.userId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const kickedRooms = client
|
|
||||||
.getRooms()
|
|
||||||
.map<[Room, RoomMember | null]>((r) => [r, r.getMember(inviterMember?.userId)])
|
|
||||||
.filter(([room, member]) => member?.isKicked());
|
|
||||||
if (kickedRooms.length) {
|
|
||||||
const exampleNames = kickedRooms
|
|
||||||
.filter(([room]) => room.normalizedName && room.normalizedName !== room.roomId)
|
|
||||||
.slice(0, 3)
|
|
||||||
.map(([room]) => room.normalizedName)
|
|
||||||
.join(", ");
|
|
||||||
if (exampleNames) {
|
|
||||||
return `User has been kicked from ${kickedRooms.length} rooms, including ${exampleNames}`;
|
|
||||||
}
|
|
||||||
return `User has been kicked from ${kickedRooms.length} rooms`;
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}, [client, inviterMember]);
|
|
||||||
|
|
||||||
const userFirstSeen = useMemo<{ title: string; score: InviteScore; description: string } | undefined>(() => {
|
|
||||||
if (!inviterMember?.userId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const earliestMembershipTs = client
|
|
||||||
.getRooms()
|
|
||||||
.map((r) => r.getMember(inviterMember?.userId))
|
|
||||||
.filter((member) => member?.membership === KnownMembership.Join)
|
|
||||||
.map((member) => member?.events.member?.getTs())
|
|
||||||
.filter((ts) => ts !== undefined)
|
|
||||||
.sort((tsA, tsB) => tsA - tsB)[0];
|
|
||||||
|
|
||||||
if (earliestMembershipTs) {
|
|
||||||
const userDuration = Date.now() - earliestMembershipTs;
|
|
||||||
if (userDuration > LONG_TERM_USER_MS) {
|
|
||||||
const description = `You first saw activity from this user ${formatDuration(userDuration)} ago.`;
|
|
||||||
return { title: `This user has been active for a while.`, description, score: InviteScore.Safe };
|
|
||||||
} else {
|
|
||||||
const description = `The earliest activity you have seen from this user was ${formatDuration(userDuration)} ago.`;
|
|
||||||
return {
|
|
||||||
title: `This user may have recently created their account.`,
|
|
||||||
description,
|
|
||||||
score: InviteScore.Unknown,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}, [client, inviterMember]);
|
|
||||||
|
|
||||||
const score = useMemo<InviteScore | null>(() => {
|
|
||||||
if (roomCount === undefined) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (roomCount === 0 || userBanned || userKicked) {
|
|
||||||
return InviteScore.Unsafe;
|
|
||||||
}
|
|
||||||
if (userFirstSeen?.score === InviteScore.Unknown || joinedTo?.score === InviteScore.Unknown) {
|
|
||||||
return InviteScore.Unknown;
|
|
||||||
}
|
|
||||||
return InviteScore.Safe;
|
|
||||||
}, [roomCount, userBanned, userKicked, joinedTo, userFirstSeen]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
score,
|
|
||||||
details: {
|
|
||||||
roomCount,
|
|
||||||
joinedTo,
|
|
||||||
userBanned,
|
|
||||||
userKicked,
|
|
||||||
userFirstSeen,
|
|
||||||
isLocalTrustedServer,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export const RoomPreviewContext: FC<{ inviterMember: RoomMember | null }> = ({ inviterMember }) => {
|
|
||||||
const { score, details } = useGetUserSafety(inviterMember);
|
|
||||||
const [learnMoreOpen, setLearnMoreOpen] = useState<boolean>(false);
|
|
||||||
|
|
||||||
if (!score) {
|
|
||||||
return (
|
|
||||||
<div className="mx_RoomPreviewContext_Badge">
|
|
||||||
<InlineSpinner />
|
|
||||||
<span>Checking invite safety</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { roomCount, joinedTo, userBanned, userKicked, userFirstSeen, isLocalTrustedServer } = details;
|
|
||||||
return (
|
|
||||||
<ul className="mx_RoomPreviewContext">
|
|
||||||
{isLocalTrustedServer && <SafetyDetailItem title="You are on the same server" description={learnMoreOpen ? "You share the same server as this user, and registration is disabled." : undefined} score={InviteScore.Safe} />}
|
|
||||||
{roomCount === 0 && <SafetyDetailItem title="You have no shared rooms" description={learnMoreOpen ? "You have no rooms in common with this user. This may be a spam invite." : undefined} score={InviteScore.Unsafe} />}
|
|
||||||
{userBanned && (
|
|
||||||
<SafetyDetailItem
|
|
||||||
score={InviteScore.Unsafe}
|
|
||||||
title="User has been banned from rooms in the past"
|
|
||||||
description={learnMoreOpen ? userBanned : undefined}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{userKicked && (
|
|
||||||
<SafetyDetailItem
|
|
||||||
score={InviteScore.Unsafe}
|
|
||||||
title="User has been kicked from rooms in the past"
|
|
||||||
description={learnMoreOpen ? userKicked : undefined}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{joinedTo && (
|
|
||||||
<SafetyDetailItem {...joinedTo} description={learnMoreOpen ? joinedTo.description : undefined} />
|
|
||||||
)}
|
|
||||||
{userFirstSeen && (
|
|
||||||
<SafetyDetailItem
|
|
||||||
{...userFirstSeen}
|
|
||||||
description={learnMoreOpen ? userFirstSeen.description : undefined}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{!learnMoreOpen && (
|
|
||||||
<li>
|
|
||||||
<Button kind="tertiary" size="sm" onClick={() => setLearnMoreOpen(true)}>
|
|
||||||
Explain safety information
|
|
||||||
</Button>
|
|
||||||
</li>
|
|
||||||
)}
|
|
||||||
</ul>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -16,6 +16,8 @@ import type {
|
|||||||
CustomMessageRenderHints as ModuleCustomCustomMessageRenderHints,
|
CustomMessageRenderHints as ModuleCustomCustomMessageRenderHints,
|
||||||
MatrixEvent as ModuleMatrixEvent,
|
MatrixEvent as ModuleMatrixEvent,
|
||||||
CustomRoomPreviewBarRenderFunction,
|
CustomRoomPreviewBarRenderFunction,
|
||||||
|
MessageProfileRenderFunction,
|
||||||
|
UserInfoRenderFunction,
|
||||||
} from "@element-hq/element-web-module-api";
|
} from "@element-hq/element-web-module-api";
|
||||||
import type React from "react";
|
import type React from "react";
|
||||||
|
|
||||||
@@ -42,7 +44,7 @@ export class CustomComponentsApi implements ICustomComponentsApi {
|
|||||||
* @param mxEvent
|
* @param mxEvent
|
||||||
* @returns An event object, or `null` if the event was not a message event.
|
* @returns An event object, or `null` if the event was not a message event.
|
||||||
*/
|
*/
|
||||||
private static getModuleMatrixEvent(mxEvent: MatrixEvent): ModuleMatrixEvent | null {
|
public static getModuleMatrixEvent(mxEvent: MatrixEvent): ModuleMatrixEvent | null {
|
||||||
const eventId = mxEvent.getId();
|
const eventId = mxEvent.getId();
|
||||||
const roomId = mxEvent.getRoomId();
|
const roomId = mxEvent.getRoomId();
|
||||||
const sender = mxEvent.sender;
|
const sender = mxEvent.sender;
|
||||||
@@ -138,6 +140,8 @@ export class CustomComponentsApi implements ICustomComponentsApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _roomPreviewBarRenderer?: CustomRoomPreviewBarRenderFunction;
|
private _roomPreviewBarRenderer?: CustomRoomPreviewBarRenderFunction;
|
||||||
|
private _userInfoRenderer?: UserInfoRenderFunction;
|
||||||
|
private _messageProfileRenderer?: MessageProfileRenderFunction;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the custom room preview bar renderer, if any has been registered.
|
* Get the custom room preview bar renderer, if any has been registered.
|
||||||
@@ -153,4 +157,25 @@ export class CustomComponentsApi implements ICustomComponentsApi {
|
|||||||
public registerRoomPreviewBar(renderer: CustomRoomPreviewBarRenderFunction): void {
|
public registerRoomPreviewBar(renderer: CustomRoomPreviewBarRenderFunction): void {
|
||||||
this._roomPreviewBarRenderer = renderer;
|
this._roomPreviewBarRenderer = renderer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the custom user info renderer, if any has been registered.
|
||||||
|
*/
|
||||||
|
public get messageProfileRenderer(): MessageProfileRenderFunction | undefined {
|
||||||
|
return this._messageProfileRenderer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public registerMessageProfile(renderer: MessageProfileRenderFunction): void {
|
||||||
|
this._messageProfileRenderer = renderer;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Get the custom user info renderer, if any has been registered.
|
||||||
|
*/
|
||||||
|
public get userInfoRenderer(): UserInfoRenderFunction | undefined {
|
||||||
|
return this._userInfoRenderer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public registerUserInfo(renderer: UserInfoRenderFunction): void {
|
||||||
|
this._userInfoRenderer = renderer;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user