Pillify event permalinks (#10392)
This commit is contained in:
parent
d8acdd1750
commit
96d1b74ffc
19 changed files with 742 additions and 144 deletions
|
@ -14,16 +14,18 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { MatrixEvent, Room, RoomMember } from "matrix-js-sdk/src/matrix";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { Room, RoomMember } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { ButtonEvent } from "../components/views/elements/AccessibleButton";
|
||||
import { PillType } from "../components/views/elements/Pill";
|
||||
import { MatrixClientPeg } from "../MatrixClientPeg";
|
||||
import { parsePermalink } from "../utils/permalinks/Permalinks";
|
||||
import dis from "../dispatcher/dispatcher";
|
||||
import { Action } from "../dispatcher/actions";
|
||||
import { PermalinkParts } from "../utils/permalinks/PermalinkConstructor";
|
||||
import { _t } from "../languageHandler";
|
||||
import { usePermalinkTargetRoom } from "./usePermalinkTargetRoom";
|
||||
import { usePermalinkEvent } from "./usePermalinkEvent";
|
||||
import { usePermalinkMember } from "./usePermalinkMember";
|
||||
|
||||
interface Args {
|
||||
/** Room in which the permalink should be displayed. */
|
||||
|
@ -71,97 +73,77 @@ interface HookResult {
|
|||
}
|
||||
|
||||
/**
|
||||
* Can be used to retrieve all information to display a permalink.
|
||||
* Tries to determine the pill type.
|
||||
*
|
||||
* If forcedType is present it will be returned.
|
||||
* If the parse result contains a room Id or alias and an event Id:
|
||||
* - Type is EventInSameRoom if the permalink room Id or alias equals the parsed room Id or alias
|
||||
* - Type is EventInOtherRoom if the permalink room Id or alias not equals the parsed room Id or alias
|
||||
* If the parse result contains a primary entity Id it will try to detect the type from it.
|
||||
* Otherwise returns null.
|
||||
*
|
||||
* @param forcedType - Forced pill type. Will be used if present and short-circuits all othe conditions.
|
||||
* @param parseResult - Permalink parser result
|
||||
* @param permalinkRoom - Room in which the permalink is displayed.
|
||||
* @returns Pill type or null if unable to determine.
|
||||
*/
|
||||
export const usePermalink: (args: Args) => HookResult = ({ room, type: argType, url }): HookResult => {
|
||||
const [member, setMember] = useState<RoomMember | null>(null);
|
||||
// room of the entity this pill points to
|
||||
const [targetRoom, setTargetRoom] = useState<Room | null>(room ?? null);
|
||||
const determineType = (
|
||||
forcedType: PillType | undefined,
|
||||
parseResult: PermalinkParts | null,
|
||||
permalinkRoom: Room | undefined,
|
||||
): PillType | null => {
|
||||
if (forcedType) return forcedType;
|
||||
|
||||
if (parseResult?.roomIdOrAlias && parseResult?.eventId) {
|
||||
if (parseResult.roomIdOrAlias === permalinkRoom?.roomId) {
|
||||
return PillType.EventInSameRoom;
|
||||
}
|
||||
|
||||
return PillType.EventInOtherRoom;
|
||||
}
|
||||
|
||||
if (parseResult?.primaryEntityId) {
|
||||
const prefix = parseResult.primaryEntityId[0] || "";
|
||||
return (
|
||||
{
|
||||
"@": PillType.UserMention,
|
||||
"#": PillType.RoomMention,
|
||||
"!": PillType.RoomMention,
|
||||
}[prefix] || null
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Can be used to retrieve all information needed to display a permalink.
|
||||
*/
|
||||
export const usePermalink: (args: Args) => HookResult = ({
|
||||
room: permalinkRoom,
|
||||
type: forcedType,
|
||||
url,
|
||||
}): HookResult => {
|
||||
let resourceId: string | null = null;
|
||||
let parseResult: PermalinkParts | null = null;
|
||||
|
||||
if (url) {
|
||||
const parseResult = parsePermalink(url);
|
||||
parseResult = parsePermalink(url);
|
||||
|
||||
if (parseResult?.primaryEntityId) {
|
||||
resourceId = parseResult.primaryEntityId;
|
||||
}
|
||||
}
|
||||
const prefix = resourceId ? resourceId[0] : "";
|
||||
const type =
|
||||
argType ||
|
||||
// try to detect the permalink type from the URL prefix
|
||||
{
|
||||
"@": PillType.UserMention,
|
||||
"#": PillType.RoomMention,
|
||||
"!": PillType.RoomMention,
|
||||
}[prefix] ||
|
||||
null;
|
||||
|
||||
const doProfileLookup = useCallback((userId: string, member: RoomMember): void => {
|
||||
MatrixClientPeg.get()
|
||||
.getProfileInfo(userId)
|
||||
.then((resp) => {
|
||||
const newMember = new RoomMember(member.roomId, userId);
|
||||
newMember.name = resp.displayname || userId;
|
||||
newMember.rawDisplayName = resp.displayname || userId;
|
||||
newMember.getMxcAvatarUrl();
|
||||
newMember.events.member = {
|
||||
getContent: () => {
|
||||
return { avatar_url: resp.avatar_url };
|
||||
},
|
||||
getDirectionalContent: function () {
|
||||
// eslint-disable-next-line
|
||||
return this.getContent();
|
||||
},
|
||||
} as MatrixEvent;
|
||||
setMember(newMember);
|
||||
})
|
||||
.catch((err) => {
|
||||
logger.error("Could not retrieve profile data for " + userId + ":", err);
|
||||
});
|
||||
}, []);
|
||||
|
||||
useMemo(() => {
|
||||
switch (type) {
|
||||
case PillType.UserMention:
|
||||
{
|
||||
if (resourceId) {
|
||||
let member = room?.getMember(resourceId) || null;
|
||||
setMember(member);
|
||||
|
||||
if (!member) {
|
||||
member = new RoomMember("", resourceId);
|
||||
doProfileLookup(resourceId, member);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case PillType.RoomMention:
|
||||
{
|
||||
if (resourceId) {
|
||||
const newRoom =
|
||||
resourceId[0] === "#"
|
||||
? MatrixClientPeg.get()
|
||||
.getRooms()
|
||||
.find((r) => {
|
||||
return (
|
||||
r.getCanonicalAlias() === resourceId ||
|
||||
(resourceId && r.getAltAliases().includes(resourceId))
|
||||
);
|
||||
})
|
||||
: MatrixClientPeg.get().getRoom(resourceId);
|
||||
setTargetRoom(newRoom || null);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}, [doProfileLookup, type, resourceId, room]);
|
||||
const type = determineType(forcedType, parseResult, permalinkRoom);
|
||||
const targetRoom = usePermalinkTargetRoom(type, parseResult, permalinkRoom);
|
||||
const event = usePermalinkEvent(type, parseResult, targetRoom);
|
||||
const member = usePermalinkMember(type, parseResult, targetRoom, event);
|
||||
|
||||
let onClick: (e: ButtonEvent) => void = () => {};
|
||||
let text = resourceId;
|
||||
|
||||
if (type === PillType.AtRoomMention && room) {
|
||||
if (type === PillType.AtRoomMention && permalinkRoom) {
|
||||
text = "@room";
|
||||
} else if (type === PillType.UserMention && member) {
|
||||
text = member.name || resourceId;
|
||||
|
@ -177,6 +159,10 @@ export const usePermalink: (args: Args) => HookResult = ({ room, type: argType,
|
|||
if (targetRoom) {
|
||||
text = targetRoom.name || resourceId;
|
||||
}
|
||||
} else if (type === PillType.EventInSameRoom) {
|
||||
text = member?.name || _t("User");
|
||||
} else if (type === PillType.EventInOtherRoom) {
|
||||
text = targetRoom?.name || _t("Room");
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
89
src/hooks/usePermalinkEvent.ts
Normal file
89
src/hooks/usePermalinkEvent.ts
Normal file
|
@ -0,0 +1,89 @@
|
|||
/*
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
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, Room } from "matrix-js-sdk/src/matrix";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { PillType } from "../components/views/elements/Pill";
|
||||
import { MatrixClientPeg } from "../MatrixClientPeg";
|
||||
import { PermalinkParts } from "../utils/permalinks/PermalinkConstructor";
|
||||
|
||||
/**
|
||||
* Tries to find the initial event.
|
||||
* If the event should not be looked up or there is no target room it returns null.
|
||||
* Otherwise it tries to get the event from the target room.
|
||||
*
|
||||
* @param shouldLookUpEvent - whether the parmalink event should be looked up
|
||||
* @param Room | null targetRoom - target room of the permalink
|
||||
* @param parseResult - permalink parse result
|
||||
* @returns The event if found or null if it should not be looked up or was not found.
|
||||
*/
|
||||
const determineInitialEvent = (
|
||||
shouldLookUpEvent: boolean,
|
||||
targetRoom: Room | null,
|
||||
parseResult: PermalinkParts | null,
|
||||
): MatrixEvent | null => {
|
||||
if (!shouldLookUpEvent || !targetRoom || !parseResult?.eventId) return null;
|
||||
|
||||
return targetRoom.findEventById(parseResult.eventId) || null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to get a permalink target event
|
||||
*
|
||||
* @param type - Permalink type
|
||||
* @param parseResult - Permalink parse result
|
||||
* @param targetRoom - Target room of the permalink {@link ./usePermalinkTargetRoom.ts}
|
||||
* @returns The permalink event if it targets an event and it can be loaded.
|
||||
* Else null.
|
||||
*/
|
||||
export const usePermalinkEvent = (
|
||||
type: PillType | null,
|
||||
parseResult: PermalinkParts | null,
|
||||
targetRoom: Room | null,
|
||||
): MatrixEvent | null => {
|
||||
// Event permalinks require to know the event.
|
||||
// If it cannot be initially determined, it will be looked up later by a memo hook.
|
||||
const shouldLookUpEvent =
|
||||
!!type &&
|
||||
!!parseResult?.roomIdOrAlias &&
|
||||
!!parseResult?.eventId &&
|
||||
[PillType.EventInSameRoom, PillType.EventInOtherRoom].includes(type);
|
||||
const eventId = parseResult?.eventId;
|
||||
const eventInRoom = determineInitialEvent(shouldLookUpEvent, targetRoom, parseResult);
|
||||
const [event, setEvent] = useState<MatrixEvent | null>(eventInRoom);
|
||||
|
||||
useEffect(() => {
|
||||
if (!shouldLookUpEvent || !eventId || event || !parseResult?.roomIdOrAlias || !parseResult.eventId) {
|
||||
// nothing to do here
|
||||
return;
|
||||
}
|
||||
|
||||
const fetchRoomEvent = async (): Promise<void> => {
|
||||
try {
|
||||
const eventData = await MatrixClientPeg.get().fetchRoomEvent(
|
||||
parseResult.roomIdOrAlias,
|
||||
parseResult.eventId,
|
||||
);
|
||||
setEvent(new MatrixEvent(eventData));
|
||||
} catch {}
|
||||
};
|
||||
|
||||
fetchRoomEvent();
|
||||
}, [event, eventId, parseResult?.eventId, parseResult?.roomIdOrAlias, shouldLookUpEvent]);
|
||||
|
||||
return event;
|
||||
};
|
111
src/hooks/usePermalinkMember.ts
Normal file
111
src/hooks/usePermalinkMember.ts
Normal file
|
@ -0,0 +1,111 @@
|
|||
/*
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
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 { logger } from "matrix-js-sdk/src/logger";
|
||||
import { MatrixEvent, Room, RoomMember } from "matrix-js-sdk/src/matrix";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { PillType } from "../components/views/elements/Pill";
|
||||
import { MatrixClientPeg } from "../MatrixClientPeg";
|
||||
import { PermalinkParts } from "../utils/permalinks/PermalinkConstructor";
|
||||
|
||||
/**
|
||||
* Tries to determine the user Id of a permalink.
|
||||
* In case of a user permalink it is the user id.
|
||||
* In case of an event permalink it is the sender user Id of the event if that event is available.
|
||||
* Otherwise returns null.
|
||||
*
|
||||
* @param type - pill type
|
||||
* @param parseResult - permalink parse result
|
||||
* @param event - permalink event, if available
|
||||
* @returns permalink user Id. null if the Id cannot be determined.
|
||||
*/
|
||||
const determineUserId = (
|
||||
type: PillType | null,
|
||||
parseResult: PermalinkParts | null,
|
||||
event: MatrixEvent | null,
|
||||
): string | null => {
|
||||
if (type === null) return null;
|
||||
|
||||
if (parseResult?.userId) return parseResult.userId;
|
||||
|
||||
if (event && [PillType.EventInSameRoom, PillType.EventInOtherRoom].includes(type)) {
|
||||
return event.getSender() ?? null;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to get the permalink member
|
||||
*
|
||||
* @param type - Permalink type
|
||||
* @param parseResult - Permalink parse result
|
||||
* @param targetRoom - Permalink target room {@link ./usePermalinkTargetRoom.ts}
|
||||
* @param event - Permalink event
|
||||
* @returns The permalink member:
|
||||
* - The room member for a user mention
|
||||
* - The sender for a permalink to an event in the same room
|
||||
* - Null in other cases or the user cannot be loaded.
|
||||
*/
|
||||
export const usePermalinkMember = (
|
||||
type: PillType | null,
|
||||
parseResult: PermalinkParts | null,
|
||||
targetRoom: Room | null,
|
||||
event: MatrixEvent | null,
|
||||
): RoomMember | null => {
|
||||
// User mentions and permalinks to events in the same room require to know the user.
|
||||
// If it cannot be initially determined, it will be looked up later by a memo hook.
|
||||
const shouldLookUpUser = type && [PillType.UserMention, PillType.EventInSameRoom].includes(type);
|
||||
const userId = determineUserId(type, parseResult, event);
|
||||
const userInRoom = shouldLookUpUser && userId && targetRoom ? targetRoom.getMember(userId) : null;
|
||||
const [member, setMember] = useState<RoomMember | null>(userInRoom);
|
||||
|
||||
useEffect(() => {
|
||||
if (!shouldLookUpUser || !userId || member) {
|
||||
// nothing to do here
|
||||
return;
|
||||
}
|
||||
|
||||
const doProfileLookup = (userId: string): void => {
|
||||
MatrixClientPeg.get()
|
||||
.getProfileInfo(userId)
|
||||
.then((resp) => {
|
||||
const newMember = new RoomMember("", userId);
|
||||
newMember.name = resp.displayname || userId;
|
||||
newMember.rawDisplayName = resp.displayname || userId;
|
||||
newMember.getMxcAvatarUrl();
|
||||
newMember.events.member = {
|
||||
getContent: () => {
|
||||
return { avatar_url: resp.avatar_url };
|
||||
},
|
||||
getDirectionalContent: function () {
|
||||
// eslint-disable-next-line
|
||||
return this.getContent();
|
||||
},
|
||||
} as MatrixEvent;
|
||||
setMember(newMember);
|
||||
})
|
||||
.catch((err) => {
|
||||
logger.error("Could not retrieve profile data for " + userId + ":", err);
|
||||
});
|
||||
};
|
||||
|
||||
doProfileLookup(userId);
|
||||
}, [member, shouldLookUpUser, targetRoom, userId]);
|
||||
|
||||
return member;
|
||||
};
|
103
src/hooks/usePermalinkTargetRoom.ts
Normal file
103
src/hooks/usePermalinkTargetRoom.ts
Normal file
|
@ -0,0 +1,103 @@
|
|||
/*
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
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 { Room } from "matrix-js-sdk/src/matrix";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { PillType } from "../components/views/elements/Pill";
|
||||
import { MatrixClientPeg } from "../MatrixClientPeg";
|
||||
import { PermalinkParts } from "../utils/permalinks/PermalinkConstructor";
|
||||
|
||||
/**
|
||||
* Tries to determine the initial room.
|
||||
* Initial here means it should be possible to load the room without sending API requests.
|
||||
* For an @room or a user mention it is the permalinkRoom.
|
||||
* If the parse result contains a room Id or alias try to find it with {@link findRoom}.
|
||||
* Otherwise returns null.
|
||||
*
|
||||
* @param type - Pill type
|
||||
* @param permalinkRoom - Room in which the permalink is displayed.
|
||||
* @param parseResult - Permalink parser result
|
||||
* @returns Initial room or null if it cannot be determined.
|
||||
*/
|
||||
const determineInitialRoom = (
|
||||
type: PillType | null,
|
||||
parseResult: PermalinkParts | null,
|
||||
permalinkRoom: Room | undefined,
|
||||
): Room | null => {
|
||||
if (type === PillType.AtRoomMention && permalinkRoom) return permalinkRoom;
|
||||
|
||||
if (type === PillType.UserMention && permalinkRoom) {
|
||||
return permalinkRoom;
|
||||
}
|
||||
|
||||
if (parseResult?.roomIdOrAlias) {
|
||||
const room = findRoom(parseResult.roomIdOrAlias);
|
||||
if (room) return room;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Tries to find a room by room Id or searching all rooms for an alias.
|
||||
*
|
||||
* @param roomIdOrAlias - Id or alias of the room to find.
|
||||
* @returns Room if found, else null.
|
||||
*/
|
||||
const findRoom = (roomIdOrAlias: string): Room | null => {
|
||||
const client = MatrixClientPeg.get();
|
||||
|
||||
return roomIdOrAlias[0] === "#"
|
||||
? client.getRooms().find((r) => {
|
||||
return r.getCanonicalAlias() === roomIdOrAlias || r.getAltAliases().includes(roomIdOrAlias);
|
||||
}) ?? null
|
||||
: client.getRoom(roomIdOrAlias);
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to get the permalink target room:
|
||||
*
|
||||
* @param type - Permalink type
|
||||
* @param parseResult - Permalink parse result
|
||||
* @param permalinkRoom - Room in which the permalink is rendered
|
||||
* @returns Returns the target room:
|
||||
* - The permalinkRoom for an @room or user mention
|
||||
* - The room of the parse result for a room mention
|
||||
* - The room of the event for an event permalink
|
||||
* - Null in other cases or if the room cannot be found
|
||||
*/
|
||||
export const usePermalinkTargetRoom = (
|
||||
type: PillType | null,
|
||||
parseResult: PermalinkParts | null,
|
||||
permalinkRoom: Room | null,
|
||||
): Room | null => {
|
||||
// The listed permalink types require a room.
|
||||
// If it cannot be initially determined, it will be looked up later by a memo hook.
|
||||
const shouldLookUpRoom =
|
||||
type && [PillType.RoomMention, PillType.EventInSameRoom, PillType.EventInOtherRoom, "space"].includes(type);
|
||||
const initialRoom = determineInitialRoom(type, parseResult, permalinkRoom);
|
||||
const [targetRoom, setTargetRoom] = useState<Room | null>(initialRoom);
|
||||
|
||||
useEffect(() => {
|
||||
if (shouldLookUpRoom && !targetRoom && parseResult?.roomIdOrAlias) {
|
||||
const newRoom = findRoom(parseResult.roomIdOrAlias);
|
||||
setTargetRoom(newRoom);
|
||||
}
|
||||
}, [parseResult?.roomIdOrAlias, shouldLookUpRoom, targetRoom]);
|
||||
|
||||
return targetRoom;
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue