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 { inviteToRoom } from "../../utils/room/inviteToRoom";
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 =
UserIdentifierCustomisations.getDisplayUserIdentifier(member.userId, {
roomId: member.roomId,
@@ -61,22 +92,24 @@ function sdkRoomMemberToRoomMember(member: SDKRoomMember): RoomMember {
}
return {
roomId: member.roomId,
userId: member.userId,
displayUserId: displayUserId,
name: member.name,
rawDisplayName: member.rawDisplayName,
disambiguate: member.disambiguate,
avatarThumbnailUrl: avatarThumbnailUrl,
powerLevel: member.powerLevel,
lastModifiedTime: member.getLastModifiedTime(),
presenceState,
isInvite: member.membership === KnownMembership.Invite,
member: {
roomId: member.roomId,
userId: member.userId,
displayUserId: displayUserId,
name: member.name,
rawDisplayName: member.rawDisplayName,
disambiguate: member.disambiguate,
avatarThumbnailUrl: avatarThumbnailUrl,
powerLevel: member.powerLevel,
lastModifiedTime: member.getLastModifiedTime(),
presenceState,
isInvite: member.membership === KnownMembership.Invite,
},
};
}
export interface MemberListViewState {
members: RoomMember[];
members: Member[];
memberCount: number;
search: (searchQuery: string) => void;
isPresenceEnabled: boolean;
@@ -85,7 +118,6 @@ export interface MemberListViewState {
canInvite: boolean;
onInviteButtonClick: (ev: ButtonEvent) => void;
}
export function useMemberListViewModel(roomId: string): MemberListViewState {
const cli = useMatrixClientContext();
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!`);
}
const sdkContext = useContext(SDKContext);
const [members, setMembers] = useState<RoomMember[]>([]);
const [members, setMembers] = useState<Member[]>([]);
const [memberCount, setMemberCount] = useState<number>(0);
const searchQuery = useRef("");
const [isLoading, setIsLoading] = useState<boolean>(true);
@@ -108,13 +140,16 @@ export function useMemberListViewModel(roomId: string): MemberListViewState {
);
const joined = joinedSdk.map(sdkRoomMemberToRoomMember);
const invited = invitedSdk.map(sdkRoomMemberToRoomMember);
setMembers([...invited, ...joined]);
if (!searchQuery.current) setMemberCount(joined.length);
const threePidInvited = getPending3PidInvites(room, searchQuery.current);
const newMembers = [...invited, ...threePidInvited, ...joined];
setMembers(newMembers);
if (!searchQuery.current) setMemberCount(newMembers.length);
},
500,
{ leading: true, trailing: true },
),
[roomId, sdkContext.memberListStore],
//todo: can we remove room here?
[roomId, sdkContext.memberListStore, room],
);
const search = useCallback(

View File

@@ -30,17 +30,22 @@ import { RoomMember } from "../../models/rooms/RoomMember";
import { E2EState } from "../views/rooms/E2EIcon";
import { _t, _td, TranslationKey } from "../../languageHandler";
import UserIdentifierCustomisations from "../../customisations/UserIdentifier";
import { ThreePIDInvite } from "../../models/rooms/ThreePIDInvite";
interface IProps {
interface MemberTileViewModelProps {
member: RoomMember;
showPresence?: boolean;
}
export interface MemberTileViewState extends IProps {
interface ThreePidTileViewModelProps {
threePidInvite: ThreePIDInvite;
}
export interface MemberTileViewState extends MemberTileViewModelProps {
e2eStatus?: E2EState;
name: string;
onClick: () => void;
title: string;
title?: string;
userLabel?: string;
}
@@ -54,7 +59,28 @@ const PowerLabel: Record<PowerStatus, TranslationKey> = {
[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>();
useEffect(() => {

View File

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

View File

@@ -20,17 +20,59 @@ import React from "react";
import DisambiguatedProfile from "../messages/DisambiguatedProfile";
import { RoomMember } from "../../../models/rooms/RoomMember";
import MemberAvatarNext from "../avatars/MemberAvatarView";
import useMemberTileViewModel from "../../viewmodels/MemberTileViewModel";
import { useThreePidTileViewModel, useMemberTileViewModel } from "../../viewmodels/MemberTileViewModel";
import E2EIcon from "./E2EIconView";
import AvatarPresenceIconView from "./PresenceIconView";
import AccessibleButton from "../elements/AccessibleButton";
import { ThreePIDInvite } from "../../../models/rooms/ThreePIDInvite";
import BaseAvatar from "../avatars/BaseAvatar";
interface IProps {
member: RoomMember;
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 member = vm.member;
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 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;
if (vm.userLabel) {
@@ -51,21 +96,15 @@ export default function MemberTileView(props: IProps): JSX.Element {
e2eIcon = <E2EIcon isUser={true} status={vm.e2eStatus} />;
}
// The wrapping div is required to make the magic mouse listener work, for some reason.
return (
<div>
<AccessibleButton className="mx_MemberTileView" title={vm.title} onClick={vm.onClick}>
<div className="mx_MemberTileView_left">
<div className="mx_MemberTileView_avatar">
{av} {presenceJSX}
</div>
<div className="mx_MemberTileView_name">{nameJSX}</div>
</div>
<div className="mx_MemberTileView_right">
{userLabelJSX}
{e2eIcon}
</div>
</AccessibleButton>
</div>
<MemberTile
title={vm.title}
onClick={vm.onClick}
avatarJsx={av}
presenceJsx={presenceJSX}
nameJsx={nameJSX}
userLabelJsx={userLabelJSX}
e2eIconJsx={e2eIcon}
/>
);
}

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
limitations under the License.
*/
import { MatrixEvent } from "matrix-js-sdk/src/matrix";
export type ThreePIDInvite = {
eventId: string;
stateKey: string;
displayName: string;
event: MatrixEvent;
};