Support 3pid invites

This commit is contained in:
R Midhun Suresh
2024-12-11 18:29:20 +05:30
parent 7ba63538f8
commit 4d4e7f3c9c
5 changed files with 149 additions and 45 deletions

View File

@@ -43,8 +43,39 @@ import PosthogTrackers from "../../PosthogTrackers";
import { ButtonEvent } from "../views/elements/AccessibleButton"; import { ButtonEvent } from "../views/elements/AccessibleButton";
import { inviteToRoom } from "../../utils/room/inviteToRoom"; import { inviteToRoom } from "../../utils/room/inviteToRoom";
import { canInviteTo } from "../../utils/room/canInviteTo"; import { canInviteTo } from "../../utils/room/canInviteTo";
import { isValid3pidInvite } from "../../RoomInvite";
import { ThreePIDInvite } from "../../models/rooms/ThreePIDInvite";
import { XOR } from "../../@types/common";
function sdkRoomMemberToRoomMember(member: SDKRoomMember): RoomMember { type Member = XOR<{ member: RoomMember }, { threePidInvite: ThreePIDInvite }>;
function getPending3PidInvites(room: Room, searchQuery?: string): Member[] {
// include 3pid invites (m.room.third_party_invite) state events.
// The HS may have already converted these into m.room.member invites so
// we shouldn't add them if the 3pid invite state key (token) is in the
// member invite (content.third_party_invite.signed.token)
const inviteEvents = room.currentState.getStateEvents("m.room.third_party_invite").filter(function (e) {
if (!isValid3pidInvite(e)) return false;
if (searchQuery && !(e.getContent().display_name as string)?.includes(searchQuery)) return false;
// discard all invites which have a m.room.member event since we've
// already added them.
const memberEvent = room.currentState.getInviteForThreePidToken(e.getStateKey()!);
if (memberEvent) return false;
return true;
});
const invites: Member[] = inviteEvents.map((e) => {
return {
threePidInvite: {
displayName: e.getContent().display_name,
event: e,
},
};
});
return invites;
}
function sdkRoomMemberToRoomMember(member: SDKRoomMember): Member {
const displayUserId = const displayUserId =
UserIdentifierCustomisations.getDisplayUserIdentifier(member.userId, { UserIdentifierCustomisations.getDisplayUserIdentifier(member.userId, {
roomId: member.roomId, roomId: member.roomId,
@@ -61,22 +92,24 @@ function sdkRoomMemberToRoomMember(member: SDKRoomMember): RoomMember {
} }
return { return {
roomId: member.roomId, member: {
userId: member.userId, roomId: member.roomId,
displayUserId: displayUserId, userId: member.userId,
name: member.name, displayUserId: displayUserId,
rawDisplayName: member.rawDisplayName, name: member.name,
disambiguate: member.disambiguate, rawDisplayName: member.rawDisplayName,
avatarThumbnailUrl: avatarThumbnailUrl, disambiguate: member.disambiguate,
powerLevel: member.powerLevel, avatarThumbnailUrl: avatarThumbnailUrl,
lastModifiedTime: member.getLastModifiedTime(), powerLevel: member.powerLevel,
presenceState, lastModifiedTime: member.getLastModifiedTime(),
isInvite: member.membership === KnownMembership.Invite, presenceState,
isInvite: member.membership === KnownMembership.Invite,
},
}; };
} }
export interface MemberListViewState { export interface MemberListViewState {
members: RoomMember[]; members: Member[];
memberCount: number; memberCount: number;
search: (searchQuery: string) => void; search: (searchQuery: string) => void;
isPresenceEnabled: boolean; isPresenceEnabled: boolean;
@@ -85,7 +118,6 @@ export interface MemberListViewState {
canInvite: boolean; canInvite: boolean;
onInviteButtonClick: (ev: ButtonEvent) => void; onInviteButtonClick: (ev: ButtonEvent) => void;
} }
export function useMemberListViewModel(roomId: string): MemberListViewState { export function useMemberListViewModel(roomId: string): MemberListViewState {
const cli = useMatrixClientContext(); const cli = useMatrixClientContext();
const room = useMemo(() => cli.getRoom(roomId), [roomId, cli]); const room = useMemo(() => cli.getRoom(roomId), [roomId, cli]);
@@ -93,7 +125,7 @@ export function useMemberListViewModel(roomId: string): MemberListViewState {
throw new Error(`Room with id ${roomId} does not exist!`); throw new Error(`Room with id ${roomId} does not exist!`);
} }
const sdkContext = useContext(SDKContext); const sdkContext = useContext(SDKContext);
const [members, setMembers] = useState<RoomMember[]>([]); const [members, setMembers] = useState<Member[]>([]);
const [memberCount, setMemberCount] = useState<number>(0); const [memberCount, setMemberCount] = useState<number>(0);
const searchQuery = useRef(""); const searchQuery = useRef("");
const [isLoading, setIsLoading] = useState<boolean>(true); const [isLoading, setIsLoading] = useState<boolean>(true);
@@ -108,13 +140,16 @@ export function useMemberListViewModel(roomId: string): MemberListViewState {
); );
const joined = joinedSdk.map(sdkRoomMemberToRoomMember); const joined = joinedSdk.map(sdkRoomMemberToRoomMember);
const invited = invitedSdk.map(sdkRoomMemberToRoomMember); const invited = invitedSdk.map(sdkRoomMemberToRoomMember);
setMembers([...invited, ...joined]); const threePidInvited = getPending3PidInvites(room, searchQuery.current);
if (!searchQuery.current) setMemberCount(joined.length); const newMembers = [...invited, ...threePidInvited, ...joined];
setMembers(newMembers);
if (!searchQuery.current) setMemberCount(newMembers.length);
}, },
500, 500,
{ leading: true, trailing: true }, { leading: true, trailing: true },
), ),
[roomId, sdkContext.memberListStore], //todo: can we remove room here?
[roomId, sdkContext.memberListStore, room],
); );
const search = useCallback( const search = useCallback(

View File

@@ -30,17 +30,22 @@ import { RoomMember } from "../../models/rooms/RoomMember";
import { E2EState } from "../views/rooms/E2EIcon"; import { E2EState } from "../views/rooms/E2EIcon";
import { _t, _td, TranslationKey } from "../../languageHandler"; import { _t, _td, TranslationKey } from "../../languageHandler";
import UserIdentifierCustomisations from "../../customisations/UserIdentifier"; import UserIdentifierCustomisations from "../../customisations/UserIdentifier";
import { ThreePIDInvite } from "../../models/rooms/ThreePIDInvite";
interface IProps { interface MemberTileViewModelProps {
member: RoomMember; member: RoomMember;
showPresence?: boolean; showPresence?: boolean;
} }
export interface MemberTileViewState extends IProps { interface ThreePidTileViewModelProps {
threePidInvite: ThreePIDInvite;
}
export interface MemberTileViewState extends MemberTileViewModelProps {
e2eStatus?: E2EState; e2eStatus?: E2EState;
name: string; name: string;
onClick: () => void; onClick: () => void;
title: string; title?: string;
userLabel?: string; userLabel?: string;
} }
@@ -54,7 +59,28 @@ const PowerLabel: Record<PowerStatus, TranslationKey> = {
[PowerStatus.Moderator]: _td("power_level|mod"), [PowerStatus.Moderator]: _td("power_level|mod"),
}; };
export default function useMemberTileViewModel(props: IProps): MemberTileViewState { export interface ThreePidTileViewState {
name: string;
onClick: () => void;
}
export function useThreePidTileViewModel(props: ThreePidTileViewModelProps): ThreePidTileViewState {
const invite = props.threePidInvite;
const name = invite.displayName;
const onClick = (): void => {
dis.dispatch({
action: Action.View3pidInvite,
event: invite.event,
});
};
return {
name,
onClick,
};
}
export function useMemberTileViewModel(props: MemberTileViewModelProps): MemberTileViewState {
const [e2eStatus, setE2eStatus] = useState<E2EState | undefined>(); const [e2eStatus, setE2eStatus] = useState<E2EState | undefined>();
useEffect(() => { useEffect(() => {

View File

@@ -21,7 +21,7 @@ import { AutoSizer } from "react-virtualized";
import { Flex } from "../../utils/Flex"; import { Flex } from "../../utils/Flex";
import { useMemberListViewModel } from "../../viewmodels/MemberListViewModel"; import { useMemberListViewModel } from "../../viewmodels/MemberListViewModel";
import MemberTileNext from "./MemberTileView"; import { RoomMemberTileView, ThreePidInviteTileView } from "./MemberTileView";
import MemberListHeaderView from "./MemberListHeaderView"; import MemberListHeaderView from "./MemberListHeaderView";
import BaseCard from "../right_panel/BaseCard"; import BaseCard from "../right_panel/BaseCard";
import { _t } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
@@ -35,10 +35,14 @@ const MemberListView: React.FC<IProps> = (props: IProps) => {
const vm = useMemberListViewModel(props.roomId); const vm = useMemberListViewModel(props.roomId);
const rowRenderer = ({ key, index, style }: ListRowProps): React.JSX.Element => { const rowRenderer = ({ key, index, style }: ListRowProps): React.JSX.Element => {
const member = vm.members[index]; const item = vm.members[index];
return ( return (
<div key={key} style={style}> <div key={key} style={style}>
<MemberTileNext member={member} showPresence={false} /> {item.member ? (
<RoomMemberTileView member={item.member} showPresence={vm.isPresenceEnabled} />
) : (
<ThreePidInviteTileView threePidInvite={item.threePidInvite} />
)}
</div> </div>
); );
}; };

View File

@@ -20,17 +20,59 @@ import React from "react";
import DisambiguatedProfile from "../messages/DisambiguatedProfile"; import DisambiguatedProfile from "../messages/DisambiguatedProfile";
import { RoomMember } from "../../../models/rooms/RoomMember"; import { RoomMember } from "../../../models/rooms/RoomMember";
import MemberAvatarNext from "../avatars/MemberAvatarView"; import MemberAvatarNext from "../avatars/MemberAvatarView";
import useMemberTileViewModel from "../../viewmodels/MemberTileViewModel"; import { useThreePidTileViewModel, useMemberTileViewModel } from "../../viewmodels/MemberTileViewModel";
import E2EIcon from "./E2EIconView"; import E2EIcon from "./E2EIconView";
import AvatarPresenceIconView from "./PresenceIconView"; import AvatarPresenceIconView from "./PresenceIconView";
import AccessibleButton from "../elements/AccessibleButton"; import AccessibleButton from "../elements/AccessibleButton";
import { ThreePIDInvite } from "../../../models/rooms/ThreePIDInvite";
import BaseAvatar from "../avatars/BaseAvatar";
interface IProps { interface IProps {
member: RoomMember; member: RoomMember;
showPresence?: boolean; showPresence?: boolean;
} }
export default function MemberTileView(props: IProps): JSX.Element { interface ThreePidProps {
threePidInvite: ThreePIDInvite;
}
interface TileProps {
avatarJsx: JSX.Element;
nameJsx: JSX.Element | string;
onClick: () => void;
title?: string;
presenceJsx?: JSX.Element;
userLabelJsx?: JSX.Element;
e2eIconJsx?: JSX.Element;
}
function MemberTile(props: TileProps): JSX.Element {
return (
// The wrapping div is required to make the magic mouse listener work, for some reason.
<div>
<AccessibleButton className="mx_MemberTileView" title={props.title} onClick={props.onClick}>
<div className="mx_MemberTileView_left">
<div className="mx_MemberTileView_avatar">
{props.avatarJsx} {props.presenceJsx}
</div>
<div className="mx_MemberTileView_name">{props.nameJsx}</div>
</div>
<div className="mx_MemberTileView_right">
{props.userLabelJsx}
{props.e2eIconJsx}
</div>
</AccessibleButton>
</div>
);
}
export function ThreePidInviteTileView(props: ThreePidProps): JSX.Element {
const vm = useThreePidTileViewModel(props);
const av = <BaseAvatar name={vm.name} size="36px" aria-hidden="true" />;
return <MemberTile nameJsx={vm.name} avatarJsx={av} onClick={vm.onClick} />;
}
export function RoomMemberTileView(props: IProps): JSX.Element {
const vm = useMemberTileViewModel(props); const vm = useMemberTileViewModel(props);
const member = vm.member; const member = vm.member;
const av = <MemberAvatarNext member={member} size="32px" aria-hidden="true" />; const av = <MemberAvatarNext member={member} size="32px" aria-hidden="true" />;
@@ -39,7 +81,10 @@ export default function MemberTileView(props: IProps): JSX.Element {
const nameJSX = <DisambiguatedProfile member={member} fallbackName={name || ""} />; const nameJSX = <DisambiguatedProfile member={member} fallbackName={name || ""} />;
const presenceState = member.presenceState; const presenceState = member.presenceState;
const presenceJSX = vm.showPresence && presenceState && <AvatarPresenceIconView presenceState={presenceState} />; let presenceJSX: JSX.Element | undefined;
if (vm.showPresence && presenceState) {
presenceJSX = <AvatarPresenceIconView presenceState={presenceState} />;
}
let userLabelJSX; let userLabelJSX;
if (vm.userLabel) { if (vm.userLabel) {
@@ -51,21 +96,15 @@ export default function MemberTileView(props: IProps): JSX.Element {
e2eIcon = <E2EIcon isUser={true} status={vm.e2eStatus} />; e2eIcon = <E2EIcon isUser={true} status={vm.e2eStatus} />;
} }
// The wrapping div is required to make the magic mouse listener work, for some reason.
return ( return (
<div> <MemberTile
<AccessibleButton className="mx_MemberTileView" title={vm.title} onClick={vm.onClick}> title={vm.title}
<div className="mx_MemberTileView_left"> onClick={vm.onClick}
<div className="mx_MemberTileView_avatar"> avatarJsx={av}
{av} {presenceJSX} presenceJsx={presenceJSX}
</div> nameJsx={nameJSX}
<div className="mx_MemberTileView_name">{nameJSX}</div> userLabelJsx={userLabelJSX}
</div> e2eIconJsx={e2eIcon}
<div className="mx_MemberTileView_right"> />
{userLabelJSX}
{e2eIcon}
</div>
</AccessibleButton>
</div>
); );
} }

View File

@@ -13,9 +13,9 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { MatrixEvent } from "matrix-js-sdk/src/matrix";
export type ThreePIDInvite = { export type ThreePIDInvite = {
eventId: string;
stateKey: string;
displayName: string; displayName: string;
event: MatrixEvent;
}; };