(chore) remove outdated code (#1765)

* optimize room typing members hook

* remove unused code - WIP

* remove old code from initMatrix

* remove twemojify function

* remove old sanitize util

* delete old markdown util

* delete Math atom component

* uninstall unused dependencies

* remove old notification system

* decrypt message in inbox notification center and fix refresh in background

* improve notification

---------

Co-authored-by: Krishan <33421343+kfiven@users.noreply.github.com>
This commit is contained in:
Ajay Bura 2024-07-08 16:57:10 +05:30 committed by GitHub
parent 60e022035f
commit 4f09e6bbb5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
147 changed files with 1164 additions and 15330 deletions

View file

@ -1,144 +0,0 @@
import EventEmitter from 'events';
import appDispatcher from '../dispatcher';
import cons from './cons';
class AccountData extends EventEmitter {
constructor(roomList) {
super();
this.matrixClient = roomList.matrixClient;
this.roomList = roomList;
this.spaces = roomList.spaces;
this.spaceShortcut = new Set();
this._populateSpaceShortcut();
this.categorizedSpaces = new Set();
this._populateCategorizedSpaces();
this._listenEvents();
appDispatcher.register(this.accountActions.bind(this));
}
_getAccountData() {
return this.matrixClient.getAccountData(cons.IN_CINNY_SPACES)?.getContent() || {};
}
_populateSpaceShortcut() {
this.spaceShortcut.clear();
const spacesContent = this._getAccountData();
if (spacesContent?.shortcut === undefined) return;
spacesContent.shortcut.forEach((shortcut) => {
if (this.spaces.has(shortcut)) this.spaceShortcut.add(shortcut);
});
if (spacesContent.shortcut.length !== this.spaceShortcut.size) {
// update shortcut list from account data if shortcut space doesn't exist.
// TODO: we can wait for sync to complete or else we may end up removing valid space id
this._updateSpaceShortcutData([...this.spaceShortcut]);
}
}
_updateSpaceShortcutData(shortcutList) {
const spaceContent = this._getAccountData();
spaceContent.shortcut = shortcutList;
this.matrixClient.setAccountData(cons.IN_CINNY_SPACES, spaceContent);
}
_populateCategorizedSpaces() {
this.categorizedSpaces.clear();
const spaceContent = this._getAccountData();
if (spaceContent?.categorized === undefined) return;
spaceContent.categorized.forEach((spaceId) => {
if (this.spaces.has(spaceId)) this.categorizedSpaces.add(spaceId);
});
if (spaceContent.categorized.length !== this.categorizedSpaces.size) {
// TODO: we can wait for sync to complete or else we may end up removing valid space id
this._updateCategorizedSpacesData([...this.categorizedSpaces]);
}
}
_updateCategorizedSpacesData(categorizedSpaceList) {
const spaceContent = this._getAccountData();
spaceContent.categorized = categorizedSpaceList;
this.matrixClient.setAccountData(cons.IN_CINNY_SPACES, spaceContent);
}
accountActions(action) {
const actions = {
[cons.actions.accountData.CREATE_SPACE_SHORTCUT]: () => {
const addRoomId = (id) => {
if (this.spaceShortcut.has(id)) return;
this.spaceShortcut.add(id);
};
if (Array.isArray(action.roomId)) {
action.roomId.forEach(addRoomId);
} else {
addRoomId(action.roomId);
}
this._updateSpaceShortcutData([...this.spaceShortcut]);
this.emit(cons.events.accountData.SPACE_SHORTCUT_UPDATED, action.roomId);
},
[cons.actions.accountData.DELETE_SPACE_SHORTCUT]: () => {
if (!this.spaceShortcut.has(action.roomId)) return;
this.spaceShortcut.delete(action.roomId);
this._updateSpaceShortcutData([...this.spaceShortcut]);
this.emit(cons.events.accountData.SPACE_SHORTCUT_UPDATED, action.roomId);
},
[cons.actions.accountData.MOVE_SPACE_SHORTCUTS]: () => {
const { roomId, toIndex } = action;
if (!this.spaceShortcut.has(roomId)) return;
this.spaceShortcut.delete(roomId);
const ssList = [...this.spaceShortcut];
if (toIndex >= ssList.length) ssList.push(roomId);
else ssList.splice(toIndex, 0, roomId);
this.spaceShortcut = new Set(ssList);
this._updateSpaceShortcutData(ssList);
this.emit(cons.events.accountData.SPACE_SHORTCUT_UPDATED, roomId);
},
[cons.actions.accountData.CATEGORIZE_SPACE]: () => {
if (this.categorizedSpaces.has(action.roomId)) return;
this.categorizedSpaces.add(action.roomId);
this._updateCategorizedSpacesData([...this.categorizedSpaces]);
this.emit(cons.events.accountData.CATEGORIZE_SPACE_UPDATED, action.roomId);
},
[cons.actions.accountData.UNCATEGORIZE_SPACE]: () => {
if (!this.categorizedSpaces.has(action.roomId)) return;
this.categorizedSpaces.delete(action.roomId);
this._updateCategorizedSpacesData([...this.categorizedSpaces]);
this.emit(cons.events.accountData.CATEGORIZE_SPACE_UPDATED, action.roomId);
},
};
actions[action.type]?.();
}
_listenEvents() {
this.matrixClient.on('accountData', (event) => {
if (event.getType() !== cons.IN_CINNY_SPACES) return;
this._populateSpaceShortcut();
this.emit(cons.events.accountData.SPACE_SHORTCUT_UPDATED);
this._populateCategorizedSpaces();
this.emit(cons.events.accountData.CATEGORIZE_SPACE_UPDATED);
});
this.roomList.on(cons.events.roomList.ROOM_LEAVED, (roomId) => {
if (this.spaceShortcut.has(roomId)) {
// if deleted space has shortcut remove it.
this.spaceShortcut.delete(roomId);
this._updateSpaceShortcutData([...this.spaceShortcut]);
this.emit(cons.events.accountData.SPACE_SHORTCUT_UPDATED, roomId);
}
if (this.categorizedSpaces.has(roomId)) {
this.categorizedSpaces.delete(roomId);
this._updateCategorizedSpacesData([...this.categorizedSpaces]);
this.emit(cons.events.accountData.CATEGORIZE_SPACE_UPDATED, roomId);
}
});
}
}
export default AccountData;

View file

@ -1,412 +0,0 @@
import EventEmitter from 'events';
import renderAvatar from '../../app/atoms/avatar/render';
import { cssColorMXID } from '../../util/colorMXID';
import { selectRoom } from '../action/navigation';
import cons from './cons';
import navigation from './navigation';
import settings from './settings';
import { setFavicon } from '../../util/common';
import LogoSVG from '../../../public/res/svg/cinny.svg';
import LogoUnreadSVG from '../../../public/res/svg/cinny-unread.svg';
import LogoHighlightSVG from '../../../public/res/svg/cinny-highlight.svg';
import { html, plain } from '../../util/markdown';
function isNotifEvent(mEvent) {
const eType = mEvent.getType();
if (!cons.supportEventTypes.includes(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;
}
function isMutedRule(rule) {
return rule.actions[0] === 'dont_notify' && rule.conditions[0].kind === 'event_match';
}
function findMutedRule(overrideRules, roomId) {
return overrideRules.find((rule) => (
rule.rule_id === roomId
&& isMutedRule(rule)
));
}
class Notifications extends EventEmitter {
constructor(roomList) {
super();
this.initialized = false;
this.favicon = LogoSVG;
this.matrixClient = roomList.matrixClient;
this.roomList = roomList;
this.roomIdToNoti = new Map();
this.roomIdToPopupNotis = new Map();
this.eventIdToPopupNoti = new Map();
// this._initNoti();
this._listenEvents();
// Ask for permission by default after loading
window.Notification?.requestPermission();
}
async _initNoti() {
this.initialized = false;
this.roomIdToNoti = new Map();
const addNoti = (roomId) => {
const room = this.matrixClient.getRoom(roomId);
if (this.getNotiType(room.roomId) === cons.notifs.MUTE) return;
if (this.doesRoomHaveUnread(room) === false) return;
const total = room.getUnreadNotificationCount('total');
const highlight = room.getUnreadNotificationCount('highlight');
this._setNoti(room.roomId, total ?? 0, highlight ?? 0);
};
[...this.roomList.rooms].forEach(addNoti);
[...this.roomList.directs].forEach(addNoti);
this.initialized = true;
this._updateFavicon();
}
doesRoomHaveUnread(room) {
const userId = this.matrixClient.getUserId();
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.getId() === readUpToId) return false;
if (isNotifEvent(event)) return true;
}
return true;
}
getNotiType(roomId) {
const mx = this.matrixClient;
let pushRule;
try {
pushRule = mx.getRoomPushRule('global', roomId);
} catch {
pushRule = undefined;
}
if (pushRule === undefined) {
const overrideRules = mx.getAccountData('m.push_rules')?.getContent()?.global?.override;
if (overrideRules === undefined) return cons.notifs.DEFAULT;
const isMuted = findMutedRule(overrideRules, roomId);
return isMuted ? cons.notifs.MUTE : cons.notifs.DEFAULT;
}
if (pushRule.actions[0] === 'notify') return cons.notifs.ALL_MESSAGES;
return cons.notifs.MENTIONS_AND_KEYWORDS;
}
getNoti(roomId) {
return this.roomIdToNoti.get(roomId) || { total: 0, highlight: 0, from: null };
}
getTotalNoti(roomId) {
const { total } = this.getNoti(roomId);
return total;
}
getHighlightNoti(roomId) {
const { highlight } = this.getNoti(roomId);
return highlight;
}
getFromNoti(roomId) {
const { from } = this.getNoti(roomId);
return from;
}
hasNoti(roomId) {
return this.roomIdToNoti.has(roomId);
}
deleteNoti(roomId) {
if (this.hasNoti(roomId)) {
const noti = this.getNoti(roomId);
this._deleteNoti(roomId, noti.total, noti.highlight);
}
}
async _updateFavicon() {
if (!this.initialized) return;
let unread = false;
let highlight = false;
[...this.roomIdToNoti.values()].find((noti) => {
if (!unread) {
unread = noti.total > 0 || noti.highlight > 0;
}
highlight = noti.highlight > 0;
if (unread && highlight) return true;
return false;
});
let newFavicon = LogoSVG;
if (unread && !highlight) {
newFavicon = LogoUnreadSVG;
}
if (unread && highlight) {
newFavicon = LogoHighlightSVG;
}
if (newFavicon === this.favicon) return;
this.favicon = newFavicon;
setFavicon(this.favicon);
}
_setNoti(roomId, total, highlight) {
const addNoti = (id, t, h, fromId) => {
const prevTotal = this.roomIdToNoti.get(id)?.total ?? null;
const noti = this.getNoti(id);
noti.total += t;
noti.highlight += h;
if (fromId) {
if (noti.from === null) noti.from = new Set();
noti.from.add(fromId);
}
this.roomIdToNoti.set(id, noti);
this.emit(cons.events.notifications.NOTI_CHANGED, id, noti.total, prevTotal);
};
const noti = this.getNoti(roomId);
const addT = (highlight > total ? highlight : total) - noti.total;
const addH = highlight - noti.highlight;
if (addT < 0 || addH < 0) return;
addNoti(roomId, addT, addH);
const allParentSpaces = this.roomList.getAllParentSpaces(roomId);
allParentSpaces.forEach((spaceId) => {
addNoti(spaceId, addT, addH, roomId);
});
this._updateFavicon();
}
_deleteNoti(roomId, total, highlight) {
const removeNoti = (id, t, h, fromId) => {
if (this.roomIdToNoti.has(id) === false) return;
const noti = this.getNoti(id);
const prevTotal = noti.total;
noti.total -= t;
noti.highlight -= h;
if (noti.total < 0) {
noti.total = 0;
noti.highlight = 0;
}
if (fromId && noti.from !== null) {
if (!this.hasNoti(fromId)) noti.from.delete(fromId);
}
if (noti.from === null || noti.from.size === 0) {
this.roomIdToNoti.delete(id);
this.emit(cons.events.notifications.FULL_READ, id);
this.emit(cons.events.notifications.NOTI_CHANGED, id, null, prevTotal);
} else {
this.roomIdToNoti.set(id, noti);
this.emit(cons.events.notifications.NOTI_CHANGED, id, noti.total, prevTotal);
}
};
removeNoti(roomId, total, highlight);
const allParentSpaces = this.roomList.getAllParentSpaces(roomId);
allParentSpaces.forEach((spaceId) => {
removeNoti(spaceId, total, highlight, roomId);
});
this._updateFavicon();
}
async _displayPopupNoti(mEvent, room) {
if (!settings.showNotifications && !settings.isNotificationSounds) return;
const actions = this.matrixClient.getPushActionsForEvent(mEvent);
if (!actions?.notify) return;
if (navigation.selectedRoomId === room.roomId && document.hasFocus()) return;
if (mEvent.isEncrypted()) {
await mEvent.attemptDecryption(this.matrixClient.crypto);
}
if (settings.showNotifications) {
let title;
if (!mEvent.sender || room.name === mEvent.sender.name) {
title = room.name;
} else if (mEvent.sender) {
title = `${mEvent.sender.name} (${room.name})`;
}
const iconSize = 36;
const icon = await renderAvatar({
text: mEvent.sender.name,
bgColor: cssColorMXID(mEvent.getSender()),
imageSrc: mEvent.sender?.getAvatarUrl(this.matrixClient.baseUrl, iconSize, iconSize, 'crop'),
size: iconSize,
borderRadius: 8,
scale: 8,
});
const content = mEvent.getContent();
const state = { kind: 'notification', onlyPlain: true };
let body;
if (content.format === 'org.matrix.custom.html') {
body = html(content.formatted_body, state);
} else {
body = plain(content.body, state);
}
const noti = new window.Notification(title, {
body: body.plain,
icon,
tag: mEvent.getId(),
silent: settings.isNotificationSounds,
});
if (settings.isNotificationSounds) {
noti.onshow = () => this._playNotiSound();
}
noti.onclick = () => selectRoom(room.roomId, mEvent.getId());
this.eventIdToPopupNoti.set(mEvent.getId(), noti);
if (this.roomIdToPopupNotis.has(room.roomId)) {
this.roomIdToPopupNotis.get(room.roomId).push(noti);
} else {
this.roomIdToPopupNotis.set(room.roomId, [noti]);
}
} else {
this._playNotiSound();
}
}
_deletePopupNoti(eventId) {
this.eventIdToPopupNoti.get(eventId)?.close();
this.eventIdToPopupNoti.delete(eventId);
}
_deletePopupRoomNotis(roomId) {
this.roomIdToPopupNotis.get(roomId)?.forEach((n) => {
this.eventIdToPopupNoti.delete(n.tag);
n.close();
});
this.roomIdToPopupNotis.delete(roomId);
}
_playNotiSound() {
if (!this._notiAudio) {
this._notiAudio = document.getElementById('notificationSound');
}
this._notiAudio.play();
}
_playInviteSound() {
if (!this._inviteAudio) {
this._inviteAudio = document.getElementById('inviteSound');
}
this._inviteAudio.play();
}
_listenEvents() {
this.matrixClient.on('Room.timeline', (mEvent, room) => {
if (mEvent.isRedaction()) this._deletePopupNoti(mEvent.event.redacts);
if (room.isSpaceRoom()) return;
if (!isNotifEvent(mEvent)) return;
const liveEvents = room.getLiveTimeline().getEvents();
const lastTimelineEvent = liveEvents[liveEvents.length - 1];
if (lastTimelineEvent.getId() !== mEvent.getId()) return;
if (mEvent.getSender() === this.matrixClient.getUserId()) return;
const total = room.getUnreadNotificationCount('total');
const highlight = room.getUnreadNotificationCount('highlight');
if (this.getNotiType(room.roomId) === cons.notifs.MUTE) {
this.deleteNoti(room.roomId, total ?? 0, highlight ?? 0);
return;
}
this._setNoti(room.roomId, total ?? 0, highlight ?? 0);
if (this.matrixClient.getSyncState() === 'SYNCING') {
this._displayPopupNoti(mEvent, room);
}
});
this.matrixClient.on('accountData', (mEvent, oldMEvent) => {
if (mEvent.getType() === 'm.push_rules') {
const override = mEvent?.getContent()?.global?.override;
const oldOverride = oldMEvent?.getContent()?.global?.override;
if (!override || !oldOverride) return;
const isMuteToggled = (rule, otherOverride) => {
const roomId = rule.rule_id;
const room = this.matrixClient.getRoom(roomId);
if (room === null) return false;
if (room.isSpaceRoom()) return false;
const isMuted = isMutedRule(rule);
if (!isMuted) return false;
const isOtherMuted = findMutedRule(otherOverride, roomId);
if (isOtherMuted) return false;
return true;
};
const mutedRules = override.filter((rule) => isMuteToggled(rule, oldOverride));
const unMutedRules = oldOverride.filter((rule) => isMuteToggled(rule, override));
mutedRules.forEach((rule) => {
this.emit(cons.events.notifications.MUTE_TOGGLED, rule.rule_id, true);
this.deleteNoti(rule.rule_id);
});
unMutedRules.forEach((rule) => {
this.emit(cons.events.notifications.MUTE_TOGGLED, rule.rule_id, false);
const room = this.matrixClient.getRoom(rule.rule_id);
if (!this.doesRoomHaveUnread(room)) return;
const total = room.getUnreadNotificationCount('total');
const highlight = room.getUnreadNotificationCount('highlight');
this._setNoti(room.roomId, total ?? 0, highlight ?? 0);
});
}
});
this.matrixClient.on('Room.receipt', (mEvent, room) => {
if (mEvent.getType() !== 'm.receipt' || room.isSpaceRoom()) return;
const content = mEvent.getContent();
const userId = this.matrixClient.getUserId();
Object.keys(content).forEach((eventId) => {
Object.entries(content[eventId]).forEach(([receiptType, receipt]) => {
if (!cons.supportReceiptTypes.includes(receiptType)) return;
if (Object.keys(receipt || {}).includes(userId)) {
this.deleteNoti(room.roomId);
this._deletePopupRoomNotis(room.roomId);
}
});
});
});
this.matrixClient.on('Room.myMembership', (room, membership) => {
if (membership === 'leave' && this.hasNoti(room.roomId)) {
this.deleteNoti(room.roomId);
}
if (membership === 'invite') {
this._playInviteSound();
}
});
}
}
export default Notifications;

View file

@ -1,383 +0,0 @@
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;

View file

@ -1,407 +0,0 @@
import EventEmitter from 'events';
import initMatrix from '../initMatrix';
import cons from './cons';
import settings from './settings';
function isEdited(mEvent) {
return mEvent.getRelation()?.rel_type === 'm.replace';
}
function isReaction(mEvent) {
return mEvent.getType() === 'm.reaction';
}
function hideMemberEvents(mEvent) {
const content = mEvent.getContent();
const prevContent = mEvent.getPrevContent();
const { membership } = content;
if (settings.hideMembershipEvents) {
if (membership === 'invite' || membership === 'ban' || membership === 'leave') return true;
if (prevContent.membership !== 'join') return true;
}
if (settings.hideNickAvatarEvents) {
if (membership === 'join' && prevContent.membership === 'join') return true;
}
return false;
}
function getRelateToId(mEvent) {
const relation = mEvent.getRelation();
return relation && relation.event_id;
}
function addToMap(myMap, mEvent) {
const relateToId = getRelateToId(mEvent);
if (relateToId === null) return null;
const mEventId = mEvent.getId();
if (typeof myMap.get(relateToId) === 'undefined') myMap.set(relateToId, []);
const mEvents = myMap.get(relateToId);
if (mEvents.find((ev) => ev.getId() === mEventId)) return mEvent;
mEvents.push(mEvent);
return mEvent;
}
function getFirstLinkedTimeline(timeline) {
let tm = timeline;
while (tm.prevTimeline) {
tm = tm.prevTimeline;
}
return tm;
}
function getLastLinkedTimeline(timeline) {
let tm = timeline;
while (tm.nextTimeline) {
tm = tm.nextTimeline;
}
return tm;
}
function iterateLinkedTimelines(timeline, backwards, callback) {
let tm = timeline;
while (tm) {
callback(tm);
if (backwards) tm = tm.prevTimeline;
else tm = tm.nextTimeline;
}
}
function isTimelineLinked(tm1, tm2) {
let tm = getFirstLinkedTimeline(tm1);
while (tm) {
if (tm === tm2) return true;
tm = tm.nextTimeline;
}
return false;
}
class RoomTimeline extends EventEmitter {
constructor(roomId) {
super();
// These are local timelines
this.timeline = [];
this.editedTimeline = new Map();
this.reactionTimeline = new Map();
this.typingMembers = new Set();
this.matrixClient = initMatrix.matrixClient;
this.roomId = roomId;
this.room = this.matrixClient.getRoom(roomId);
this.liveTimeline = this.room.getLiveTimeline();
this.activeTimeline = this.liveTimeline;
this.isOngoingPagination = false;
this.ongoingDecryptionCount = 0;
this.initialized = false;
setTimeout(() => this.room.loadMembersIfNeeded());
// TODO: remove below line
window.selectedRoom = this;
}
isServingLiveTimeline() {
return getLastLinkedTimeline(this.activeTimeline) === this.liveTimeline;
}
canPaginateBackward() {
if (this.timeline[0]?.getType() === 'm.room.create') return false;
const tm = getFirstLinkedTimeline(this.activeTimeline);
return tm.getPaginationToken('b') !== null;
}
canPaginateForward() {
return !this.isServingLiveTimeline();
}
isEncrypted() {
return this.matrixClient.isRoomEncrypted(this.roomId);
}
clearLocalTimelines() {
this.timeline = [];
}
addToTimeline(mEvent) {
if (mEvent.getType() === 'm.room.member' && hideMemberEvents(mEvent)) {
return;
}
if (mEvent.isRedacted()) return;
if (isReaction(mEvent)) {
addToMap(this.reactionTimeline, mEvent);
return;
}
if (!cons.supportEventTypes.includes(mEvent.getType())) return;
if (isEdited(mEvent)) {
addToMap(this.editedTimeline, mEvent);
return;
}
this.timeline.push(mEvent);
}
_populateAllLinkedEvents(timeline) {
const firstTimeline = getFirstLinkedTimeline(timeline);
iterateLinkedTimelines(firstTimeline, false, (tm) => {
tm.getEvents().forEach((mEvent) => this.addToTimeline(mEvent));
});
}
_populateTimelines() {
this.clearLocalTimelines();
this._populateAllLinkedEvents(this.activeTimeline);
}
async _reset() {
if (this.isEncrypted()) await this.decryptAllEventsOfTimeline(this.activeTimeline);
this._populateTimelines();
if (!this.initialized) {
this.initialized = true;
this._listenEvents();
}
}
async loadLiveTimeline() {
this.activeTimeline = this.liveTimeline;
await this._reset();
this.emit(cons.events.roomTimeline.READY, null);
return true;
}
async loadEventTimeline(eventId) {
// we use first unfiltered EventTimelineSet for room pagination.
const timelineSet = this.getUnfilteredTimelineSet();
try {
const eventTimeline = await this.matrixClient.getEventTimeline(timelineSet, eventId);
this.activeTimeline = eventTimeline;
await this._reset();
this.emit(cons.events.roomTimeline.READY, eventId);
return true;
} catch {
return false;
}
}
async paginateTimeline(backwards = false, limit = 30) {
if (this.initialized === false) return false;
if (this.isOngoingPagination) return false;
this.isOngoingPagination = true;
const timelineToPaginate = backwards
? getFirstLinkedTimeline(this.activeTimeline)
: getLastLinkedTimeline(this.activeTimeline);
if (timelineToPaginate.getPaginationToken(backwards ? 'b' : 'f') === null) {
this.emit(cons.events.roomTimeline.PAGINATED, backwards, 0);
this.isOngoingPagination = false;
return false;
}
const oldSize = this.timeline.length;
try {
await this.matrixClient.paginateEventTimeline(timelineToPaginate, { backwards, limit });
if (this.isEncrypted()) await this.decryptAllEventsOfTimeline(this.activeTimeline);
this._populateTimelines();
const loaded = this.timeline.length - oldSize;
this.emit(cons.events.roomTimeline.PAGINATED, backwards, loaded);
this.isOngoingPagination = false;
return true;
} catch {
this.emit(cons.events.roomTimeline.PAGINATED, backwards, 0);
this.isOngoingPagination = false;
return false;
}
}
decryptAllEventsOfTimeline(eventTimeline) {
const decryptionPromises = eventTimeline
.getEvents()
.filter((event) => event.isEncrypted() && !event.clearEvent)
.reverse()
.map((event) => event.attemptDecryption(this.matrixClient.crypto, { isRetry: true }));
return Promise.allSettled(decryptionPromises);
}
hasEventInTimeline(eventId, timeline = this.activeTimeline) {
const timelineSet = this.getUnfilteredTimelineSet();
const eventTimeline = timelineSet.getTimelineForEvent(eventId);
if (!eventTimeline) return false;
return isTimelineLinked(eventTimeline, timeline);
}
getUnfilteredTimelineSet() {
return this.room.getUnfilteredTimelineSet();
}
getEventReaders(mEvent) {
const liveEvents = this.liveTimeline.getEvents();
const readers = [];
if (!mEvent) return [];
for (let i = liveEvents.length - 1; i >= 0; i -= 1) {
readers.splice(readers.length, 0, ...this.room.getUsersReadUpTo(liveEvents[i]));
if (mEvent === liveEvents[i]) break;
}
return [...new Set(readers)];
}
getLiveReaders() {
const liveEvents = this.liveTimeline.getEvents();
const getLatestVisibleEvent = () => {
for (let i = liveEvents.length - 1; i >= 0; i -= 1) {
const mEvent = liveEvents[i];
if (mEvent.getType() === 'm.room.member' && hideMemberEvents(mEvent)) {
// eslint-disable-next-line no-continue
continue;
}
if (!mEvent.isRedacted()
&& !isReaction(mEvent)
&& !isEdited(mEvent)
&& cons.supportEventTypes.includes(mEvent.getType())
) return mEvent;
}
return liveEvents[liveEvents.length - 1];
};
return this.getEventReaders(getLatestVisibleEvent());
}
getUnreadEventIndex(readUpToEventId) {
if (!this.hasEventInTimeline(readUpToEventId)) return -1;
const readUpToEvent = this.findEventByIdInTimelineSet(readUpToEventId);
if (!readUpToEvent) return -1;
const rTs = readUpToEvent.getTs();
const tLength = this.timeline.length;
for (let i = 0; i < tLength; i += 1) {
const mEvent = this.timeline[i];
if (mEvent.getTs() > rTs) return i;
}
return -1;
}
getReadUpToEventId() {
return this.room.getEventReadUpTo(this.matrixClient.getUserId());
}
getEventIndex(eventId) {
return this.timeline.findIndex((mEvent) => mEvent.getId() === eventId);
}
findEventByIdInTimelineSet(eventId, eventTimelineSet = this.getUnfilteredTimelineSet()) {
return eventTimelineSet.findEventById(eventId);
}
findEventById(eventId) {
return this.timeline[this.getEventIndex(eventId)] ?? null;
}
deleteFromTimeline(eventId) {
const i = this.getEventIndex(eventId);
if (i === -1) return undefined;
return this.timeline.splice(i, 1)[0];
}
_listenEvents() {
this._listenRoomTimeline = (event, room, toStartOfTimeline, removed, data) => {
if (room.roomId !== this.roomId) return;
if (this.isOngoingPagination) return;
// User is currently viewing the old events probably
// no need to add new event and emit changes.
// only add reactions and edited messages
if (this.isServingLiveTimeline() === false) {
if (!isReaction(event) && !isEdited(event)) return;
}
// We only process live events here
if (!data.liveEvent) return;
if (event.isEncrypted()) {
// We will add this event after it is being decrypted.
this.ongoingDecryptionCount += 1;
return;
}
// FIXME: An unencrypted plain event can come
// while previous event is still decrypting
// and has not been added to timeline
// causing unordered timeline view.
this.addToTimeline(event);
this.emit(cons.events.roomTimeline.EVENT, event);
};
this._listenDecryptEvent = (event) => {
if (event.getRoomId() !== this.roomId) return;
if (this.isOngoingPagination) return;
// Not a live event.
// so we don't need to process it here
if (this.ongoingDecryptionCount === 0) return;
if (this.ongoingDecryptionCount > 0) {
this.ongoingDecryptionCount -= 1;
}
this.addToTimeline(event);
this.emit(cons.events.roomTimeline.EVENT, event);
};
this._listenRedaction = (mEvent, room) => {
if (room.roomId !== this.roomId) return;
const rEvent = this.deleteFromTimeline(mEvent.event.redacts);
this.editedTimeline.delete(mEvent.event.redacts);
this.reactionTimeline.delete(mEvent.event.redacts);
this.emit(cons.events.roomTimeline.EVENT_REDACTED, rEvent, mEvent);
};
this._listenTypingEvent = (event, member) => {
if (member.roomId !== this.roomId) return;
const isTyping = member.typing;
if (isTyping) this.typingMembers.add(member.userId);
else this.typingMembers.delete(member.userId);
this.emit(cons.events.roomTimeline.TYPING_MEMBERS_UPDATED, new Set([...this.typingMembers]));
};
this._listenReciptEvent = (event, room) => {
// we only process receipt for latest message here.
if (room.roomId !== this.roomId) return;
const receiptContent = event.getContent();
const mEvents = this.liveTimeline.getEvents();
const lastMEvent = mEvents[mEvents.length - 1];
const lastEventId = lastMEvent.getId();
const lastEventRecipt = receiptContent[lastEventId];
if (typeof lastEventRecipt === 'undefined') return;
if (lastEventRecipt['m.read']) {
this.emit(cons.events.roomTimeline.LIVE_RECEIPT);
}
};
this.matrixClient.on('Room.timeline', this._listenRoomTimeline);
this.matrixClient.on('Room.redaction', this._listenRedaction);
this.matrixClient.on('Event.decrypted', this._listenDecryptEvent);
this.matrixClient.on('RoomMember.typing', this._listenTypingEvent);
this.matrixClient.on('Room.receipt', this._listenReciptEvent);
}
removeInternalListeners() {
if (!this.initialized) return;
this.matrixClient.removeListener('Room.timeline', this._listenRoomTimeline);
this.matrixClient.removeListener('Room.redaction', this._listenRedaction);
this.matrixClient.removeListener('Event.decrypted', this._listenDecryptEvent);
this.matrixClient.removeListener('RoomMember.typing', this._listenTypingEvent);
this.matrixClient.removeListener('Room.receipt', this._listenReciptEvent);
}
}
export default RoomTimeline;

View file

@ -1,49 +0,0 @@
import { RoomHierarchy } from 'matrix-js-sdk/lib/room-hierarchy';
class RoomsHierarchy {
constructor(matrixClient, limit = 20, maxDepth = 1, suggestedOnly = false) {
this.matrixClient = matrixClient;
this._maxDepth = maxDepth;
this._suggestedOnly = suggestedOnly;
this._limit = limit;
this.roomIdToHierarchy = new Map();
}
getHierarchy(roomId) {
return this.roomIdToHierarchy.get(roomId);
}
removeHierarchy(roomId) {
return this.roomIdToHierarchy.delete(roomId);
}
canLoadMore(roomId) {
const roomHierarchy = this.getHierarchy(roomId);
if (!roomHierarchy) return true;
return roomHierarchy.canLoadMore;
}
async load(roomId, limit = this._limit) {
let roomHierarchy = this.getHierarchy(roomId);
if (!roomHierarchy) {
roomHierarchy = new RoomHierarchy(
{ roomId, client: this.matrixClient },
limit,
this._maxDepth,
this._suggestedOnly,
);
this.roomIdToHierarchy.set(roomId, roomHierarchy);
}
try {
await roomHierarchy.load(limit);
return roomHierarchy.rooms;
} catch {
return roomHierarchy.rooms;
}
}
}
export default RoomsHierarchy;

View file

@ -1,423 +0,0 @@
import EventEmitter from 'events';
import encrypt from 'browser-encrypt-attachment';
import { encode } from 'blurhash';
import { getShortcodeToEmoji } from '../../app/organisms/emoji-board/custom-emoji';
import { getBlobSafeMimeType } from '../../util/mimetypes';
import { sanitizeText } from '../../util/sanitize';
import cons from './cons';
import settings from './settings';
import { markdown, plain } from '../../util/markdown';
const blurhashField = 'xyz.amorgan.blurhash';
function encodeBlurhash(img) {
const canvas = document.createElement('canvas');
canvas.width = 100;
canvas.height = 100;
const context = canvas.getContext('2d');
context.drawImage(img, 0, 0, canvas.width, canvas.height);
const data = context.getImageData(0, 0, canvas.width, canvas.height);
return encode(data.data, data.width, data.height, 4, 4);
}
function loadImage(url) {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = (err) => reject(err);
img.src = url;
});
}
function loadVideo(videoFile) {
return new Promise((resolve, reject) => {
const video = document.createElement('video');
video.preload = 'metadata';
video.playsInline = true;
video.muted = true;
const reader = new FileReader();
reader.onload = (ev) => {
// Wait until we have enough data to thumbnail the first frame.
video.onloadeddata = async () => {
resolve(video);
video.pause();
};
video.onerror = (e) => {
reject(e);
};
video.src = ev.target.result;
video.load();
video.play();
};
reader.onerror = (e) => {
reject(e);
};
if (videoFile.type === 'video/quicktime') {
const quicktimeVideoFile = new File([videoFile], videoFile.name, { type: 'video/mp4' });
reader.readAsDataURL(quicktimeVideoFile);
} else {
reader.readAsDataURL(videoFile);
}
});
}
function getVideoThumbnail(video, width, height, mimeType) {
return new Promise((resolve) => {
const MAX_WIDTH = 800;
const MAX_HEIGHT = 600;
let targetWidth = width;
let targetHeight = height;
if (targetHeight > MAX_HEIGHT) {
targetWidth = Math.floor(targetWidth * (MAX_HEIGHT / targetHeight));
targetHeight = MAX_HEIGHT;
}
if (targetWidth > MAX_WIDTH) {
targetHeight = Math.floor(targetHeight * (MAX_WIDTH / targetWidth));
targetWidth = MAX_WIDTH;
}
const canvas = document.createElement('canvas');
canvas.width = targetWidth;
canvas.height = targetHeight;
const context = canvas.getContext('2d');
context.drawImage(video, 0, 0, targetWidth, targetHeight);
canvas.toBlob((thumbnail) => {
resolve({
thumbnail,
info: {
w: targetWidth,
h: targetHeight,
mimetype: thumbnail.type,
size: thumbnail.size,
},
});
}, mimeType);
});
}
class RoomsInput extends EventEmitter {
constructor(mx, roomList) {
super();
this.matrixClient = mx;
this.roomList = roomList;
this.roomIdToInput = new Map();
}
cleanEmptyEntry(roomId) {
const input = this.getInput(roomId);
const isEmpty = typeof input.attachment === 'undefined'
&& typeof input.replyTo === 'undefined'
&& (typeof input.message === 'undefined' || input.message === '');
if (isEmpty) {
this.roomIdToInput.delete(roomId);
}
}
getInput(roomId) {
return this.roomIdToInput.get(roomId) || {};
}
setMessage(roomId, message) {
const input = this.getInput(roomId);
input.message = message;
this.roomIdToInput.set(roomId, input);
if (message === '') this.cleanEmptyEntry(roomId);
}
getMessage(roomId) {
const input = this.getInput(roomId);
if (typeof input.message === 'undefined') return '';
return input.message;
}
setReplyTo(roomId, replyTo) {
const input = this.getInput(roomId);
input.replyTo = replyTo;
this.roomIdToInput.set(roomId, input);
}
getReplyTo(roomId) {
const input = this.getInput(roomId);
if (typeof input.replyTo === 'undefined') return null;
return input.replyTo;
}
cancelReplyTo(roomId) {
const input = this.getInput(roomId);
if (typeof input.replyTo === 'undefined') return;
delete input.replyTo;
this.roomIdToInput.set(roomId, input);
}
setAttachment(roomId, file) {
const input = this.getInput(roomId);
input.attachment = {
file,
};
this.roomIdToInput.set(roomId, input);
}
getAttachment(roomId) {
const input = this.getInput(roomId);
if (typeof input.attachment === 'undefined') return null;
return input.attachment.file;
}
cancelAttachment(roomId) {
const input = this.getInput(roomId);
if (typeof input.attachment === 'undefined') return;
const { uploadingPromise } = input.attachment;
if (uploadingPromise) {
this.matrixClient.cancelUpload(uploadingPromise);
delete input.attachment.uploadingPromise;
}
delete input.attachment;
delete input.isSending;
this.roomIdToInput.set(roomId, input);
this.emit(cons.events.roomsInput.ATTACHMENT_CANCELED, roomId);
}
isSending(roomId) {
return this.roomIdToInput.get(roomId)?.isSending || false;
}
getContent(roomId, options, message, reply, edit) {
const msgType = options?.msgType || 'm.text';
const autoMarkdown = options?.autoMarkdown ?? true;
const room = this.matrixClient.getRoom(roomId);
const userNames = room.currentState.userIdsToDisplayNames;
const parentIds = this.roomList.getAllParentSpaces(room.roomId);
const parentRooms = [...parentIds].map((id) => this.matrixClient.getRoom(id));
const emojis = getShortcodeToEmoji(this.matrixClient, [room, ...parentRooms]);
const output = settings.isMarkdown && autoMarkdown ? markdown : plain;
const body = output(message, { userNames, emojis });
const content = {
body: body.plain,
msgtype: msgType,
};
if (!body.onlyPlain || reply) {
content.format = 'org.matrix.custom.html';
content.formatted_body = body.html;
}
if (edit) {
content['m.new_content'] = { ...content };
content['m.relates_to'] = {
event_id: edit.getId(),
rel_type: 'm.replace',
};
const isReply = edit.getWireContent()['m.relates_to']?.['m.in_reply_to'];
if (isReply) {
content.format = 'org.matrix.custom.html';
content.formatted_body = body.html;
}
content.body = ` * ${content.body}`;
if (content.formatted_body) content.formatted_body = ` * ${content.formatted_body}`;
if (isReply) {
const eBody = edit.getContent().body;
const replyHead = eBody.substring(0, eBody.indexOf('\n\n'));
if (replyHead) content.body = `${replyHead}\n\n${content.body}`;
const eFBody = edit.getContent().formatted_body;
const fReplyHead = eFBody.substring(0, eFBody.indexOf('</mx-reply>'));
if (fReplyHead) content.formatted_body = `${fReplyHead}</mx-reply>${content.formatted_body}`;
}
}
if (reply) {
content['m.relates_to'] = {
'm.in_reply_to': {
event_id: reply.eventId,
},
};
content.body = `> <${reply.userId}> ${reply.body.replace(/\n/g, '\n> ')}\n\n${content.body}`;
const replyToLink = `<a href="https://matrix.to/#/${encodeURIComponent(roomId)}/${encodeURIComponent(reply.eventId)}">In reply to</a>`;
const userLink = `<a href="https://matrix.to/#/${encodeURIComponent(reply.userId)}">${sanitizeText(reply.userId)}</a>`;
const fallback = `<mx-reply><blockquote>${replyToLink}${userLink}<br />${reply.formattedBody || sanitizeText(reply.body)}</blockquote></mx-reply>`;
content.formatted_body = fallback + content.formatted_body;
}
return content;
}
async sendInput(roomId, options) {
const input = this.getInput(roomId);
input.isSending = true;
this.roomIdToInput.set(roomId, input);
if (input.attachment) {
await this.sendFile(roomId, input.attachment.file);
if (!this.isSending(roomId)) return;
}
if (this.getMessage(roomId).trim() !== '') {
const content = this.getContent(roomId, options, input.message, input.replyTo);
this.matrixClient.sendMessage(roomId, content);
}
if (this.isSending(roomId)) this.roomIdToInput.delete(roomId);
this.emit(cons.events.roomsInput.MESSAGE_SENT, roomId);
}
async sendSticker(roomId, data) {
const { mxc: url, body, httpUrl } = data;
const info = {};
const img = new Image();
img.src = httpUrl;
try {
const res = await fetch(httpUrl);
const blob = await res.blob();
info.w = img.width;
info.h = img.height;
info.mimetype = blob.type;
info.size = blob.size;
info.thumbnail_info = { ...info };
info.thumbnail_url = url;
} catch {
// send sticker without info
}
this.matrixClient.sendEvent(roomId, 'm.sticker', {
body,
url,
info,
});
this.emit(cons.events.roomsInput.MESSAGE_SENT, roomId);
}
async sendFile(roomId, file) {
const fileType = getBlobSafeMimeType(file.type).slice(0, file.type.indexOf('/'));
const info = {
mimetype: file.type,
size: file.size,
};
const content = { info };
let uploadData = null;
if (fileType === 'image') {
const img = await loadImage(URL.createObjectURL(file));
info.w = img.width;
info.h = img.height;
info[blurhashField] = encodeBlurhash(img);
content.msgtype = 'm.image';
content.body = file.name || 'Image';
} else if (fileType === 'video') {
content.msgtype = 'm.video';
content.body = file.name || 'Video';
try {
const video = await loadVideo(file);
info.w = video.videoWidth;
info.h = video.videoHeight;
info[blurhashField] = encodeBlurhash(video);
const thumbnailData = await getVideoThumbnail(video, video.videoWidth, video.videoHeight, 'image/jpeg');
const thumbnailUploadData = await this.uploadFile(roomId, thumbnailData.thumbnail);
info.thumbnail_info = thumbnailData.info;
if (this.matrixClient.isRoomEncrypted(roomId)) {
info.thumbnail_file = thumbnailUploadData.file;
} else {
info.thumbnail_url = thumbnailUploadData.url;
}
} catch (e) {
this.emit(cons.events.roomsInput.FILE_UPLOAD_CANCELED, roomId);
return;
}
} else if (fileType === 'audio') {
content.msgtype = 'm.audio';
content.body = file.name || 'Audio';
} else {
content.msgtype = 'm.file';
content.body = file.name || 'File';
}
try {
uploadData = await this.uploadFile(roomId, file, (data) => {
// data have two properties: data.loaded, data.total
this.emit(cons.events.roomsInput.UPLOAD_PROGRESS_CHANGES, roomId, data);
});
this.emit(cons.events.roomsInput.FILE_UPLOADED, roomId);
} catch (e) {
this.emit(cons.events.roomsInput.FILE_UPLOAD_CANCELED, roomId);
return;
}
if (this.matrixClient.isRoomEncrypted(roomId)) {
content.file = uploadData.file;
await this.matrixClient.sendMessage(roomId, content);
} else {
content.url = uploadData.url;
await this.matrixClient.sendMessage(roomId, content);
}
}
async uploadFile(roomId, file, progressHandler) {
const isEncryptedRoom = this.matrixClient.isRoomEncrypted(roomId);
let encryptInfo = null;
let encryptBlob = null;
if (isEncryptedRoom) {
const dataBuffer = await file.arrayBuffer();
if (typeof this.getInput(roomId).attachment === 'undefined') throw new Error('Attachment canceled');
const encryptedResult = await encrypt.encryptAttachment(dataBuffer);
if (typeof this.getInput(roomId).attachment === 'undefined') throw new Error('Attachment canceled');
encryptInfo = encryptedResult.info;
encryptBlob = new Blob([encryptedResult.data]);
}
const uploadingPromise = this.matrixClient.uploadContent(isEncryptedRoom ? encryptBlob : file, {
// don't send filename if room is encrypted.
includeFilename: !isEncryptedRoom,
progressHandler,
});
const input = this.getInput(roomId);
input.attachment.uploadingPromise = uploadingPromise;
this.roomIdToInput.set(roomId, input);
const { content_uri: url } = await uploadingPromise;
delete input.attachment.uploadingPromise;
this.roomIdToInput.set(roomId, input);
if (isEncryptedRoom) {
encryptInfo.url = url;
if (file.type) encryptInfo.mimetype = file.type;
return { file: encryptInfo };
}
return { url };
}
async sendEditedMessage(roomId, mEvent, editedBody) {
const content = this.getContent(
roomId,
{ msgType: mEvent.getWireContent().msgtype },
editedBody,
null,
mEvent,
);
this.matrixClient.sendMessage(roomId, content);
}
}
export default RoomsInput;

