Files
cinny/src/client/state/RoomList.js
Ajay Bura 0b06bed1db 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
2023-06-12 16:45:23 +05:30

384 lines
12 KiB
JavaScript

import EventEmitter from 'events';
import appDispatcher from '../dispatcher';
import cons from './cons';
function isMEventSpaceChild(mEvent) {
return mEvent.getType() === 'm.space.child' && Object.keys(mEvent.getContent()).length > 0;
}
/**
* @param {() => boolean} callback if return true wait will over else callback will be called again.
* @param {number} timeout timeout to callback
* @param {number} maxTry maximum callback try > 0. -1 means no limit
*/
async function waitFor(callback, timeout = 400, maxTry = -1) {
if (maxTry === 0) return false;
const isOver = async () => new Promise((resolve) => {
setTimeout(() => resolve(callback()), timeout);
});
if (await isOver()) return true;
return waitFor(callback, timeout, maxTry - 1);
}
class RoomList extends EventEmitter {
constructor(matrixClient) {
super();
this.matrixClient = matrixClient;
this.mDirects = this.getMDirects();
// Contains roomId to parent spaces roomId mapping of all spaces children.
// No matter if you have joined those children rooms or not.
this.roomIdToParents = new Map();
this.inviteDirects = new Set();
this.inviteSpaces = new Set();
this.inviteRooms = new Set();
this.directs = new Set();
this.spaces = new Set();
this.rooms = new Set();
this.processingRooms = new Map();
this._populateRooms();
this._listenEvents();
appDispatcher.register(this.roomActions.bind(this));
}
isOrphan(roomId) {
return !this.roomIdToParents.has(roomId);
}
getOrphanSpaces() {
return [...this.spaces].filter((roomId) => !this.roomIdToParents.has(roomId));
}
getOrphanRooms() {
return [...this.rooms].filter((roomId) => !this.roomIdToParents.has(roomId));
}
getOrphans() {
const rooms = [...this.spaces].concat([...this.rooms]);
return rooms.filter((roomId) => !this.roomIdToParents.has(roomId));
}
getSpaceChildren(roomId) {
const space = this.matrixClient.getRoom(roomId);
if (space === null) return null;
const mSpaceChild = space?.currentState.getStateEvents('m.space.child');
const children = [];
mSpaceChild.forEach((mEvent) => {
const childId = mEvent.event.state_key;
if (isMEventSpaceChild(mEvent)) children.push(childId);
});
return children;
}
getCategorizedSpaces(spaceIds) {
const categorized = new Map();
const categorizeSpace = (spaceId) => {
if (categorized.has(spaceId)) return;
const mappedChild = new Set();
categorized.set(spaceId, mappedChild);
const child = this.getSpaceChildren(spaceId);
child.forEach((childId) => {
const room = this.matrixClient.getRoom(childId);
if (room === null || room.getMyMembership() !== 'join') return;
if (room.isSpaceRoom()) categorizeSpace(childId);
else mappedChild.add(childId);
});
};
spaceIds.forEach(categorizeSpace);
return categorized;
}
addToRoomIdToParents(roomId, parentRoomId) {
if (!this.roomIdToParents.has(roomId)) {
this.roomIdToParents.set(roomId, new Set());
}
const parents = this.roomIdToParents.get(roomId);
parents.add(parentRoomId);
}
removeFromRoomIdToParents(roomId, parentRoomId) {
if (!this.roomIdToParents.has(roomId)) return;
const parents = this.roomIdToParents.get(roomId);
parents.delete(parentRoomId);
if (parents.size === 0) this.roomIdToParents.delete(roomId);
}
getAllParentSpaces(roomId) {
const allParents = new Set();
const addAllParentIds = (rId) => {
if (allParents.has(rId)) return;
allParents.add(rId);
const parents = this.roomIdToParents.get(rId);
if (parents === undefined) return;
parents.forEach((id) => addAllParentIds(id));
};
addAllParentIds(roomId);
allParents.delete(roomId);
return allParents;
}
addToSpaces(roomId) {
this.spaces.add(roomId);
const allParentSpaces = this.getAllParentSpaces(roomId);
const spaceChildren = this.getSpaceChildren(roomId);
spaceChildren?.forEach((childId) => {
if (allParentSpaces.has(childId)) return;
this.addToRoomIdToParents(childId, roomId);
});
}
deleteFromSpaces(roomId) {
this.spaces.delete(roomId);
const spaceChildren = this.getSpaceChildren(roomId);
spaceChildren?.forEach((childId) => {
this.removeFromRoomIdToParents(childId, roomId);
});
}
roomActions(action) {
const addRoom = (roomId, isDM) => {
const myRoom = this.matrixClient.getRoom(roomId);
if (myRoom === null) return false;
if (isDM) this.directs.add(roomId);
else if (myRoom.isSpaceRoom()) this.addToSpaces(roomId);
else this.rooms.add(roomId);
return true;
};
const actions = {
[cons.actions.room.JOIN]: () => {
if (addRoom(action.roomId, action.isDM)) {
setTimeout(() => {
this.emit(cons.events.roomList.ROOM_JOINED, action.roomId);
this.emit(cons.events.roomList.ROOMLIST_UPDATED);
}, 100);
} else {
this.processingRooms.set(action.roomId, {
roomId: action.roomId,
isDM: action.isDM,
task: 'JOIN',
});
}
},
[cons.actions.room.CREATE]: () => {
if (addRoom(action.roomId, action.isDM)) {
setTimeout(() => {
this.emit(cons.events.roomList.ROOM_CREATED, action.roomId);
this.emit(cons.events.roomList.ROOM_JOINED, action.roomId);
this.emit(cons.events.roomList.ROOMLIST_UPDATED);
}, 100);
} else {
this.processingRooms.set(action.roomId, {
roomId: action.roomId,
isDM: action.isDM,
task: 'CREATE',
});
}
},
};
actions[action.type]?.();
}
getMDirects() {
const mDirectsId = new Set();
const mDirect = this.matrixClient
.getAccountData('m.direct')
?.getContent();
if (typeof mDirect === 'undefined') return mDirectsId;
Object.keys(mDirect).forEach((direct) => {
mDirect[direct].forEach((directId) => mDirectsId.add(directId));
});
return mDirectsId;
}
_populateRooms() {
this.directs.clear();
this.roomIdToParents.clear();
this.spaces.clear();
this.rooms.clear();
this.inviteDirects.clear();
this.inviteSpaces.clear();
this.inviteRooms.clear();
this.matrixClient.getRooms().forEach((room) => {
const { roomId } = room;
if (room.getMyMembership() === 'invite') {
if (this._isDMInvite(room)) this.inviteDirects.add(roomId);
else if (room.isSpaceRoom()) this.inviteSpaces.add(roomId);
else this.inviteRooms.add(roomId);
return;
}
if (room.getMyMembership() !== 'join') return;
if (this.mDirects.has(roomId)) this.directs.add(roomId);
else if (room.isSpaceRoom()) this.addToSpaces(roomId);
else this.rooms.add(roomId);
});
}
_isDMInvite(room) {
if (this.mDirects.has(room.roomId)) return true;
const me = room.getMember(this.matrixClient.getUserId());
const myEventContent = me.events.member.getContent();
return myEventContent.membership === 'invite' && myEventContent.is_direct;
}
_listenEvents() {
// Update roomList when m.direct changes
this.matrixClient.on('accountData', (event) => {
if (event.getType() !== 'm.direct') return;
const latestMDirects = this.getMDirects();
latestMDirects.forEach((directId) => {
if (this.mDirects.has(directId)) return;
this.mDirects.add(directId);
const myRoom = this.matrixClient.getRoom(directId);
if (myRoom === null) return;
if (myRoom.getMyMembership() === 'join') {
this.directs.add(directId);
this.rooms.delete(directId);
this.emit(cons.events.roomList.ROOMLIST_UPDATED);
}
});
[...this.directs].forEach((directId) => {
if (latestMDirects.has(directId)) return;
this.mDirects.delete(directId);
const myRoom = this.matrixClient.getRoom(directId);
if (myRoom === null) return;
if (myRoom.getMyMembership() === 'join') {
this.directs.delete(directId);
this.rooms.add(directId);
this.emit(cons.events.roomList.ROOMLIST_UPDATED);
}
});
});
this.matrixClient.on('Room.name', (room) => {
this.emit(cons.events.roomList.ROOMLIST_UPDATED);
this.emit(cons.events.roomList.ROOM_PROFILE_UPDATED, room.roomId);
});
this.matrixClient.on('RoomState.events', (mEvent, state) => {
if (mEvent.getType() === 'm.space.child') {
const roomId = mEvent.event.room_id;
const childId = mEvent.event.state_key;
if (isMEventSpaceChild(mEvent)) {
const allParentSpaces = this.getAllParentSpaces(roomId);
// only add if it doesn't make a cycle
if (!allParentSpaces.has(childId)) {
this.addToRoomIdToParents(childId, roomId);
}
} else {
this.removeFromRoomIdToParents(childId, roomId);
}
this.emit(cons.events.roomList.ROOMLIST_UPDATED);
return;
}
if (mEvent.getType() === 'm.room.join_rules') {
this.emit(cons.events.roomList.ROOMLIST_UPDATED);
return;
}
if (['m.room.avatar', 'm.room.topic'].includes(mEvent.getType())) {
if (mEvent.getType() === 'm.room.avatar') {
this.emit(cons.events.roomList.ROOMLIST_UPDATED);
}
this.emit(cons.events.roomList.ROOM_PROFILE_UPDATED, state.roomId);
}
});
this.matrixClient.on('Room.myMembership', async (room, membership, prevMembership) => {
// room => prevMembership = null | invite | join | leave | kick | ban | unban
// room => membership = invite | join | leave | kick | ban | unban
const { roomId } = room;
const isRoomReady = () => this.matrixClient.getRoom(roomId) !== null;
if (['join', 'invite'].includes(membership) && isRoomReady() === false) {
if (await waitFor(isRoomReady, 200, 100) === false) return;
}
if (membership === 'unban') return;
if (membership === 'invite') {
if (this._isDMInvite(room)) this.inviteDirects.add(roomId);
else if (room.isSpaceRoom()) this.inviteSpaces.add(roomId);
else this.inviteRooms.add(roomId);
this.emit(cons.events.roomList.INVITELIST_UPDATED, roomId);
return;
}
if (prevMembership === 'invite') {
if (this.inviteDirects.has(roomId)) this.inviteDirects.delete(roomId);
else if (this.inviteSpaces.has(roomId)) this.inviteSpaces.delete(roomId);
else this.inviteRooms.delete(roomId);
this.emit(cons.events.roomList.INVITELIST_UPDATED, roomId);
}
if (['leave', 'kick', 'ban'].includes(membership)) {
if (this.directs.has(roomId)) this.directs.delete(roomId);
else if (this.spaces.has(roomId)) this.deleteFromSpaces(roomId);
else this.rooms.delete(roomId);
this.emit(cons.events.roomList.ROOM_LEAVED, roomId);
this.emit(cons.events.roomList.ROOMLIST_UPDATED);
return;
}
// when user create room/DM OR accept room/dm invite from this client.
// we will update this.rooms/this.directs with user action
if (membership === 'join' && this.processingRooms.has(roomId)) {
const procRoomInfo = this.processingRooms.get(roomId);
if (procRoomInfo.isDM) this.directs.add(roomId);
else if (room.isSpaceRoom()) this.addToSpaces(roomId);
else this.rooms.add(roomId);
if (procRoomInfo.task === 'CREATE') this.emit(cons.events.roomList.ROOM_CREATED, roomId);
this.emit(cons.events.roomList.ROOM_JOINED, roomId);
this.emit(cons.events.roomList.ROOMLIST_UPDATED);
this.processingRooms.delete(roomId);
return;
}
if (this.mDirects.has(roomId) && membership === 'join') {
this.directs.add(roomId);
this.emit(cons.events.roomList.ROOM_JOINED, roomId);
this.emit(cons.events.roomList.ROOMLIST_UPDATED);
return;
}
if (membership === 'join') {
if (room.isSpaceRoom()) this.addToSpaces(roomId);
else this.rooms.add(roomId);
this.emit(cons.events.roomList.ROOM_JOINED, roomId);
this.emit(cons.events.roomList.ROOMLIST_UPDATED);
}
});
}
}
export default RoomList;