import EventEmitter from 'events'; import appDispatcher from '../dispatcher'; import cons from './cons'; class RoomList extends EventEmitter { constructor(matrixClient) { super(); this.matrixClient = matrixClient; this.mDirects = this.getMDirects(); 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)); } 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.spaces.add(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.spaces.clear(); this.rooms.clear(); this.inviteDirects.clear(); this.inviteSpaces.clear(); this.inviteRooms.clear(); this.matrixClient.getRooms().forEach((room) => { const { roomId } = room; const tombstone = room.currentState.events.get('m.room.tombstone'); if (typeof tombstone !== 'undefined') { const repRoomId = tombstone.get('').getContent().replacement_room; const repRoomMembership = this.matrixClient.getRoom(repRoomId)?.getMyMembership(); if (repRoomMembership === 'join') return; } 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.spaces.add(roomId); else this.rooms.add(roomId); }); } _isDMInvite(room) { 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) => { const myRoom = this.matrixClient.getRoom(directId); if (this.mDirects.has(directId)) return; // Update mDirects this.mDirects.add(directId); if (myRoom === null) return; if (this._isDMInvite(myRoom)) return; if (myRoom.getMyMembership === 'join' && !this.directs.has(directId)) { this.directs.add(directId); } // Newly added room. // at this time my membership can be invite | join if (myRoom.getMyMembership() === 'join' && this.rooms.has(directId)) { // found a DM which accidentally gets added to this.rooms this.rooms.delete(directId); this.emit(cons.events.roomList.ROOMLIST_UPDATED); } }); }); this.matrixClient.on('Room.name', () => { this.emit(cons.events.roomList.ROOMLIST_UPDATED); }); this.matrixClient.on('Room.receipt', (event) => { if (event.getType() === 'm.receipt') { const evContent = event.getContent(); const userId = Object.keys(evContent[Object.keys(evContent)[0]]['m.read'])[0]; if (userId !== this.matrixClient.getUserId()) return; this.emit(cons.events.roomList.ROOMLIST_UPDATED); } }); this.matrixClient.on('RoomState.events', (event) => { if (event.getType() !== 'm.room.join_rules') return; this.emit(cons.events.roomList.ROOMLIST_UPDATED); }); this.matrixClient.on('Room.myMembership', (room, membership, prevMembership) => { // room => prevMembership = null | invite | join | leave | kick | ban | unban // room => membership = invite | join | leave | kick | ban | unban const { roomId } = room; if (membership === 'unban') return; // When user_reject/sender_undo room invite 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); } // When user get invited 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; } // When user join room (first time) or start DM. if ((prevMembership === null || prevMembership === 'invite') && membership === 'join') { // when user create room/DM OR accept room/dm invite from this client. // we will update this.rooms/this.directs with user action if (this.directs.has(roomId) || this.spaces.has(roomId) || this.rooms.has(roomId)) return; if (this.processingRooms.has(roomId)) { const procRoomInfo = this.processingRooms.get(roomId); if (procRoomInfo.isDM) this.directs.add(roomId); else if (room.isSpaceRoom()) this.spaces.add(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 (room.isSpaceRoom()) { this.spaces.add(roomId); this.emit(cons.events.roomList.ROOM_JOINED, roomId); this.emit(cons.events.roomList.ROOMLIST_UPDATED); return; } // below code intented to work when user create room/DM // OR accept room/dm invite from other client. // and we have to update our client. (it's ok to have 10sec delay) // create a buffer of 10sec and HOPE client.accoundData get updated // then accoundData event listener will update this.mDirects. // and we will be able to know if it's a DM. // ---------- // less likely situation: // if we don't get accountData with 10sec then: // we will temporary add it to this.rooms. // and in future when accountData get updated // accountData listener will automatically goona REMOVE it from this.rooms // and will ADD it to this.directs // and emit the cons.events.roomList.ROOMLIST_UPDATED to update the UI. setTimeout(() => { if (this.directs.has(roomId) || this.spaces.has(roomId) || this.rooms.has(roomId)) return; if (this.mDirects.has(roomId)) this.directs.add(roomId); else this.rooms.add(roomId); this.emit(cons.events.roomList.ROOM_JOINED, roomId); this.emit(cons.events.roomList.ROOMLIST_UPDATED); }, 10000); return; } // when room is a DM add/remove it from DM's and return. if (this.directs.has(roomId)) { if (membership === 'leave' || membership === 'kick' || membership === 'ban') { this.directs.delete(roomId); this.emit(cons.events.roomList.ROOM_LEAVED, roomId); } } if (this.mDirects.has(roomId)) { if (membership === 'join') { this.directs.add(roomId); this.emit(cons.events.roomList.ROOM_JOINED, roomId); } this.emit(cons.events.roomList.ROOMLIST_UPDATED); return; } // when room is not a DM add/remove it from rooms. if (membership === 'leave' || membership === 'kick' || membership === 'ban') { if (room.isSpaceRoom()) this.spaces.delete(roomId); else this.rooms.delete(roomId); this.emit(cons.events.roomList.ROOM_LEAVED, roomId); } if (membership === 'join') { if (room.isSpaceRoom()) this.spaces.add(roomId); else this.rooms.add(roomId); this.emit(cons.events.roomList.ROOM_JOINED, roomId); } this.emit(cons.events.roomList.ROOMLIST_UPDATED); }); this.matrixClient.on('Room.timeline', () => { this.emit(cons.events.roomList.ROOMLIST_UPDATED); }); } } export default RoomList;