import { IconName, IconSrc } from 'folds'; import { EventTimeline, EventTimelineSet, EventType, IMentions, IPowerLevelsContent, IPushRule, IPushRules, JoinRule, MatrixClient, MatrixEvent, MsgType, NotificationCountType, RelationType, Room, RoomMember, } from 'matrix-js-sdk'; import { CryptoBackend } from 'matrix-js-sdk/lib/common-crypto/CryptoBackend'; import { AccountDataEvent } from '../../types/matrix/accountData'; import { Membership, MessageEvent, NotificationType, RoomToParents, RoomType, StateEvent, UnreadInfo, } from '../../types/matrix/room'; export const getStateEvent = ( room: Room, eventType: StateEvent, stateKey = '' ): MatrixEvent | undefined => room.getLiveTimeline().getState(EventTimeline.FORWARDS)?.getStateEvents(eventType, stateKey) ?? undefined; export const getStateEvents = (room: Room, eventType: StateEvent): MatrixEvent[] => room.getLiveTimeline().getState(EventTimeline.FORWARDS)?.getStateEvents(eventType) ?? []; export const getAccountData = ( mx: MatrixClient, eventType: AccountDataEvent ): MatrixEvent | undefined => mx.getAccountData(eventType); export const getMDirects = (mDirectEvent: MatrixEvent): Set => { const roomIds = new Set(); 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 true; return event.getContent().type !== RoomType.Space; }; 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 && Array.isArray(mEvent.getContent<{ via: string[] }>().via) ); } export const getAllParents = (roomToParents: RoomToParents, roomId: string): Set => { const allParents = new Set(); 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((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(); 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 getOrphanParents = (roomToParents: RoomToParents, roomId: string): string[] => { const parents = getAllParents(roomToParents, roomId); const orphanParents = Array.from(parents).filter( (parentRoomId) => !roomToParents.has(parentRoomId) ); return orphanParents; }; 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(EventType.PushRules)?.getContent() ?.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; }; const NOTIFICATION_EVENT_TYPES = [ 'm.room.create', 'm.room.message', 'm.room.encrypted', 'm.room.member', 'm.sticker', ]; export const isNotificationEvent = (mEvent: MatrixEvent) => { const eType = mEvent.getType(); if (!NOTIFICATION_EVENT_TYPES.includes(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 roomHaveNotification = (room: Room): boolean => { const total = room.getUnreadNotificationCount(NotificationCountType.Total); const highlight = room.getUnreadNotificationCount(NotificationCountType.Highlight); return total > 0 || highlight > 0; }; 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((unread, room) => { if (room.isSpaceRoom()) return unread; if (room.getMyMembership() !== 'join') return unread; if (getNotificationType(mx, room.roomId) === NotificationType.Mute) return unread; if (roomHaveNotification(room) || roomHaveUnread(mx, room)) { unread.push(getUnreadInfo(room)); } return unread; }, []); return unreadInfos; }; export const joinRuleToIconSrc = ( icons: Record, 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, size: 32 | 96 = 32, useAuthentication = false ): string | undefined => { const mxcUrl = room.getMxcAvatarUrl(); return mxcUrl ? mx.mxcUrlToHttp(mxcUrl, size, size, 'crop', undefined, false, useAuthentication) ?? undefined : undefined; }; export const getDirectRoomAvatarUrl = ( mx: MatrixClient, room: Room, size: 32 | 96 = 32, useAuthentication = false ): string | undefined => { const mxcUrl = room.getAvatarFallbackMember()?.getMxcAvatarUrl(); if (!mxcUrl) { return getRoomAvatarUrl(mx, room, size, useAuthentication); } return ( mx.mxcUrlToHttp(mxcUrl, size, size, 'crop', undefined, false, useAuthentication) ?? undefined ); }; export const trimReplyFromBody = (body: string): string => { const match = body.match(/^> <.+?> .+\n(>.*\n)*?\n/m); if (!match) return body; return body.slice(match[0].length); }; export const trimReplyFromFormattedBody = (formattedBody: string): string => { const suffix = ''; const i = formattedBody.lastIndexOf(suffix); if (i < 0) { return formattedBody; } return formattedBody.slice(i + suffix.length); }; 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 = `In reply to`; const userLink = `${userId}`; return `
${replyToLink}${userLink}
${formattedBody}
`; }; 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 getMemberSearchStr = ( member: RoomMember, query: string, mxIdToName: (mxId: string) => string ): string[] => [ member.rawDisplayName === member.userId ? mxIdToName(member.userId) : member.rawDisplayName, query.startsWith('@') || query.indexOf(':') > -1 ? member.userId : mxIdToName(member.userId), ]; export const getMemberAvatarMxc = (room: Room, userId: string): string | undefined => { const member = room.getMember(userId); return member?.getMxcAvatarUrl(); }; export const isMembershipChanged = (mEvent: MatrixEvent): boolean => mEvent.getContent().membership !== mEvent.getPrevContent().membership || mEvent.getContent().reason !== mEvent.getPrevContent().reason; 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, }); export const getEventReactions = (timelineSet: EventTimelineSet, eventId: string) => timelineSet.relations.getChildEventsForEvent( eventId, RelationType.Annotation, EventType.Reaction ); export const getEventEdits = (timelineSet: EventTimelineSet, eventId: string, eventType: string) => timelineSet.relations.getChildEventsForEvent(eventId, RelationType.Replace, eventType); export const getLatestEdit = ( targetEvent: MatrixEvent, editEvents: MatrixEvent[] ): MatrixEvent | undefined => { const eventByTargetSender = (rEvent: MatrixEvent) => rEvent.getSender() === targetEvent.getSender(); return editEvents.sort((m1, m2) => m2.getTs() - m1.getTs()).find(eventByTargetSender); }; export const getEditedEvent = ( mEventId: string, mEvent: MatrixEvent, timelineSet: EventTimelineSet ): MatrixEvent | undefined => { const edits = getEventEdits(timelineSet, mEventId, mEvent.getType()); return edits && getLatestEdit(mEvent, edits.getRelations()); }; export const canEditEvent = (mx: MatrixClient, mEvent: MatrixEvent) => { const content = mEvent.getContent(); const relationType = content['m.relates_to']?.rel_type; return ( mEvent.getSender() === mx.getUserId() && (!relationType || relationType === RelationType.Thread) && mEvent.getType() === MessageEvent.RoomMessage && (content.msgtype === MsgType.Text || content.msgtype === MsgType.Emote || content.msgtype === MsgType.Notice) ); }; export const getLatestEditableEvt = ( timeline: EventTimeline, canEdit: (mEvent: MatrixEvent) => boolean ): MatrixEvent | undefined => { const events = timeline.getEvents(); for (let i = events.length - 1; i >= 0; i -= 1) { const evt = events[i]; if (canEdit(evt)) return evt; } return undefined; }; export const reactionOrEditEvent = (mEvent: MatrixEvent) => mEvent.getRelation()?.rel_type === RelationType.Annotation || mEvent.getRelation()?.rel_type === RelationType.Replace; export const getMentionContent = (userIds: string[], room: boolean): IMentions => { const mMentions: IMentions = {}; if (userIds.length > 0) { mMentions.user_ids = userIds; } if (room) { mMentions.room = true; } return mMentions; }; export const getCommonRooms = ( mx: MatrixClient, rooms: string[], otherUserId: string ): string[] => { const commonRooms: string[] = []; rooms.forEach((roomId) => { const room = mx.getRoom(roomId); if (!room || room.getMyMembership() !== Membership.Join) return; const common = room.hasMembershipState(otherUserId, Membership.Join); if (common) { commonRooms.push(roomId); } }); return commonRooms; }; export const bannedInRooms = (mx: MatrixClient, rooms: string[], otherUserId: string): boolean => rooms.some((roomId) => { const room = mx.getRoom(roomId); if (!room || room.getMyMembership() !== Membership.Join) return false; const banned = room.hasMembershipState(otherUserId, Membership.Ban); return banned; }); export const guessPerfectParent = ( mx: MatrixClient, roomId: string, parents: string[] ): string | undefined => { if (parents.length === 1) { return parents[0]; } const getSpecialUsers = (rId: string): string[] => { const r = mx.getRoom(rId); const powerLevels = r && getStateEvent(r, StateEvent.RoomPowerLevels)?.getContent(); const { users_default: usersDefault, users } = powerLevels ?? {}; if (typeof users !== 'object') return []; const defaultPower = typeof usersDefault === 'number' ? usersDefault : 0; return Object.keys(users).filter((userId) => users[userId] > defaultPower); }; let perfectParent: string | undefined; let score = 0; const roomSpecialUsers = getSpecialUsers(roomId); parents.forEach((parentId) => { const parentSpecialUsers = getSpecialUsers(parentId); const matchedUsersCount = parentSpecialUsers.filter((userId) => roomSpecialUsers.includes(userId) ).length; if (matchedUsersCount > score) { score = matchedUsersCount; perfectParent = parentId; } }); return perfectParent; };