mirror of
https://github.com/cinnyapp/cinny.git
synced 2025-11-05 06:50:28 +03:00
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:
parent
2055d7a07f
commit
0b06bed1db
128 changed files with 8799 additions and 409 deletions
265
src/app/utils/room.ts
Normal file
265
src/app/utils/room.ts
Normal 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>`;
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue