move notifications to new sdk

This commit is contained in:
Ajay Bura 2026-08-25 11:25:30 +05:30
commit 8ef2ecb419
3 changed files with 394 additions and 381 deletions

View file

@ -3,7 +3,7 @@ import { PendingObservableResult } from 'neurone/lib/utils/observable';
import { RoomMembership } from 'neurone/lib/api/types';
import { useClient } from './useClient';
import { useObservableValuePromise } from './useObservableValue';
import { JoinRoomMemberObject } from '../../client/storage/collections';
import { getJoinRoomMemberId, JoinRoomMemberObject } from '../../client/storage/collections';
import { getMxIdLocalPart } from '../utils/matrix';
import {
roomMemberContentAvatarUrl,
@ -23,14 +23,9 @@ export type RoomMembersAPI = {
memberEventsResult: PendingObservableResult<JoinRoomMemberObject[]>;
};
export const useCreateRoomMembersAPI = (roomId: string): RoomMembersAPI => {
const ct = useClient();
const memberEventsPromise = useMemo(
() => ct.storage.joinRoomMember.observableRead.queryIndex('roomId', roomId),
[ct, roomId]
);
const memberEventsResult = useObservableValuePromise(memberEventsPromise);
const useMemberEvents = (
memberEventsResult: PendingObservableResult<JoinRoomMemberObject[]>
): RoomMembersAPI => {
const memberEvents = memberEventsResult.value;
const userIdToMemberEvent = useMemo(() => {
@ -92,6 +87,18 @@ export const useCreateRoomMembersAPI = (roomId: string): RoomMembersAPI => {
return api;
};
export const useCreateRoomMembersAPI = (roomId: string): RoomMembersAPI => {
const ct = useClient();
const memberEventsPromise = useMemo(
() => ct.storage.joinRoomMember.observableRead.queryIndex('roomId', roomId),
[ct, roomId]
);
const memberEventsResult = useObservableValuePromise(memberEventsPromise);
return useMemberEvents(memberEventsResult);
};
const RoomMembersAPIContext = createContext<RoomMembersAPI | null>(null);
export const RoomMembersAPIProvider = RoomMembersAPIContext.Provider;
@ -103,3 +110,27 @@ export const useRoomMembersAPI = (): RoomMembersAPI => {
return api;
};
export const useRoomMembers = (roomId: string, users: string[]) => {
const ct = useClient();
const memberEventsPromise = useMemo(
() =>
ct.storage.joinRoomMember.observableRead.getMultiple(
users.map((userId) => getJoinRoomMemberId(roomId, userId))
),
[ct, roomId, users]
);
const memberEventsResult = useObservableValuePromise(memberEventsPromise);
const memberEvents = useMemo(() => {
if (memberEventsResult.value) {
return {
value: memberEventsResult.value.filter((i) => i !== undefined),
};
}
return memberEventsResult;
}, [memberEventsResult]);
return useMemberEvents(memberEvents);
};

View file

@ -1,5 +1,13 @@
/* eslint-disable react/destructuring-assignment */
import React, { MouseEventHandler, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import React, {
MouseEventHandler,
ReactNode,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import {
Avatar,
Box,
@ -14,75 +22,50 @@ import {
toRem,
} from 'folds';
import { useSearchParams } from 'react-router-dom';
import {
INotification,
INotificationsResponse,
IRoomEvent,
JoinRule,
Method,
RelationType,
Room,
} from 'matrix-js-sdk';
import { useVirtualizer } from '@tanstack/react-virtual';
import { HTMLReactParserOptions } from 'html-react-parser';
import { Opts as LinkifyOpts } from 'linkifyjs';
import { useAtomValue } from 'jotai';
import {
getNotifications,
GetNotificationsOK,
} from 'neurone/lib/api/matrix/client/v3/notifications';
import { parseMatrixResp } from 'neurone/lib/matrix-request';
import { NotificationChunk } from 'neurone/lib/api/types';
import { Page, PageContent, PageContentCenter, PageHeader } from '../../../components/page';
import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { getMxIdLocalPart, mxcUrlToHttp } from '../../../utils/matrix';
import { InboxNotificationsPathSearchParams } from '../../paths';
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
import { SequenceCard } from '../../../components/sequence-card';
import { RoomAvatar, RoomIcon } from '../../../components/room-avatar';
import {
getEditedEvent,
getMemberAvatarMxc,
getMemberDisplayName,
getRoomAvatarUrl,
} from '../../../utils/room';
import { ScrollTopContainer } from '../../../components/scroll-top-container';
import { useInterval } from '../../../hooks/useInterval';
import {
AvatarBase,
ImageContent,
MSticker,
MessageNotDecryptedContent,
MessageUnsupportedContent,
ModernLayout,
RedactedContent,
Reply,
Time,
Username,
UsernameBold,
} from '../../../components/message';
import {
factoryRenderLinkifyWithMention,
getReactCustomHtmlParser,
LINKIFY_OPTS,
makeMentionCustomProps,
renderMatrixMention,
} from '../../../plugins/react-custom-html-parser';
import { RenderMessageContent } from '../../../components/RenderMessageContent';
import { useSetting } from '../../../state/hooks/settings';
import { Image } from '../../../components/media';
import { ImageViewer } from '../../../components/image-viewer';
import { GetContentCallback, MessageEvent, StateEvent } from '../../../../types/matrix/room';
import { useMatrixEventRenderer } from '../../../hooks/useMatrixEventRenderer';
import * as customHtmlCss from '../../../styles/CustomHtml.css';
import { useSettings } from '../../../state/hooks/settings';
import { useRoomNavigate } from '../../../hooks/useRoomNavigate';
import { markAsRead } from '../../../utils/notifications';
import { ContainerColor } from '../../../styles/ContainerColor.css';
import { VirtualTile } from '../../../components/virtualizer';
import { UserAvatar } from '../../../components/user-avatar';
import { EncryptedContent } from '../../../features/room/message';
import { useMentionClickHandler } from '../../../hooks/useMentionClickHandler';
import { useSpoilerClickHandler } from '../../../hooks/useSpoilerClickHandler';
import { ScreenSize, useScreenSizeContext } from '../../../hooks/useScreenSize';
import { BackRouteHandler } from '../../../components/BackRouteHandler';
import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';
import { allRoomsAtom } from '../../../state/room-list/roomList';
import { usePowerLevels } from '../../../hooks/usePowerLevels';
import { usePowerLevelTags } from '../../../hooks/usePowerLevelTags';
import {
PowerLevelsContextProvider,
usePowerLevels,
usePowerLevelsContext,
usePowerLevelsState,
} from '../../../hooks/usePowerLevels';
import {
PowerLevelTagsContextProvider,
usePowerLevelTags,
usePowerLevelTagsContext,
usePowerLevelTagsState,
} from '../../../hooks/usePowerLevelTags';
import { useTheme } from '../../../hooks/useTheme';
import { PowerIcon } from '../../../components/power';
import colorMXID from '../../../../util/colorMXID';
@ -94,10 +77,33 @@ import {
} from '../../../hooks/useMemberPowerTag';
import { useRoomCreatorsTag } from '../../../hooks/useRoomCreatorsTag';
import { useRoomCreators } from '../../../hooks/useRoomCreators';
import { useClient } from '../../../hooks/useClient';
import {
RoomMembersAPIProvider,
useRoomMembers,
useRoomMembersAPI,
} from '../../../hooks/useRoomMembers';
import { useMessageParserOptions } from '../../../hooks/useMessageParserOptions';
import { useRoomHeroes } from '../../../hooks/storage/useRoomHeros';
import { GetRoomNameCallback, useGetRoomName } from '../../../hooks/storage/useGetRoomName';
import { GetRoomAvatarCallback, useGetRoomAvatar } from '../../../hooks/storage/useGetRoomAvatar';
import { mxcToHttp } from '../../../../client/mxcToHttp';
import { useRoomUnread } from '../../../state/hooks/unread';
import { unreadRoomsAtom } from '../../../state/room/roomToUnread';
import { ErrorCode } from '../../../cs-errorcode';
import {
useObservableValue,
useObservableValueOptionally,
} from '../../../hooks/useObservableValue';
import { Timeline } from '../../../../client/room';
import { MessageContent } from '../../../components/MessageContent';
import { getLatestEditEvent } from '../../../utils/room';
import { RenderMessageContent } from '../../../components/RenderMessageContent';
import { rContent } from '../../../../client/reader';
type RoomNotificationsGroup = {
roomId: string;
notifications: INotification[];
notifications: NotificationChunk[];
};
type NotificationTimeline = {
nextToken?: string;
@ -107,7 +113,7 @@ type LoadTimeline = (from?: string) => Promise<void>;
type SilentReloadTimeline = () => Promise<void>;
const groupNotifications = (
notifications: INotification[],
notifications: NotificationChunk[],
allowRooms: Set<string>
): RoomNotificationsGroup[] => {
const groups: RoomNotificationsGroup[] = [];
@ -132,7 +138,7 @@ const useNotificationTimeline = (
paginationLimit: number,
onlyHighlight?: boolean
): [NotificationTimeline, LoadTimeline, SilentReloadTimeline] => {
const mx = useMatrixClient();
const ct = useClient();
const allRooms = useAtomValue(allRoomsAtom);
const allJoinedRooms = useMemo(() => new Set(allRooms), [allRooms]);
@ -141,15 +147,21 @@ const useNotificationTimeline = (
});
const fetchNotifications = useCallback(
(from?: string, limit?: number, only?: 'highlight') => {
const queryParams = { from, limit, only };
return mx.http.authedRequest<INotificationsResponse>(
Method.Get,
'/notifications',
queryParams
async (from?: string, limit?: number, only?: 'highlight') => {
const resp = await ct.requestAuthJSON(
getNotifications({ searchParams: { from, limit, only } })
);
const [err, result] = await parseMatrixResp<GetNotificationsOK>(resp);
if (err) {
if (err.data.errcode === ErrorCode.M_UNRECOGNIZED) {
throw new Error('This feature is not supported by your homeserver!');
}
throw new Error(err.data.error ?? err.data.errcode);
}
return result;
},
[mx]
[ct]
);
const loadTimeline: LoadTimeline = useCallback(
@ -178,8 +190,12 @@ const useNotificationTimeline = (
);
/**
* Reload timeline silently i.e without setting to default
* before fetching notifications from start
* Reload timeline silently i.e without resetting to default.
* Fetches the first page and diffs it against current state:
* - if nothing new, keeps the existing state reference untouched
* - if there are new notifications, only prepends them to the top
* (merging into the first existing group when it's the same room)
* instead of re-fetching/rebuilding the whole timeline.
*/
const silentReloadTimeline: SilentReloadTimeline = useCallback(async () => {
const data = await fetchNotifications(
@ -187,221 +203,256 @@ const useNotificationTimeline = (
paginationLimit,
onlyHighlight ? 'highlight' : undefined
);
const groups = groupNotifications(data.notifications, allJoinedRooms);
setNotificationTimeline({
nextToken: data.next_token,
groups,
setNotificationTimeline((currentTimeline) => {
// Nothing loaded yet, just seed the state normally.
if (currentTimeline.groups.length === 0) {
return {
nextToken: data.next_token,
groups: groupNotifications(data.notifications, allJoinedRooms),
};
}
const existingIds = new Set<string>();
currentTimeline.groups.forEach((group) => {
group.notifications.forEach((n) => existingIds.add(n.event.event_id));
});
const newNotifications = data.notifications.filter((n) => !existingIds.has(n.event.event_id));
// Nothing changed — return the same reference so consumers relying
// on identity (e.g. memoized lists) don't needlessly re-render.
if (newNotifications.length === 0) {
return currentTimeline;
}
const newGroups = groupNotifications(newNotifications, allJoinedRooms);
const lastNewGroup = newGroups[newGroups.length - 1];
const firstExistingGroup = currentTimeline.groups[0];
// Merge into the existing first group if it's the same room, so we
// don't end up with two adjacent groups for the same room.
if (lastNewGroup && firstExistingGroup?.roomId === lastNewGroup.roomId) {
const mergedGroup: RoomNotificationsGroup = {
roomId: firstExistingGroup.roomId,
notifications: [...lastNewGroup.notifications, ...firstExistingGroup.notifications],
};
return {
nextToken: currentTimeline.nextToken,
groups: [...newGroups.slice(0, -1), mergedGroup, ...currentTimeline.groups.slice(1)],
};
}
return {
nextToken: currentTimeline.nextToken,
groups: [...newGroups, ...currentTimeline.groups],
};
});
}, [paginationLimit, onlyHighlight, fetchNotifications, allJoinedRooms]);
return [notificationTimeline, loadTimeline, silentReloadTimeline];
};
type RoomNotificationsGroupProps = {
room: Room;
notifications: INotification[];
type RoomNotificationsProps = {
roomId: string;
timeline?: Timeline;
notifications: NotificationChunk[];
mediaAutoLoad?: boolean;
urlPreview?: boolean;
hideActivity: boolean;
onOpen: (roomId: string, eventId: string) => void;
legacyUsernameColor?: boolean;
hour24Clock: boolean;
dateFormatString: string;
getRoomName: GetRoomNameCallback;
onOpen: (roomId: string, eventId: string) => void;
};
function RoomNotificationsGroupComp({
room,
function RoomNotifications({
roomId,
timeline,
notifications,
mediaAutoLoad,
urlPreview,
hideActivity,
onOpen,
legacyUsernameColor,
hour24Clock,
dateFormatString,
}: RoomNotificationsGroupProps) {
const mx = useMatrixClient();
getRoomName,
onOpen,
}: RoomNotificationsProps) {
const ct = useClient();
const membersAPI = useRoomMembersAPI();
const useAuthentication = useMediaAuthentication();
// const unread = useRoomUnread(room.roomId, roomToUnreadAtom);
const powerLevels = usePowerLevels(room);
const creators = useRoomCreators(room);
const redacted = useObservableValueOptionally(timeline?.relations.redacted);
const edits = useObservableValueOptionally(timeline?.relations.edits.eventToRelations);
const creators = useRoomCreators(roomId);
const powerLevels = usePowerLevelsContext();
const getMemberPowerTag = useGetMemberPowerTag(creators, powerLevels);
const creatorsTag = useRoomCreatorsTag();
const powerLevelTags = usePowerLevelTags(room, powerLevels);
const getMemberPowerTag = useGetMemberPowerTag(room, creators, powerLevels);
const powerLevelTags = usePowerLevelTagsContext();
const theme = useTheme();
const accessibleTagColors = useAccessiblePowerTagColors(theme.kind, creatorsTag, powerLevelTags);
const mentionClickHandler = useMentionClickHandler(room.roomId);
const spoilerClickHandler = useSpoilerClickHandler();
const linkifyOpts = useMemo<LinkifyOpts>(
() => ({
...LINKIFY_OPTS,
render: factoryRenderLinkifyWithMention((href) =>
renderMatrixMention(mx, room.roomId, href, makeMentionCustomProps(mentionClickHandler))
),
}),
[mx, room, mentionClickHandler]
);
const htmlReactParserOptions = useMemo<HTMLReactParserOptions>(
() =>
getReactCustomHtmlParser(mx, room.roomId, {
linkifyOpts,
useAuthentication,
handleSpoilerClick: spoilerClickHandler,
handleMentionClick: mentionClickHandler,
}),
[mx, room, linkifyOpts, mentionClickHandler, spoilerClickHandler, useAuthentication]
);
const renderMatrixEvent = useMatrixEventRenderer<[IRoomEvent, string, GetContentCallback]>(
{
[MessageEvent.RoomMessage]: (event, displayName, getContent) => {
if (event.unsigned?.redacted_because) {
return <RedactedContent reason={event.unsigned?.redacted_because.content.reason} />;
}
return (
<RenderMessageContent
displayName={displayName}
msgType={event.content.msgtype ?? ''}
ts={event.origin_server_ts}
getContent={getContent}
mediaAutoLoad={mediaAutoLoad}
urlPreview={urlPreview}
htmlReactParserOptions={htmlReactParserOptions}
linkifyOpts={linkifyOpts}
outlineAttachment
/>
);
},
[MessageEvent.RoomMessageEncrypted]: (evt, displayName) => {
const evtTimeline = room.getTimelineForEvent(evt.event_id);
const mEvent = evtTimeline?.getEvents().find((e) => e.getId() === evt.event_id);
if (!mEvent || !evtTimeline) {
return (
<Box grow="Yes" direction="Column">
<Text size="T400" priority="300">
<code className={customHtmlCss.Code}>{evt.type}</code>
{' event'}
</Text>
</Box>
);
}
return (
<EncryptedContent mEvent={mEvent}>
{() => {
if (mEvent.isRedacted()) return <RedactedContent />;
if (mEvent.getType() === MessageEvent.Sticker)
return (
<MSticker
content={mEvent.getContent()}
renderImageContent={(props) => (
<ImageContent
{...props}
autoPlay={mediaAutoLoad}
renderImage={(p) => <Image {...p} loading="lazy" />}
renderViewer={(p) => <ImageViewer {...p} />}
/>
)}
/>
);
if (mEvent.getType() === MessageEvent.RoomMessage) {
const editedEvent = getEditedEvent(
evt.event_id,
mEvent,
evtTimeline.getTimelineSet()
);
const getContent = (() =>
editedEvent?.getContent()['m.new_content'] ??
mEvent.getContent()) as GetContentCallback;
return (
<RenderMessageContent
displayName={displayName}
msgType={mEvent.getContent().msgtype ?? ''}
ts={mEvent.getTs()}
edited={!!editedEvent}
getContent={getContent}
mediaAutoLoad={mediaAutoLoad}
urlPreview={urlPreview}
htmlReactParserOptions={htmlReactParserOptions}
linkifyOpts={linkifyOpts}
/>
);
}
if (mEvent.getType() === MessageEvent.RoomMessageEncrypted)
return (
<Text>
<MessageNotDecryptedContent />
</Text>
);
return (
<Text>
<MessageUnsupportedContent />
</Text>
);
}}
</EncryptedContent>
);
},
[MessageEvent.Sticker]: (event, displayName, getContent) => {
if (event.unsigned?.redacted_because) {
return <RedactedContent reason={event.unsigned?.redacted_because.content.reason} />;
}
return (
<MSticker
content={getContent()}
renderImageContent={(props) => (
<ImageContent
{...props}
autoPlay={mediaAutoLoad}
renderImage={(p) => <Image {...p} loading="lazy" />}
renderViewer={(p) => <ImageViewer {...p} />}
/>
)}
/>
);
},
[StateEvent.RoomTombstone]: (event) => {
const { content } = event;
return (
<Box grow="Yes" direction="Column">
<Text size="T400" priority="300">
Room Tombstone. {content.body}
</Text>
</Box>
);
},
},
undefined,
(event) => {
if (event.unsigned?.redacted_because) {
return <RedactedContent reason={event.unsigned?.redacted_because.content.reason} />;
}
return (
<Box grow="Yes" direction="Column">
<Text size="T400" priority="300">
<code className={customHtmlCss.Code}>{event.type}</code>
{' event'}
</Text>
</Box>
);
}
const { linkifyOpts, htmlReactParserOptions } = useMessageParserOptions(
roomId,
getRoomName,
membersAPI.getName
);
const handleOpenClick: MouseEventHandler = (evt) => {
const eventId = evt.currentTarget.getAttribute('data-event-id');
if (!eventId) return;
onOpen(room.roomId, eventId);
onOpen(roomId, eventId);
};
return (
<Box direction="Column" gap="100">
{notifications.map((notification) => {
const { event } = notification;
const displayName = membersAPI.getName(event.sender);
const senderAvatarMxc = membersAPI.getAvatar(event.sender);
const memberPowerTag = getMemberPowerTag(event.sender);
const tagColor = memberPowerTag?.color
? accessibleTagColors?.get(memberPowerTag.color)
: undefined;
const tagIconSrc = memberPowerTag?.icon
? getPowerTagIconSrc(ct, useAuthentication, memberPowerTag.icon)
: undefined;
const usernameColor = legacyUsernameColor ? colorMXID(event.sender) : tagColor;
const editEvents = edits?.get(event.event_id);
const editedEvent = editEvents && getLatestEditEvent(event.sender, editEvents, redacted);
return (
<SequenceCard
key={notification.event.event_id}
style={{ padding: `${config.space.S300} ${config.space.S400}` }}
variant="SurfaceVariant"
direction="Column"
>
<ModernLayout
before={
<AvatarBase>
<Avatar size="300">
<UserAvatar
userId={event.sender}
src={
senderAvatarMxc
? mxcToHttp(
ct.session.baseUrl,
senderAvatarMxc,
useAuthentication
).thumbnail({ width: 96, height: 96, method: 'crop' })
: undefined
}
alt={displayName}
renderFallback={() => <Icon size="200" src={Icons.User} filled />}
/>
</Avatar>
</AvatarBase>
}
>
<Box gap="300" justifyContent="SpaceBetween" alignItems="Center" grow="Yes">
<Box gap="200" alignItems="Baseline">
<Box alignItems="Center" gap="200">
<Username style={{ color: usernameColor }}>
<Text as="span" truncate>
<UsernameBold>{displayName}</UsernameBold>
</Text>
</Username>
{tagIconSrc && <PowerIcon size="100" iconSrc={tagIconSrc} />}
</Box>
<Time
ts={event.origin_server_ts}
hour24Clock={hour24Clock}
dateFormatString={dateFormatString}
/>
</Box>
<Box shrink="No" gap="200" alignItems="Center">
<Chip
data-event-id={event.event_id}
onClick={handleOpenClick}
variant="Secondary"
radii="400"
>
<Text size="T200">Open</Text>
</Chip>
</Box>
</Box>
{redacted?.has(event.event_id) ? (
<RedactedContent />
) : (
<MessageContent
event={event}
editedEvent={editedEvent}
roomId={roomId}
decryptionManager={ct.decryption}
>
{(contentEvent) => (
<RenderMessageContent
displayName={membersAPI.getName(event.sender)}
eventType={contentEvent.type}
ts={event.origin_server_ts}
edited={!!editedEvent}
content={rContent.newContent(contentEvent.content) ?? contentEvent.content}
mediaAutoLoad={mediaAutoLoad}
urlPreview={urlPreview}
htmlReactParserOptions={htmlReactParserOptions}
linkifyOpts={linkifyOpts}
/>
)}
</MessageContent>
)}
</ModernLayout>
</SequenceCard>
);
})}
</Box>
);
}
type RoomNotificationsGroupProps = {
roomId: string;
notifications: NotificationChunk[];
getRoomName: GetRoomNameCallback;
getRoomAvatar: GetRoomAvatarCallback;
hideActivity: boolean;
children: ReactNode;
};
function RoomNotificationsGroupComp({
roomId,
notifications,
getRoomName,
getRoomAvatar,
hideActivity,
children,
}: RoomNotificationsGroupProps) {
const ct = useClient();
const useAuthentication = useMediaAuthentication();
const unread = useRoomUnread(roomId, unreadRoomsAtom);
const users = useMemo(
() => Array.from(new Set(notifications.map((n) => n.event.sender))),
[notifications]
);
const avatarMxc = getRoomAvatar(roomId);
const powerLevelStateResult = usePowerLevelsState(roomId);
const powerLevels = usePowerLevels(powerLevelStateResult.value);
const powerLevelTagsStateResult = usePowerLevelTagsState(roomId);
const powerLevelTags = usePowerLevelTags(powerLevels, powerLevelTagsStateResult.value);
const membersAPI = useRoomMembers(roomId, users);
const handleMarkAsRead = () => {
markAsRead(mx, room.roomId, hideActivity);
// markAsRead(mx, room.roomId, hideActivity);
};
return (
@ -410,21 +461,29 @@ function RoomNotificationsGroupComp({
<Box gap="200" grow="Yes">
<Avatar size="200" radii="300">
<RoomAvatar
roomId={room.roomId}
src={getRoomAvatarUrl(mx, room, 96, useAuthentication)}
alt={room.name}
roomId={roomId}
src={
avatarMxc
? mxcToHttp(ct.session.baseUrl, avatarMxc, useAuthentication).thumbnail({
width: 48,
height: 48,
method: 'crop',
})
: undefined
}
alt={getRoomName(roomId)}
renderFallback={() => (
<RoomIcon
size="50"
roomType={room.getType()}
joinRule={room.getJoinRule() ?? JoinRule.Restricted}
roomType={ct.roomToType.read.get(roomId)}
joinRule={ct.roomToJoinRule.read.get(roomId)}
filled
/>
)}
/>
</Avatar>
<Text size="H4" truncate>
{room.name}
{getRoomName(roomId) ?? 'Unknown'}
</Text>
</Box>
<Box shrink="No">
@ -440,108 +499,14 @@ function RoomNotificationsGroupComp({
)}
</Box>
</Header>
<Box direction="Column" gap="100">
{notifications.map((notification) => {
const { event } = notification;
const displayName =
getMemberDisplayName(room, event.sender) ??
getMxIdLocalPart(event.sender) ??
event.sender;
const senderAvatarMxc = getMemberAvatarMxc(room, event.sender);
const getContent = (() => event.content) as GetContentCallback;
const relation = event.content['m.relates_to'];
const replyEventId = relation?.['m.in_reply_to']?.event_id;
const threadRootId =
relation?.rel_type === RelationType.Thread ? relation.event_id : undefined;
const memberPowerTag = getMemberPowerTag(event.sender);
const tagColor = memberPowerTag?.color
? accessibleTagColors?.get(memberPowerTag.color)
: undefined;
const tagIconSrc = memberPowerTag?.icon
? getPowerTagIconSrc(mx, useAuthentication, memberPowerTag.icon)
: undefined;
const usernameColor = legacyUsernameColor ? colorMXID(event.sender) : tagColor;
return (
<SequenceCard
key={notification.event.event_id}
style={{ padding: config.space.S400 }}
variant="SurfaceVariant"
direction="Column"
>
<ModernLayout
before={
<AvatarBase>
<Avatar size="300">
<UserAvatar
userId={event.sender}
src={
senderAvatarMxc
? mxcUrlToHttp(
mx,
senderAvatarMxc,
useAuthentication,
48,
48,
'crop'
) ?? undefined
: undefined
}
alt={displayName}
renderFallback={() => <Icon size="200" src={Icons.User} filled />}
/>
</Avatar>
</AvatarBase>
}
>
<Box gap="300" justifyContent="SpaceBetween" alignItems="Center" grow="Yes">
<Box gap="200" alignItems="Baseline">
<Box alignItems="Center" gap="200">
<Username style={{ color: usernameColor }}>
<Text as="span" truncate>
<UsernameBold>{displayName}</UsernameBold>
</Text>
</Username>
{tagIconSrc && <PowerIcon size="100" iconSrc={tagIconSrc} />}
</Box>
<Time
ts={event.origin_server_ts}
hour24Clock={hour24Clock}
dateFormatString={dateFormatString}
/>
</Box>
<Box shrink="No" gap="200" alignItems="Center">
<Chip
data-event-id={event.event_id}
onClick={handleOpenClick}
variant="Secondary"
radii="400"
>
<Text size="T200">Open</Text>
</Chip>
</Box>
</Box>
{replyEventId && (
<Reply
room={room}
replyEventId={replyEventId}
threadRootId={threadRootId}
onClick={handleOpenClick}
getMemberPowerTag={getMemberPowerTag}
accessibleTagColors={accessibleTagColors}
legacyUsernameColor={legacyUsernameColor}
/>
)}
{renderMatrixEvent(event.type, false, event, displayName, getContent)}
</ModernLayout>
</SequenceCard>
);
})}
</Box>
<RoomMembersAPIProvider value={membersAPI}>
<PowerLevelsContextProvider value={powerLevels}>
<PowerLevelTagsContextProvider value={powerLevelTags}>
{children}
</PowerLevelTagsContextProvider>
</PowerLevelsContextProvider>
</RoomMembersAPIProvider>
</Box>
);
}
@ -559,13 +524,16 @@ const useNotificationsSearchParams = (
const DEFAULT_REFRESH_MS = 7000;
export function Notifications() {
const mx = useMatrixClient();
const [hideActivity] = useSetting('hideActivity');
const [mediaAutoLoad] = useSetting('mediaAutoLoad');
const [urlPreview] = useSetting('urlPreview');
const [legacyUsernameColor] = useSetting('legacyUsernameColor');
const [hour24Clock] = useSetting('hour24Clock');
const [dateFormatString] = useSetting('dateFormatString');
const ct = useClient();
const {
hideActivity,
mediaAutoLoad,
urlPreview,
legacyUsernameColor,
hour24Clock,
dateFormatString,
} = useSettings();
const screenSize = useScreenSizeContext();
const mDirects = useAtomValue(mDirectAtom);
@ -579,11 +547,7 @@ export function Notifications() {
const onlyHighlight = notificationsSearchParams.only === 'highlight';
const setOnlyHighlighted = (highlight: boolean) => {
if (highlight) {
setSearchParams(
new URLSearchParams({
only: 'highlight',
})
);
setSearchParams(new URLSearchParams({ only: 'highlight' }));
return;
}
setSearchParams();
@ -631,6 +595,16 @@ export function Notifications() {
}
}, [timelineState, notificationTimeline, lastVItemIndex, loadTimeline]);
const notificationRooms = useMemo(
() => Array.from(new Set(notificationTimeline.groups.map((g) => g.roomId))),
[notificationTimeline]
);
const roomToTimeline = useObservableValue(ct.roomToTimeline);
const [getRoomHeroes, roomToHeroMembersResult] = useRoomHeroes(notificationRooms);
const getRoomName = useGetRoomName(getRoomHeroes, roomToHeroMembersResult.value);
const getRoomAvatar = useGetRoomAvatar(getRoomHeroes, roomToHeroMembersResult.value);
return (
<Page>
<PageHeader balance>
@ -710,8 +684,6 @@ export function Notifications() {
{vItems.map((vItem) => {
const group = notificationTimeline.groups[vItem.index];
if (!group) return null;
const groupRoom = mx.getRoom(group.roomId);
if (!groupRoom) return null;
return (
<VirtualTile
@ -721,18 +693,25 @@ export function Notifications() {
key={vItem.index}
>
<RoomNotificationsGroupComp
room={groupRoom}
roomId={group.roomId}
notifications={group.notifications}
mediaAutoLoad={mediaAutoLoad}
urlPreview={urlPreview}
getRoomName={getRoomName}
getRoomAvatar={getRoomAvatar}
hideActivity={hideActivity}
onOpen={navigateRoom}
legacyUsernameColor={
legacyUsernameColor || mDirects.has(groupRoom.roomId)
}
hour24Clock={hour24Clock}
dateFormatString={dateFormatString}
/>
>
<RoomNotifications
roomId={group.roomId}
timeline={roomToTimeline.get(group.roomId)}
notifications={group.notifications}
mediaAutoLoad={mediaAutoLoad}
urlPreview={urlPreview}
onOpen={navigateRoom}
legacyUsernameColor={legacyUsernameColor || mDirects.has(group.roomId)}
hour24Clock={hour24Clock}
dateFormatString={dateFormatString}
getRoomName={getRoomName}
/>
</RoomNotificationsGroupComp>
</VirtualTile>
);
})}
@ -777,8 +756,11 @@ export function Notifications() {
direction="Column"
gap="200"
>
<Text size="L400">{(timelineState.error as Error).name}</Text>
<Text size="T300">{(timelineState.error as Error).message}</Text>
<Text size="L400">
{timelineState.error instanceof Error
? timelineState.error.message
: 'Unknown Error'}
</Text>
</Box>
)}
</Box>

View file

@ -463,11 +463,11 @@ export const getReactionGroups = (
export const getLatestEditEvent = (
sender: string,
events: MessageEventWithoutRoomId[],
redacted: Set<string>
redacted: Set<string> | undefined
): MessageEventWithoutRoomId | undefined => {
let event: MessageEventWithoutRoomId | undefined;
events.forEach((e) => {
if (redacted.has(e.event_id) || sender !== e.sender) return;
if (redacted?.has(e.event_id) || sender !== e.sender) return;
if (!event) {
event = e;
return;