Refactor state & Custom editor (#1190)

* Fix eslint

* Enable ts strict mode

* install folds, jotai & immer

* Enable immer map/set

* change cross-signing alert anim to 30 iteration

* Add function to access matrix client

* Add new types

* Add disposable util

* Add room utils

* Add mDirect list atom

* Add invite list atom

* add room list atom

* add utils for jotai atoms

* Add room id to parents atom

* Add mute list atom

* Add room to unread atom

* Use hook to bind atoms with sdk

* Add settings atom

* Add settings hook

* Extract set settings hook

* Add Sidebar components

* WIP

* Add bind atoms hook

* Fix init muted room list atom

* add navigation atoms

* Add custom editor

* Fix hotkeys

* Update folds

* Add editor output function

* Add matrix client context

* Add tooltip to editor toolbar items

* WIP - Add editor to room input

* Refocus editor on toolbar item click

* Add Mentions - WIP

* update folds

* update mention focus outline

* rename emoji element type

* Add auto complete menu

* add autocomplete query functions

* add index file for editor

* fix bug in getPrevWord function

* Show room mention autocomplete

* Add async search function

* add use async search hook

* use async search in room mention autocomplete

* remove folds prefer font for now

* allow number array in async search

* reset search with empty query

* Autocomplete unknown room mention

* Autocomplete first room mention on tab

* fix roomAliasFromQueryText

* change mention color to primary

* add isAlive hook

* add getMxIdLocalPart to mx utils

* fix getRoomAvatarUrl size

* fix types

* add room members hook

* fix bug in room mention

* add user mention autocomplete

* Fix async search giving prev result after no match

* update folds

* add twemoji font

* add use state provider hook

* add prevent scroll with arrow key util

* add ts to custom-emoji and emoji files

* add types

* add hook for emoji group labels

* add hook for emoji group icons

* add emoji board with basic emoji

* add emojiboard in room input

* select multiple emoji with shift press

* display custom emoji in emojiboard

* Add emoji preview

* focus element on hover

* update folds

* position emojiboard properly

* convert recent-emoji.js to ts

* add use recent emoji hook

* add io.element.recent_emoji to account data evt

* Render recent emoji in emoji board

* show custom emoji from parent spaces

* show room emoji

* improve emoji sidebar

* update folds

* fix pack avatar and name fallback in emoji board

* add stickers to emoji board

* fix bug in emoji preview

* Add sticker icon in room input

* add debounce hook

* add search in emoji board

* Optimize emoji board

* fix emoji board sidebar divider

* sync emojiboard sidebar with scroll & update ui

* Add use throttle hook

* support custom emoji in editor

* remove duplicate emoji selection function

* fix emoji and mention spacing

* add emoticon autocomplete in editor

* fix string

* makes emoji size relative to font size in editor

* add option to render link element

* add spoiler in editor

* fix sticker in emoji board search using wrong type

* render custom placeholder

* update hotkey for block quote and block code

* add terminate search function in async search

* add getImageInfo to matrix utils

* send stickers

* add resize observer hook

* move emoji board component hooks in hooks dir

* prevent editor expand hides room timeline

* send typing notifications

* improve emoji style and performance

* fix imports

* add on paste param to editor

* add selectFile utils

* add file picker hook

* add file paste handler hook

* add file drop handler

* update folds

* Add file upload card

* add bytes to size util

* add blurHash util

* add await to js lib

* add browser-encrypt-attachment types

* add list atom

* convert mimetype file to ts

* add matrix types

* add matrix file util

* add file related dom utils

* add common utils

* add upload atom

* add room input draft atom

* add upload card renderer component

* add upload board component

* add support for file upload in editor

* send files with message / enter

* fix circular deps

* store editor toolbar state in local store

* move msg content util to separate file

* store msg draft on room switch

* fix following member not updating on msg sent

* add theme for folds component

* fix system default theme

* Add reply support in editor

* prevent initMatrix to init multiple time

* add state event hooks

* add async callback hook

* Show tombstone info for tombstone room

* fix room tombstone component border

* add power level hook

* Add room input placeholder component

* Show input placeholder for muted member
This commit is contained in:
Ajay Bura 2023-06-12 21:15:23 +10:00 committed by GitHub
parent 2055d7a07f
commit 0b06bed1db
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
128 changed files with 8799 additions and 409 deletions

265
src/app/utils/room.ts Normal file
View file

@ -0,0 +1,265 @@
import { IconName, IconSrc } from 'folds';
import {
IPushRule,
IPushRules,
JoinRule,
MatrixClient,
MatrixEvent,
NotificationCountType,
Room,
} from 'matrix-js-sdk';
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>`;
};