* fix intersection & resize observer * add binary search util * add scroll info util * add virtual paginator hook - WIP * render timeline using paginator hook * add continuous pagination to fill timeline * add doc comments in virtual paginator hook * add scroll to element func in virtual paginator * extract timeline pagination login into hook * add sliding name for timeline messages - testing * scroll with live event * change message rending style * make message timestamp smaller * remove unused imports * add random number between util * add compact message component * add sanitize html types * fix sending alias in room mention * get room member display name util * add get room with canonical alias util * add sanitize html util * render custom html with new styles * fix linkifying link text * add reaction component * display message reactions in timeline * Change mention color * show edited message * add event sent by function factory * add functions to get emoji shortcode * add component for reaction msg * add tooltip for who has reacted * add message layouts & placeholder * fix reaction size * fix dark theme colors * add code highlight with prismjs * add options to configure spacing in msgs * render message reply * fix trim reply from body regex * fix crash when loading reply * fix reply hover style * decrypt event on timeline paginate * update custom html code style * remove console logs * fix virtual paginator scroll to func * fix virtual paginator scroll to types * add stop scroll for in view item options * fix virtual paginator out of range scroll to index * scroll to and highlight reply on click * fix reply hover style * make message avatar clickable * fix scrollTo issue in virtual paginator * load reply from fetch * import virtual paginator restore scroll * load timeline for specific event * Fix back pagination recalibration * fix reply min height * revert code block colors to secondary * stop sanitizing text in code block * add decrypt file util * add image media component * update folds * fix code block font style * add msg event type * add scale dimension util * strict msg layout type * add image renderer component * add message content fallback components * add message matrix event renderer components * render matrix event using hooks * add attachment component * add attachment content types * handle error when rendering image in timeline * add video component * render video * include blurhash in thumbnails * generate thumbnails for image message * fix reactToDom spoiler opts * add hooks for HTMLMediaElement * render audio file in timeline * add msg image content component * fix image content props * add video content component * render new image/video component in timeline * remove console.log * convert seconds to milliseconds in video info * add load thumbnail prop to video content component * add file saver types * add file header component * add file content component * render file in timeline * add media control component * render audio message in room timeline * remove moved components * safely load message reply * add media loading hook * update media control layout * add loading indication in audio component * fill audio play icon when playing audio * fix media expanding * add image viewer - WIP * add pan and zoom control to image viewer * add text based file viewer * add pdf viewer * add error handling in pdf viewer * add download btn to pdf viewer * fix file button spinner fill * fix file opens on re-render * add range slider in audio content player * render location in timeline * update folds * display membership event in timeline * make reactions toggle * render sticker messages in timeline * render room name, topic, avatar change and event * fix typos * update render state event type style * add room intro in start of timeline * add power levels context * fix wrong param passing in RoomView * fix sending typing notification in wrong room Slate onChange callback was not updating with react re-renders. * send typing status on key up * add typing indicator component * add typing member atom * display typing status in member drawer * add room view typing member component * display typing members in room view * remove old roomTimeline uses * add event readers hook * add latest event hook * display following members in room view * fetch event instead of event context for reply * fix typo in virtual paginator hook * add scroll to latest btn in timeline * change scroll to latest chip variant * destructure paginator object to improve perf * restore forward dir scroll in virtual paginator * run scroll to bottom in layout effect * display unread message indicator in timeline * make component for room timeline float * add timeline divider component * add day divider and format message time * apply message spacing to dividers * format date in room intro * send read receipt on message arrive * add event readers component * add reply, read receipt, source delete opt * bug fixes * update timeline on delete & show reason * fix empty reaction container style * show msg selection effect on msg option open * add report message options * add options to send quick reactions * add emoji board in message options * add reaction viewer * fix styles * show view reaction in msg options menu * fix spacing between two msg by same person * add option menu in other rendered event * handle m.room.encrypted messages * fix italic reply text overflow cut * handle encrypted sticker messages * remove console log * prevent message context menu with alt key pressed * make mentions clickable in messages * add options to show and hidden events in timeline * add option to disable media autoload * remove old emojiboard opener * add options to use system emoji * refresh timeline on reset * fix stuck typing member in member drawer
299 lines
9.1 KiB
TypeScript
299 lines
9.1 KiB
TypeScript
import { IconName, IconSrc } from 'folds';
|
|
|
|
import {
|
|
EventTimeline,
|
|
IPushRule,
|
|
IPushRules,
|
|
JoinRule,
|
|
MatrixClient,
|
|
MatrixEvent,
|
|
NotificationCountType,
|
|
Room,
|
|
} from 'matrix-js-sdk';
|
|
import { CryptoBackend } from 'matrix-js-sdk/lib/common-crypto/CryptoBackend';
|
|
import { AccountDataEvent } from '../../types/matrix/accountData';
|
|
import {
|
|
NotificationType,
|
|
RoomToParents,
|
|
RoomType,
|
|
StateEvent,
|
|
UnreadInfo,
|
|
} from '../../types/matrix/room';
|
|
|
|
export const getStateEvent = (
|
|
room: Room,
|
|
eventType: StateEvent,
|
|
stateKey = ''
|
|
): MatrixEvent | undefined => room.currentState.getStateEvents(eventType, stateKey) ?? undefined;
|
|
|
|
export const getStateEvents = (room: Room, eventType: StateEvent): MatrixEvent[] =>
|
|
room.currentState.getStateEvents(eventType);
|
|
|
|
export const getAccountData = (
|
|
mx: MatrixClient,
|
|
eventType: AccountDataEvent
|
|
): MatrixEvent | undefined => mx.getAccountData(eventType);
|
|
|
|
export const getMDirects = (mDirectEvent: MatrixEvent): Set<string> => {
|
|
const roomIds = new Set<string>();
|
|
const userIdToDirects = mDirectEvent?.getContent();
|
|
|
|
if (userIdToDirects === undefined) return roomIds;
|
|
|
|
Object.keys(userIdToDirects).forEach((userId) => {
|
|
const directs = userIdToDirects[userId];
|
|
if (Array.isArray(directs)) {
|
|
directs.forEach((id) => {
|
|
if (typeof id === 'string') roomIds.add(id);
|
|
});
|
|
}
|
|
});
|
|
|
|
return roomIds;
|
|
};
|
|
|
|
export const isDirectInvite = (room: Room | null, myUserId: string | null): boolean => {
|
|
if (!room || !myUserId) return false;
|
|
const me = room.getMember(myUserId);
|
|
const memberEvent = me?.events?.member;
|
|
const content = memberEvent?.getContent();
|
|
return content?.is_direct === true;
|
|
};
|
|
|
|
export const isSpace = (room: Room | null): boolean => {
|
|
if (!room) return false;
|
|
const event = getStateEvent(room, StateEvent.RoomCreate);
|
|
if (!event) return false;
|
|
return event.getContent().type === RoomType.Space;
|
|
};
|
|
|
|
export const isRoom = (room: Room | null): boolean => {
|
|
if (!room) return false;
|
|
const event = getStateEvent(room, StateEvent.RoomCreate);
|
|
if (!event) return false;
|
|
return event.getContent().type === undefined;
|
|
};
|
|
|
|
export const isUnsupportedRoom = (room: Room | null): boolean => {
|
|
if (!room) return false;
|
|
const event = getStateEvent(room, StateEvent.RoomCreate);
|
|
if (!event) return true; // Consider room unsupported if m.room.create event doesn't exist
|
|
return event.getContent().type !== undefined && event.getContent().type !== RoomType.Space;
|
|
};
|
|
|
|
export function isValidChild(mEvent: MatrixEvent): boolean {
|
|
return mEvent.getType() === StateEvent.SpaceChild && Object.keys(mEvent.getContent()).length > 0;
|
|
}
|
|
|
|
export const getAllParents = (roomToParents: RoomToParents, roomId: string): Set<string> => {
|
|
const allParents = new Set<string>();
|
|
|
|
const addAllParentIds = (rId: string) => {
|
|
if (allParents.has(rId)) return;
|
|
allParents.add(rId);
|
|
|
|
const parents = roomToParents.get(rId);
|
|
parents?.forEach((id) => addAllParentIds(id));
|
|
};
|
|
addAllParentIds(roomId);
|
|
allParents.delete(roomId);
|
|
return allParents;
|
|
};
|
|
|
|
export const getSpaceChildren = (room: Room) =>
|
|
getStateEvents(room, StateEvent.SpaceChild).reduce<string[]>((filtered, mEvent) => {
|
|
const stateKey = mEvent.getStateKey();
|
|
if (isValidChild(mEvent) && stateKey) {
|
|
filtered.push(stateKey);
|
|
}
|
|
return filtered;
|
|
}, []);
|
|
|
|
export const mapParentWithChildren = (
|
|
roomToParents: RoomToParents,
|
|
roomId: string,
|
|
children: string[]
|
|
) => {
|
|
const allParents = getAllParents(roomToParents, roomId);
|
|
children.forEach((childId) => {
|
|
if (allParents.has(childId)) {
|
|
// Space cycle detected.
|
|
return;
|
|
}
|
|
const parents = roomToParents.get(childId) ?? new Set<string>();
|
|
parents.add(roomId);
|
|
roomToParents.set(childId, parents);
|
|
});
|
|
};
|
|
|
|
export const getRoomToParents = (mx: MatrixClient): RoomToParents => {
|
|
const map: RoomToParents = new Map();
|
|
mx.getRooms()
|
|
.filter((room) => isSpace(room))
|
|
.forEach((room) => mapParentWithChildren(map, room.roomId, getSpaceChildren(room)));
|
|
|
|
return map;
|
|
};
|
|
|
|
export const isMutedRule = (rule: IPushRule) =>
|
|
rule.actions[0] === 'dont_notify' && rule.conditions?.[0]?.kind === 'event_match';
|
|
|
|
export const findMutedRule = (overrideRules: IPushRule[], roomId: string) =>
|
|
overrideRules.find((rule) => rule.rule_id === roomId && isMutedRule(rule));
|
|
|
|
export const getNotificationType = (mx: MatrixClient, roomId: string): NotificationType => {
|
|
let roomPushRule: IPushRule | undefined;
|
|
try {
|
|
roomPushRule = mx.getRoomPushRule('global', roomId);
|
|
} catch {
|
|
roomPushRule = undefined;
|
|
}
|
|
|
|
if (!roomPushRule) {
|
|
const overrideRules = mx.getAccountData('m.push_rules')?.getContent<IPushRules>()
|
|
?.global?.override;
|
|
if (!overrideRules) return NotificationType.Default;
|
|
|
|
return findMutedRule(overrideRules, roomId) ? NotificationType.Mute : NotificationType.Default;
|
|
}
|
|
|
|
if (roomPushRule.actions[0] === 'notify') return NotificationType.AllMessages;
|
|
return NotificationType.MentionsAndKeywords;
|
|
};
|
|
|
|
export const isNotificationEvent = (mEvent: MatrixEvent) => {
|
|
const eType = mEvent.getType();
|
|
if (
|
|
['m.room.create', 'm.room.message', 'm.room.encrypted', 'm.room.member', 'm.sticker'].find(
|
|
(type) => type === eType
|
|
)
|
|
)
|
|
return false;
|
|
if (eType === 'm.room.member') return false;
|
|
|
|
if (mEvent.isRedacted()) return false;
|
|
if (mEvent.getRelation()?.rel_type === 'm.replace') return false;
|
|
|
|
return true;
|
|
};
|
|
|
|
export const roomHaveUnread = (mx: MatrixClient, room: Room) => {
|
|
const userId = mx.getUserId();
|
|
if (!userId) return false;
|
|
const readUpToId = room.getEventReadUpTo(userId);
|
|
const liveEvents = room.getLiveTimeline().getEvents();
|
|
|
|
if (liveEvents[liveEvents.length - 1]?.getSender() === userId) {
|
|
return false;
|
|
}
|
|
|
|
for (let i = liveEvents.length - 1; i >= 0; i -= 1) {
|
|
const event = liveEvents[i];
|
|
if (!event) return false;
|
|
if (event.getId() === readUpToId) return false;
|
|
if (isNotificationEvent(event)) return true;
|
|
}
|
|
return true;
|
|
};
|
|
|
|
export const getUnreadInfo = (room: Room): UnreadInfo => {
|
|
const total = room.getUnreadNotificationCount(NotificationCountType.Total);
|
|
const highlight = room.getUnreadNotificationCount(NotificationCountType.Highlight);
|
|
return {
|
|
roomId: room.roomId,
|
|
highlight,
|
|
total: highlight > total ? highlight : total,
|
|
};
|
|
};
|
|
|
|
export const getUnreadInfos = (mx: MatrixClient): UnreadInfo[] => {
|
|
const unreadInfos = mx.getRooms().reduce<UnreadInfo[]>((unread, room) => {
|
|
if (room.isSpaceRoom()) return unread;
|
|
if (room.getMyMembership() !== 'join') return unread;
|
|
if (getNotificationType(mx, room.roomId) === NotificationType.Mute) return unread;
|
|
|
|
if (roomHaveUnread(mx, room)) {
|
|
unread.push(getUnreadInfo(room));
|
|
}
|
|
|
|
return unread;
|
|
}, []);
|
|
return unreadInfos;
|
|
};
|
|
|
|
export const joinRuleToIconSrc = (
|
|
icons: Record<IconName, IconSrc>,
|
|
joinRule: JoinRule,
|
|
space: boolean
|
|
): IconSrc | undefined => {
|
|
if (joinRule === JoinRule.Restricted) {
|
|
return space ? icons.Space : icons.Hash;
|
|
}
|
|
if (joinRule === JoinRule.Knock) {
|
|
return space ? icons.SpaceLock : icons.HashLock;
|
|
}
|
|
if (joinRule === JoinRule.Invite) {
|
|
return space ? icons.SpaceLock : icons.HashLock;
|
|
}
|
|
if (joinRule === JoinRule.Public) {
|
|
return space ? icons.SpaceGlobe : icons.HashGlobe;
|
|
}
|
|
return undefined;
|
|
};
|
|
|
|
export const getRoomAvatarUrl = (mx: MatrixClient, room: Room): string | undefined => {
|
|
const url =
|
|
room.getAvatarFallbackMember()?.getAvatarUrl(mx.baseUrl, 32, 32, 'crop', undefined, false) ??
|
|
undefined;
|
|
if (url) return url;
|
|
return room.getAvatarUrl(mx.baseUrl, 32, 32, 'crop') ?? undefined;
|
|
};
|
|
|
|
export const parseReplyBody = (userId: string, body: string) =>
|
|
`> <${userId}> ${body.replace(/\n/g, '\n> ')}\n\n`;
|
|
|
|
export const parseReplyFormattedBody = (
|
|
roomId: string,
|
|
userId: string,
|
|
eventId: string,
|
|
formattedBody: string
|
|
): string => {
|
|
const replyToLink = `<a href="https://matrix.to/#/${encodeURIComponent(
|
|
roomId
|
|
)}/${encodeURIComponent(eventId)}">In reply to</a>`;
|
|
const userLink = `<a href="https://matrix.to/#/${encodeURIComponent(userId)}">${userId}</a>`;
|
|
|
|
return `<mx-reply><blockquote>${replyToLink}${userLink}<br />${formattedBody}</blockquote></mx-reply>`;
|
|
};
|
|
|
|
export const getMemberDisplayName = (room: Room, userId: string): string | undefined => {
|
|
const member = room.getMember(userId);
|
|
const name = member?.rawDisplayName;
|
|
if (name === userId) return undefined;
|
|
return name;
|
|
};
|
|
|
|
export const getMemberAvatarMxc = (room: Room, userId: string): string | undefined => {
|
|
const member = room.getMember(userId);
|
|
return member?.getMxcAvatarUrl();
|
|
};
|
|
|
|
export const decryptAllTimelineEvent = async (mx: MatrixClient, timeline: EventTimeline) => {
|
|
const crypto = mx.getCrypto();
|
|
if (!crypto) return;
|
|
const decryptionPromises = timeline
|
|
.getEvents()
|
|
.filter((event) => event.isEncrypted())
|
|
.reverse()
|
|
.map((event) => event.attemptDecryption(crypto as CryptoBackend, { isRetry: true }));
|
|
await Promise.allSettled(decryptionPromises);
|
|
};
|
|
|
|
export const getReactionContent = (eventId: string, key: string, shortcode?: string) => ({
|
|
'm.relates_to': {
|
|
event_id: eventId,
|
|
key,
|
|
rel_type: 'm.annotation',
|
|
},
|
|
shortcode,
|
|
});
|