View file

@ -8,10 +8,6 @@ const cons = {
},
DEVICE_DISPLAY_NAME: 'Cinny Web',
IN_CINNY_SPACES: 'in.cinny.spaces',
tabs: {
HOME: 'home',
DIRECTS: 'dm',
},
supportEventTypes: [
'm.room.create',
'm.room.message',
@ -37,43 +33,19 @@ const cons = {
},
actions: {
navigation: {
SELECT_TAB: 'SELECT_TAB',
SELECT_SPACE: 'SELECT_SPACE',
SELECT_ROOM: 'SELECT_ROOM',
OPEN_SPACE_SETTINGS: 'OPEN_SPACE_SETTINGS',
OPEN_SPACE_MANAGE: 'OPEN_SPACE_MANAGE',
OPEN_SPACE_ADDEXISTING: 'OPEN_SPACE_ADDEXISTING',
TOGGLE_ROOM_SETTINGS: 'TOGGLE_ROOM_SETTINGS',
OPEN_SHORTCUT_SPACES: 'OPEN_SHORTCUT_SPACES',
OPEN_INVITE_LIST: 'OPEN_INVITE_LIST',
OPEN_PUBLIC_ROOMS: 'OPEN_PUBLIC_ROOMS',
OPEN_CREATE_ROOM: 'OPEN_CREATE_ROOM',
OPEN_JOIN_ALIAS: 'OPEN_JOIN_ALIAS',
OPEN_INVITE_USER: 'OPEN_INVITE_USER',
OPEN_PROFILE_VIEWER: 'OPEN_PROFILE_VIEWER',
OPEN_SETTINGS: 'OPEN_SETTINGS',
OPEN_EMOJIBOARD: 'OPEN_EMOJIBOARD',
OPEN_READRECEIPTS: 'OPEN_READRECEIPTS',
OPEN_VIEWSOURCE: 'OPEN_VIEWSOURCE',
CLICK_REPLY_TO: 'CLICK_REPLY_TO',
OPEN_SEARCH: 'OPEN_SEARCH',
OPEN_REUSABLE_CONTEXT_MENU: 'OPEN_REUSABLE_CONTEXT_MENU',
OPEN_NAVIGATION: 'OPEN_NAVIGATION',
OPEN_REUSABLE_DIALOG: 'OPEN_REUSABLE_DIALOG',
OPEN_EMOJI_VERIFICATION: 'OPEN_EMOJI_VERIFICATION',
},
room: {
JOIN: 'JOIN',
LEAVE: 'LEAVE',
CREATE: 'CREATE',
},
accountData: {
CREATE_SPACE_SHORTCUT: 'CREATE_SPACE_SHORTCUT',
DELETE_SPACE_SHORTCUT: 'DELETE_SPACE_SHORTCUT',
MOVE_SPACE_SHORTCUTS: 'MOVE_SPACE_SHORTCUTS',
CATEGORIZE_SPACE: 'CATEGORIZE_SPACE',
UNCATEGORIZE_SPACE: 'UNCATEGORIZE_SPACE',
},
settings: {
TOGGLE_SYSTEM_THEME: 'TOGGLE_SYSTEM_THEME',
TOGGLE_MARKDOWN: 'TOGGLE_MARKDOWN',
@ -86,66 +58,23 @@ const cons = {
},
events: {
navigation: {
TAB_SELECTED: 'TAB_SELECTED',
SPACE_SELECTED: 'SPACE_SELECTED',
ROOM_SELECTED: 'ROOM_SELECTED',
SPACE_SETTINGS_OPENED: 'SPACE_SETTINGS_OPENED',
SPACE_MANAGE_OPENED: 'SPACE_MANAGE_OPENED',
SPACE_ADDEXISTING_OPENED: 'SPACE_ADDEXISTING_OPENED',
ROOM_SETTINGS_TOGGLED: 'ROOM_SETTINGS_TOGGLED',
SHORTCUT_SPACES_OPENED: 'SHORTCUT_SPACES_OPENED',
INVITE_LIST_OPENED: 'INVITE_LIST_OPENED',
PUBLIC_ROOMS_OPENED: 'PUBLIC_ROOMS_OPENED',
CREATE_ROOM_OPENED: 'CREATE_ROOM_OPENED',
JOIN_ALIAS_OPENED: 'JOIN_ALIAS_OPENED',
INVITE_USER_OPENED: 'INVITE_USER_OPENED',
SETTINGS_OPENED: 'SETTINGS_OPENED',
PROFILE_VIEWER_OPENED: 'PROFILE_VIEWER_OPENED',
EMOJIBOARD_OPENED: 'EMOJIBOARD_OPENED',
READRECEIPTS_OPENED: 'READRECEIPTS_OPENED',
VIEWSOURCE_OPENED: 'VIEWSOURCE_OPENED',
REPLY_TO_CLICKED: 'REPLY_TO_CLICKED',
SEARCH_OPENED: 'SEARCH_OPENED',
REUSABLE_CONTEXT_MENU_OPENED: 'REUSABLE_CONTEXT_MENU_OPENED',
NAVIGATION_OPENED: 'NAVIGATION_OPENED',
REUSABLE_DIALOG_OPENED: 'REUSABLE_DIALOG_OPENED',
EMOJI_VERIFICATION_OPENED: 'EMOJI_VERIFICATION_OPENED',
},
roomList: {
ROOMLIST_UPDATED: 'ROOMLIST_UPDATED',
INVITELIST_UPDATED: 'INVITELIST_UPDATED',
ROOM_JOINED: 'ROOM_JOINED',
ROOM_LEAVED: 'ROOM_LEAVED',
ROOM_CREATED: 'ROOM_CREATED',
ROOM_PROFILE_UPDATED: 'ROOM_PROFILE_UPDATED',
},
accountData: {
SPACE_SHORTCUT_UPDATED: 'SPACE_SHORTCUT_UPDATED',
CATEGORIZE_SPACE_UPDATED: 'CATEGORIZE_SPACE_UPDATED',
},
notifications: {
NOTI_CHANGED: 'NOTI_CHANGED',
FULL_READ: 'FULL_READ',
MUTE_TOGGLED: 'MUTE_TOGGLED',
},
roomTimeline: {
READY: 'READY',
EVENT: 'EVENT',
PAGINATED: 'PAGINATED',
TYPING_MEMBERS_UPDATED: 'TYPING_MEMBERS_UPDATED',
LIVE_RECEIPT: 'LIVE_RECEIPT',
EVENT_REDACTED: 'EVENT_REDACTED',
AT_BOTTOM: 'AT_BOTTOM',
SCROLL_TO_LIVE: 'SCROLL_TO_LIVE',
},
roomsInput: {
MESSAGE_SENT: 'MESSAGE_SENT',
ATTACHMENT_SET: 'ATTACHMENT_SET',
FILE_UPLOADED: 'FILE_UPLOADED',
UPLOAD_PROGRESS_CHANGES: 'UPLOAD_PROGRESS_CHANGES',
FILE_UPLOAD_CANCELED: 'FILE_UPLOAD_CANCELED',
ATTACHMENT_CANCELED: 'ATTACHMENT_CANCELED',
},
settings: {
SYSTEM_THEME_TOGGLED: 'SYSTEM_THEME_TOGGLED',
MARKDOWN_TOGGLED: 'MARKDOWN_TOGGLED',

View file

@ -5,268 +5,9 @@ import cons from './cons';
class Navigation extends EventEmitter {
constructor() {
super();
// this will attached by initMatrix
this.initMatrix = {};
this.selectedTab = cons.tabs.HOME;
this.selectedSpaceId = null;
this.selectedSpacePath = [cons.tabs.HOME];
this.selectedRoomId = null;
this.recentRooms = [];
this.spaceToRoom = new Map();
this.rawModelStack = [];
}
_addToSpacePath(roomId, asRoot) {
if (typeof roomId !== 'string') {
this.selectedSpacePath = [cons.tabs.HOME];
return;
}
if (asRoot) {
this.selectedSpacePath = [roomId];
return;
}
if (this.selectedSpacePath.includes(roomId)) {
const spIndex = this.selectedSpacePath.indexOf(roomId);
this.selectedSpacePath = this.selectedSpacePath.slice(0, spIndex + 1);
return;
}
this.selectedSpacePath.push(roomId);
}
_mapRoomToSpace(roomId) {
const { roomList, accountData } = this.initMatrix;
if (
this.selectedTab === cons.tabs.HOME
&& roomList.rooms.has(roomId)
&& !roomList.roomIdToParents.has(roomId)
) {
this.spaceToRoom.set(cons.tabs.HOME, {
roomId,
timestamp: Date.now(),
});
return;
}
if (this.selectedTab === cons.tabs.DIRECTS && roomList.directs.has(roomId)) {
this.spaceToRoom.set(cons.tabs.DIRECTS, {
roomId,
timestamp: Date.now(),
});
return;
}
const parents = roomList.roomIdToParents.get(roomId);
if (!parents) return;
if (parents.has(this.selectedSpaceId)) {
this.spaceToRoom.set(this.selectedSpaceId, {
roomId,
timestamp: Date.now(),
});
} else if (accountData.categorizedSpaces.has(this.selectedSpaceId)) {
const categories = roomList.getCategorizedSpaces([this.selectedSpaceId]);
const parent = [...parents].find((pId) => categories.has(pId));
if (parent) {
this.spaceToRoom.set(parent, {
roomId,
timestamp: Date.now(),
});
}
}
}
_selectRoom(roomId, eventId) {
const prevSelectedRoomId = this.selectedRoomId;
this.selectedRoomId = roomId;
if (prevSelectedRoomId !== roomId) this._mapRoomToSpace(roomId);
this.removeRecentRoom(prevSelectedRoomId);
this.addRecentRoom(prevSelectedRoomId);
this.removeRecentRoom(this.selectedRoomId);
this.emit(
cons.events.navigation.ROOM_SELECTED,
this.selectedRoomId,
prevSelectedRoomId,
eventId,
);
}
_selectTabWithRoom(roomId) {
const { roomList, accountData } = this.initMatrix;
const { categorizedSpaces } = accountData;
if (roomList.isOrphan(roomId)) {
if (roomList.directs.has(roomId)) {
this._selectSpace(null, true, false);
this._selectTab(cons.tabs.DIRECTS, false);
return;
}
this._selectSpace(null, true, false);
this._selectTab(cons.tabs.HOME, false);
return;
}
const parents = roomList.roomIdToParents.get(roomId);
if (parents.has(this.selectedSpaceId)) {
return;
}
if (categorizedSpaces.has(this.selectedSpaceId)) {
const categories = roomList.getCategorizedSpaces([this.selectedSpaceId]);
if ([...parents].find((pId) => categories.has(pId))) {
// No need to select tab
// As one of parent is child of selected categorized space.
return;
}
}
const spaceInPath = [...this.selectedSpacePath].reverse().find((sId) => parents.has(sId));
if (spaceInPath) {
this._selectSpace(spaceInPath, false, false);
return;
}
if (roomList.directs.has(roomId)) {
this._selectSpace(null, true, false);
this._selectTab(cons.tabs.DIRECTS, false);
return;
}
if (parents.size > 0) {
const sortedParents = [...parents].sort((p1, p2) => {
const t1 = this.spaceToRoom.get(p1)?.timestamp ?? 0;
const t2 = this.spaceToRoom.get(p2)?.timestamp ?? 0;
return t2 - t1;
});
this._selectSpace(sortedParents[0], true, false);
this._selectTab(sortedParents[0], false);
}
}
_getLatestActiveRoomId(roomIds) {
const mx = this.initMatrix.matrixClient;
let ts = 0;
let roomId = null;
roomIds.forEach((childId) => {
const room = mx.getRoom(childId);
if (!room) return;
const newTs = room.getLastActiveTimestamp();
if (newTs > ts) {
ts = newTs;
roomId = childId;
}
});
return roomId;
}
_getLatestSelectedRoomId(spaceIds) {
let ts = 0;
let roomId = null;
spaceIds.forEach((sId) => {
const data = this.spaceToRoom.get(sId);
if (!data) return;
const newTs = data.timestamp;
if (newTs > ts) {
ts = newTs;
roomId = data.roomId;
}
});
return roomId;
}
_selectTab(tabId, selectRoom = true) {
this.selectedTab = tabId;
if (selectRoom) this._selectRoomWithTab(this.selectedTab);
this.emit(cons.events.navigation.TAB_SELECTED, this.selectedTab);
}
_selectSpace(roomId, asRoot, selectRoom = true) {
this._addToSpacePath(roomId, asRoot);
this.selectedSpaceId = roomId;
if (!asRoot && selectRoom) this._selectRoomWithSpace(this.selectedSpaceId);
this.emit(cons.events.navigation.SPACE_SELECTED, this.selectedSpaceId);
}
_selectRoomWithSpace(spaceId) {
if (!spaceId) return;
const { roomList, accountData, matrixClient } = this.initMatrix;
const { categorizedSpaces } = accountData;
const data = this.spaceToRoom.get(spaceId);
if (data && !categorizedSpaces.has(spaceId)) {
this._selectRoom(data.roomId);
return;
}
const children = [];
if (categorizedSpaces.has(spaceId)) {
const categories = roomList.getCategorizedSpaces([spaceId]);
const latestSelectedRoom = this._getLatestSelectedRoomId([...categories.keys()]);
if (latestSelectedRoom) {
this._selectRoom(latestSelectedRoom);
return;
}
categories?.forEach((categoryId) => {
categoryId?.forEach((childId) => {
children.push(childId);
});
});
} else {
roomList.getSpaceChildren(spaceId).forEach((id) => {
if (matrixClient.getRoom(id)?.isSpaceRoom() === false) {
children.push(id);
}
});
}
if (!children) {
this._selectRoom(null);
return;
}
this._selectRoom(this._getLatestActiveRoomId(children));
}
_selectRoomWithTab(tabId) {
const { roomList } = this.initMatrix;
if (tabId === cons.tabs.HOME || tabId === cons.tabs.DIRECTS) {
const data = this.spaceToRoom.get(tabId);
if (data) {
this._selectRoom(data.roomId);
return;
}
const children = tabId === cons.tabs.HOME ? roomList.getOrphanRooms() : [...roomList.directs];
this._selectRoom(this._getLatestActiveRoomId(children));
return;
}
this._selectRoomWithSpace(tabId);
}
removeRecentRoom(roomId) {
if (typeof roomId !== 'string') return;
const roomIdIndex = this.recentRooms.indexOf(roomId);
if (roomIdIndex >= 0) {
this.recentRooms.splice(roomIdIndex, 1);
}
}
addRecentRoom(roomId) {
if (typeof roomId !== 'string') return;
this.recentRooms.push(roomId);
if (this.recentRooms.length > 10) {
this.recentRooms.splice(0, 1);
}
}
get isRawModalVisible() {
return this.rawModelStack.length > 0;
}
@ -278,27 +19,9 @@ class Navigation extends EventEmitter {
navigate(action) {
const actions = {
[cons.actions.navigation.SELECT_TAB]: () => {
const roomId = (
action.tabId !== cons.tabs.HOME && action.tabId !== cons.tabs.DIRECTS
) ? action.tabId : null;
this._selectSpace(roomId, true);
this._selectTab(action.tabId);
},
[cons.actions.navigation.SELECT_SPACE]: () => {
this._selectSpace(action.roomId, false);
},
[cons.actions.navigation.SELECT_ROOM]: () => {
if (action.roomId) this._selectTabWithRoom(action.roomId);
this._selectRoom(action.roomId, action.eventId);
},
[cons.actions.navigation.OPEN_SPACE_SETTINGS]: () => {
this.emit(cons.events.navigation.SPACE_SETTINGS_OPENED, action.roomId, action.tabText);
},
[cons.actions.navigation.OPEN_SPACE_MANAGE]: () => {
this.emit(cons.events.navigation.SPACE_MANAGE_OPENED, action.roomId);
},
[cons.actions.navigation.OPEN_SPACE_ADDEXISTING]: () => {
this.emit(cons.events.navigation.SPACE_ADDEXISTING_OPENED, action.roomId, action.spaces);
},
@ -309,15 +32,6 @@ class Navigation extends EventEmitter {
action.tabText
);
},
[cons.actions.navigation.OPEN_SHORTCUT_SPACES]: () => {
this.emit(cons.events.navigation.SHORTCUT_SPACES_OPENED);
},
[cons.actions.navigation.OPEN_INVITE_LIST]: () => {
this.emit(cons.events.navigation.INVITE_LIST_OPENED);
},
[cons.actions.navigation.OPEN_PUBLIC_ROOMS]: () => {
this.emit(cons.events.navigation.PUBLIC_ROOMS_OPENED, action.searchTerm);
},
[cons.actions.navigation.OPEN_CREATE_ROOM]: () => {
this.emit(
cons.events.navigation.CREATE_ROOM_OPENED,
@ -340,38 +54,6 @@ class Navigation extends EventEmitter {
[cons.actions.navigation.OPEN_SETTINGS]: () => {
this.emit(cons.events.navigation.SETTINGS_OPENED, action.tabText);
},
[cons.actions.navigation.OPEN_NAVIGATION]: () => {
this.emit(cons.events.navigation.NAVIGATION_OPENED);
},
[cons.actions.navigation.OPEN_EMOJIBOARD]: () => {
this.emit(
cons.events.navigation.EMOJIBOARD_OPENED,
action.cords,
action.requestEmojiCallback,
);
},
[cons.actions.navigation.OPEN_READRECEIPTS]: () => {
this.emit(
cons.events.navigation.READRECEIPTS_OPENED,
action.roomId,
action.userIds,
);
},
[cons.actions.navigation.OPEN_VIEWSOURCE]: () => {
this.emit(
cons.events.navigation.VIEWSOURCE_OPENED,
action.event,
);
},
[cons.actions.navigation.CLICK_REPLY_TO]: () => {
this.emit(
cons.events.navigation.REPLY_TO_CLICKED,
action.userId,
action.eventId,
action.body,
action.formattedBody,
);
},
[cons.actions.navigation.OPEN_SEARCH]: () => {
this.emit(
cons.events.navigation.SEARCH_OPENED,