* 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
384 lines
12 KiB
JavaScript
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;
|