From 839a1324a317b3ba3df1b930c9340f5cfdd994ed Mon Sep 17 00:00:00 2001 From: Sebin Song Date: Sat, 24 Feb 2024 06:28:05 +1300 Subject: [PATCH] #1571 - Create vuex module for chat (#1854) * add chatroom/vuexModule.js with some boilerplate code in there * fix a linter error * move chat&DM related getters in state.js to the chatroom vuexModule * move all the chatroom-relateed mutations to the vuexModules * add chatroom vuexModule to store in state.js * a small bug-fix for re-login process * typo fix for the cypress failure * remove postUpgradeVerification code (resetting groups anyway) --------- Co-authored-by: Greg Slepak --- frontend/model/chatroom/vuexModule.js | 217 +++++++++++++++++++++++++ frontend/model/contracts/chatroom.js | 6 +- frontend/model/contracts/group.js | 2 +- frontend/model/state.js | 221 +------------------------- 4 files changed, 227 insertions(+), 219 deletions(-) create mode 100644 frontend/model/chatroom/vuexModule.js diff --git a/frontend/model/chatroom/vuexModule.js b/frontend/model/chatroom/vuexModule.js new file mode 100644 index 0000000000..10abbadfea --- /dev/null +++ b/frontend/model/chatroom/vuexModule.js @@ -0,0 +1,217 @@ +'use strict' + +import sbp from '@sbp/sbp' +import { Vue } from '@common/common.js' +import { merge, cloneDeep, union } from '@model/contracts/shared/giLodash.js' +import { MESSAGE_NOTIFY_SETTINGS, MESSAGE_TYPES } from '@model/contracts/shared/constants.js' +const defaultState = { + currentChatRoomIDs: {}, // { [groupId]: currentChatRoomId } + chatRoomScrollPosition: {}, // [chatRoomId]: messageHash + chatRoomUnread: {}, // [chatRoomId]: { readUntil: { messageHash, createdDate }, messages: [{ messageHash, createdDate, type, deletedDate? }]} + chatNotificationSettings: {} // { messageNotification: MESSAGE_NOTIFY_SETTINGS, messageSound: MESSAGE_NOTIFY_SETTINGS } +} + +// getters +const getters = { + currentChatRoomId (state, getters, rootState) { + return state.currentChatRoomIDs[rootState.currentGroupId] || null + }, + currentChatRoomState (state, getters, rootState) { + return rootState[getters.currentChatRoomId] || {} // avoid "undefined" vue errors at inoportune times + }, + chatNotificationSettings (state) { + return Object.assign({ + default: { + messageNotification: MESSAGE_NOTIFY_SETTINGS.DIRECT_MESSAGES, + messageSound: MESSAGE_NOTIFY_SETTINGS.DIRECT_MESSAGES + } + }, state.chatNotificationSettings || {}) + }, + ourGroupDirectMessages (state, getters, rootState) { + const currentGroupDirectMessages = {} + for (const chatRoomId of Object.keys(getters.ourDirectMessages)) { + const chatRoomState = rootState[chatRoomId] + const directMessageSettings = getters.ourDirectMessages[chatRoomId] + + // NOTE: skip DMs whose chatroom contracts are not synced yet + if (!chatRoomState || !chatRoomState.members?.[getters.ourIdentityContractId]) { + continue + } + // NOTE: direct messages should be filtered to the ones which are visible and of active group members + const members = Object.keys(chatRoomState.members) + const partners = members + .filter(memberID => memberID !== getters.ourIdentityContractId) + .sort((p1, p2) => { + const p1JoinedDate = new Date(chatRoomState.members[p1].joinedDate).getTime() + const p2JoinedDate = new Date(chatRoomState.members[p2].joinedDate).getTime() + return p1JoinedDate - p2JoinedDate + }) + const hasActiveMember = partners.some(memberID => Object.keys(getters.groupProfiles).includes(memberID)) + if (directMessageSettings.visible && hasActiveMember) { + // NOTE: lastJoinedParter is chatroom member who has joined the chatroom for the last time. + // His profile picture can be used as the picture of the direct message + // possibly with the badge of the number of partners. + const lastJoinedPartner = partners[partners.length - 1] + currentGroupDirectMessages[chatRoomId] = { + ...directMessageSettings, + members, + partners, + lastJoinedPartner, + // TODO: The UI should display display names, usernames and (in the future) + // identity contract IDs differently in some way (e.g., font, font size, + // prefix (@), etc.) to make it impossible (or at least obvious) to impersonate + // users (e.g., 'user1' changing their display name to 'user2') + title: partners.map(cID => getters.userDisplayNameFromID(cID)).join(', '), + picture: getters.ourContactProfiles[lastJoinedPartner]?.picture + } + } + } + return currentGroupDirectMessages + }, + // NOTE: this getter is used to find the ID of the direct message in the current group + // with the name[s] of partner[s]. Normally it's more useful to find direct message + // by the partners instead of contractID + ourGroupDirectMessageFromUserIds (state, getters) { + return (partners) => { // NOTE: string | string[] + if (typeof partners === 'string') { + partners = [partners] + } + const currentGroupDirectMessages = getters.ourGroupDirectMessages + return Object.keys(currentGroupDirectMessages).find(chatRoomId => { + const cPartners = currentGroupDirectMessages[chatRoomId].partners + return cPartners.length === partners.length && union(cPartners, partners).length === partners.length + }) + } + }, + isDirectMessage (state, getters) { + // NOTE: identity contract could not be synced at the time of calling this getter + return chatRoomId => !!getters.ourGroupDirectMessages[chatRoomId || getters.currentChatRoomId] + }, + isJoinedChatRoom (state, getters, rootState) { + return (chatRoomId: string, memberID?: string) => !!rootState[chatRoomId]?.members?.[memberID || getters.ourIdentityContractId] + }, + currentChatVm (state, getters, rootState) { + return rootState?.[getters.currentChatRoomId]?._vm || null + }, + currentChatRoomScrollPosition (state, getters) { + return state.chatRoomScrollPosition[getters.currentChatRoomId] // undefined means to the latest + }, + ourUnreadMessages (state, getters) { + return state.chatRoomUnread + }, + currentChatRoomReadUntil (state, getters) { + // NOTE: Optional Chaining (?) is necessary when user viewing the chatroom which he is not part of + return getters.ourUnreadMessages[getters.currentChatRoomId]?.readUntil // undefined means to the latest + }, + chatRoomUnreadMessages (state, getters) { + return (chatRoomId: string) => { + // NOTE: Optional Chaining (?) is necessary when user tries to get mentions of the chatroom which he is not part of + return getters.ourUnreadMessages[chatRoomId]?.messages || [] + } + }, + chatRoomUnreadMentions (state, getters) { + return (chatRoomId: string) => { + // NOTE: Optional Chaining (?) is necessary when user tries to get mentions of the chatroom which he is not part of + return (getters.ourUnreadMessages[chatRoomId]?.messages || []).filter(m => m.type === MESSAGE_TYPES.TEXT) + } + }, + groupUnreadMessages (state, getters, rootState) { + return (groupID: string) => Object.keys(getters.ourUnreadMessages) + .filter(cID => getters.isDirectMessage(cID) || Object.keys(rootState[groupID]?.chatRooms || {}).includes(cID)) + .map(cID => getters.ourUnreadMessages[cID].messages.length) + .reduce((sum, n) => sum + n, 0) + }, + groupIdFromChatRoomId (state, getters, rootState) { + return (chatRoomId: string) => Object.keys(rootState.contracts) + .find(cId => rootState.contracts[cId].type === 'gi.contracts/group' && + Object.keys(rootState[cId].chatRooms).includes(chatRoomId)) + }, + chatRoomsInDetail (state, getters, rootState) { + const chatRoomsInDetail = merge({}, getters.getGroupChatRooms) + for (const contractID in chatRoomsInDetail) { + const chatRoom = rootState[contractID] + if (chatRoom && chatRoom.attributes && + chatRoom.members[rootState.loggedIn.identityContractID]) { + chatRoomsInDetail[contractID] = { + ...chatRoom.attributes, + id: contractID, + unreadMessagesCount: getters.chatRoomUnreadMessages(contractID).length, + joined: true + } + } else { + const { name, privacyLevel } = chatRoomsInDetail[contractID] + chatRoomsInDetail[contractID] = { id: contractID, name, privacyLevel, joined: false } + } + } + return chatRoomsInDetail + }, + chatRoomMembersInSort (state, getters) { + return getters.groupMembersSorted + .map(member => ({ contractID: member.contractID, username: member.username, displayName: member.displayName })) + .filter(member => !!getters.chatRoomMembers[member.contractID]) || [] + } +} + +// mutations +const mutations = { + setCurrentChatRoomId (state, { groupId, chatRoomId }) { + const rootState = sbp('state/vuex/state') + + if (groupId && state[groupId] && chatRoomId) { // useful when initialize when syncing in another device + Vue.set(state.currentChatRoomIDs, groupId, chatRoomId) + } else if (chatRoomId) { // set chatRoomId as the current chatroomId of current group + Vue.set(state.currentChatRoomIDs, rootState.currentGroupId, chatRoomId) + } else if (groupId && rootState[groupId]) { // set defaultChatRoomId as the current chatroomId of current group + Vue.set(state.currentChatRoomIDs, rootState.currentGroupId, rootState[groupId].generalChatRoomId || null) + } else { // reset + Vue.set(state.currentChatRoomIDs, rootState.currentGroupId, null) + } + }, + setChatRoomScrollPosition (state, { chatRoomId, messageHash }) { + Vue.set(state.chatRoomScrollPosition, chatRoomId, messageHash) + }, + deleteChatRoomScrollPosition (state, { chatRoomId }) { + Vue.delete(state.chatRoomScrollPosition, chatRoomId) + }, + setChatRoomReadUntil (state, { chatRoomId, messageHash, createdDate }) { + Vue.set(state.chatRoomUnread, chatRoomId, { + readUntil: { messageHash, createdDate, deletedDate: null }, + messages: state.chatRoomUnread[chatRoomId].messages + ?.filter(m => new Date(m.createdDate).getTime() > new Date(createdDate).getTime()) || [] + }) + }, + deleteChatRoomReadUntil (state, { chatRoomId, deletedDate }) { + if (state.chatRoomUnread[chatRoomId].readUntil) { + Vue.set(state.chatRoomUnread[chatRoomId].readUntil, 'deletedDate', deletedDate) + } + }, + addChatRoomUnreadMessage (state, { chatRoomId, messageHash, createdDate, type }) { + state.chatRoomUnread[chatRoomId].messages.push({ messageHash, createdDate, type }) + }, + deleteChatRoomUnreadMessage (state, { chatRoomId, messageHash }) { + Vue.set( + state.chatRoomUnread[chatRoomId], + 'messages', + state.chatRoomUnread[chatRoomId].messages.filter(m => m.messageHash !== messageHash) + ) + }, + deleteChatRoomUnread (state, { chatRoomId }) { + Vue.delete(state.chatRoomUnread, chatRoomId) + }, + setChatroomNotificationSettings (state, { chatRoomId, settings }) { + if (chatRoomId) { + if (!state.chatNotificationSettings[chatRoomId]) { + Vue.set(state.chatNotificationSettings, chatRoomId, {}) + } + for (const key in settings) { + Vue.set(state.chatNotificationSettings[chatRoomId], key, settings[key]) + } + } + } +} + +export default ({ + state: () => cloneDeep(defaultState), + getters, + mutations +}: Object) diff --git a/frontend/model/contracts/chatroom.js b/frontend/model/contracts/chatroom.js index 3217097b0a..ba280ec9f0 100644 --- a/frontend/model/contracts/chatroom.js +++ b/frontend/model/contracts/chatroom.js @@ -156,7 +156,7 @@ sbp('chelonia/defineContract', { } }, sideEffect ({ contractID }) { - Vue.set(sbp('state/vuex/state').chatRoomUnread, contractID, { + Vue.set(sbp('state/vuex/state')?.chatroom?.chatRoomUnread, contractID, { readUntil: undefined, messages: [] }) @@ -519,7 +519,7 @@ sbp('chelonia/defineContract', { const rootState = sbp('state/vuex/state') const me = rootState.loggedIn.identityContractID - if (rootState.chatRoomScrollPosition[contractID] === data.hash) { + if (rootState.chatroom.chatRoomScrollPosition[contractID] === data.hash) { sbp('state/vuex/commit', 'setChatRoomScrollPosition', { chatRoomId: contractID, messageHash: null }) @@ -527,7 +527,7 @@ sbp('chelonia/defineContract', { // NOTE: readUntil can't be undefined because it would be set in advance // while syncing the contracts events especially join, addMessage, ... - if (rootState.chatRoomUnread[contractID].readUntil.messageHash === data.hash) { + if (rootState.chatroom.chatRoomUnread[contractID].readUntil.messageHash === data.hash) { sbp('state/vuex/commit', 'deleteChatRoomReadUntil', { chatRoomId: contractID, deletedDate: meta.createdDate diff --git a/frontend/model/contracts/group.js b/frontend/model/contracts/group.js index 089346404a..1c72607830 100644 --- a/frontend/model/contracts/group.js +++ b/frontend/model/contracts/group.js @@ -1415,7 +1415,7 @@ sbp('chelonia/defineContract', { sbp('okTurtles.data/delete', `JOINING_CHATROOM-${data.chatRoomID}-${memberID}`) sbp('chelonia/contract/remove', data.chatRoomID).then(() => { const rootState = sbp('state/vuex/state') - if (rootState.currentChatRoomIDs[contractID] === data.chatRoomID) { + if (rootState.chatroom.currentChatRoomIDs[contractID] === data.chatRoomID) { sbp('state/vuex/commit', 'setCurrentChatRoomId', { groupId: contractID }) diff --git a/frontend/model/state.js b/frontend/model/state.js index f8204a5117..02eae48788 100644 --- a/frontend/model/state.js +++ b/frontend/model/state.js @@ -8,24 +8,21 @@ import { Vue, L } from '@common/common.js' import { EVENT_HANDLED, CONTRACT_REGISTERED } from '~/shared/domains/chelonia/events.js' import { LOGOUT } from '~/frontend/utils/events.js' import Vuex from 'vuex' -import { MESSAGE_NOTIFY_SETTINGS, MESSAGE_TYPES, INVITE_INITIAL_CREATOR } from '@model/contracts/shared/constants.js' +import { INVITE_INITIAL_CREATOR } from '@model/contracts/shared/constants.js' import { PAYMENT_NOT_RECEIVED } from '@model/contracts/shared/payments/index.js' -import { omit, merge, cloneDeep, debounce, union } from '@model/contracts/shared/giLodash.js' +import { omit, cloneDeep, debounce } from '@model/contracts/shared/giLodash.js' import { unadjustedDistribution, adjustedDistribution } from '@model/contracts/shared/distribution/distribution.js' import { applyStorageRules } from '~/frontend/model/notifications/utils.js' // Vuex modules. import notificationModule from '~/frontend/model/notifications/vuexModule.js' import settingsModule from '~/frontend/model/settings/vuexModule.js' +import chatroomModule from '~/frontend/model/chatroom/vuexModule.js' Vue.use(Vuex) const initialState = { currentGroupId: null, - currentChatRoomIDs: {}, // { [groupId]: currentChatRoomId } - chatRoomScrollPosition: {}, // [chatRoomId]: messageHash - chatRoomUnread: {}, // [chatRoomId]: { readUntil: { messageHash, createdDate }, messages: [{ messageHash, createdDate, type, deletedDate? }]} - chatNotificationSettings: {}, // { messageNotification: MESSAGE_NOTIFY_SETTINGS, messageSound: MESSAGE_NOTIFY_SETTINGS } contracts: {}, // contractIDs => { type:string, HEAD:string, height:number } (for contracts we've successfully subscribed to) loggedIn: false, // false | { username: string, identityContractID: string } namespaceLookups: Object.create(null), // { [username]: sbp('namespace/lookup') } @@ -58,6 +55,7 @@ sbp('sbp/selectors/register', { const state = cloneDeep(initialState) state.notifications = notificationModule.state() state.settings = settingsModule.state() + state.chatroom = chatroomModule.state() store.replaceState(state) }, 'state/vuex/replace': (state) => store.replaceState(state), @@ -71,25 +69,6 @@ sbp('sbp/selectors/register', { // if (!state.notifications) { // state.notifications = [] // } - - // TODO: need to remove the whole content after we release 0.2.* - for (const chatRoomId in state.chatRoomUnread) { - if (!state.chatRoomUnread[chatRoomId].messages) { - state.chatRoomUnread[chatRoomId].messages = [] - } - if (state.chatRoomUnread[chatRoomId].mentions) { - state.chatRoomUnread[chatRoomId].mentions.forEach(m => { - state.chatRoomUnread[chatRoomId].messages.push(Object.assign({ type: MESSAGE_TYPES.TEXT }, m)) - }) - Vue.delete(state.chatRoomUnread[chatRoomId], 'mentions') - } - if (state.chatRoomUnread[chatRoomId].others) { - state.chatRoomUnread[chatRoomId].others.forEach(o => { - state.chatRoomUnread[chatRoomId].messages.push(Object.assign({ type: MESSAGE_TYPES.INTERACTIVE }, o)) - }) - Vue.delete(state.chatRoomUnread[chatRoomId], 'others') - } - } }, 'state/vuex/save': async function () { const state = store.state @@ -113,58 +92,6 @@ const mutations = { // TODO: unsubscribe from events for all members who are not in this group Vue.set(state, 'currentGroupId', currentGroupId) }, - setCurrentChatRoomId (state, { groupId, chatRoomId }) { - if (groupId && state[groupId] && chatRoomId) { // useful when initialize when syncing in another device - Vue.set(state.currentChatRoomIDs, groupId, chatRoomId) - } else if (chatRoomId) { // set chatRoomId as the current chatroomId of current group - Vue.set(state.currentChatRoomIDs, state.currentGroupId, chatRoomId) - } else if (groupId && state[groupId]) { // set defaultChatRoomId as the current chatroomId of current group - Vue.set(state.currentChatRoomIDs, state.currentGroupId, state[groupId].generalChatRoomId || null) - } else { // reset - Vue.set(state.currentChatRoomIDs, state.currentGroupId, null) - } - }, - setChatRoomScrollPosition (state, { chatRoomId, messageHash }) { - Vue.set(state.chatRoomScrollPosition, chatRoomId, messageHash) - }, - deleteChatRoomScrollPosition (state, { chatRoomId }) { - Vue.delete(state.chatRoomScrollPosition, chatRoomId) - }, - setChatRoomReadUntil (state, { chatRoomId, messageHash, createdDate }) { - Vue.set(state.chatRoomUnread, chatRoomId, { - readUntil: { messageHash, createdDate, deletedDate: null }, - messages: state.chatRoomUnread[chatRoomId].messages - ?.filter(m => new Date(m.createdDate).getTime() > new Date(createdDate).getTime()) || [] - }) - }, - deleteChatRoomReadUntil (state, { chatRoomId, deletedDate }) { - if (state.chatRoomUnread[chatRoomId].readUntil) { - Vue.set(state.chatRoomUnread[chatRoomId].readUntil, 'deletedDate', deletedDate) - } - }, - addChatRoomUnreadMessage (state, { chatRoomId, messageHash, createdDate, type }) { - state.chatRoomUnread[chatRoomId].messages.push({ messageHash, createdDate, type }) - }, - deleteChatRoomUnreadMessage (state, { chatRoomId, messageHash }) { - Vue.set( - state.chatRoomUnread[chatRoomId], - 'messages', - state.chatRoomUnread[chatRoomId].messages.filter(m => m.messageHash !== messageHash) - ) - }, - deleteChatRoomUnread (state, { chatRoomId }) { - Vue.delete(state.chatRoomUnread, chatRoomId) - }, - setChatroomNotificationSettings (state, { chatRoomId, settings }) { - if (chatRoomId) { - if (!state.chatNotificationSettings[chatRoomId]) { - Vue.set(state.chatNotificationSettings, chatRoomId, {}) - } - for (const key in settings) { - Vue.set(state.chatNotificationSettings[chatRoomId], key, settings[key]) - } - } - }, // Since Chelonia directly modifies contract state without using 'commit', we // need this hack to tell the vuex developer tool it needs to refresh the state noop () {} @@ -205,17 +132,6 @@ const getters = { currentIdentityState (state) { return (state.loggedIn && state[state.loggedIn.identityContractID]) || {} }, - currentChatRoomState (state, getters) { - return state[getters.currentChatRoomId] || {} // avoid "undefined" vue errors at inoportune times - }, - chatNotificationSettings (state) { - return Object.assign({ - default: { - messageNotification: MESSAGE_NOTIFY_SETTINGS.DIRECT_MESSAGES, - messageSound: MESSAGE_NOTIFY_SETTINGS.DIRECT_MESSAGES - } - }, state.chatNotificationSettings || {}) - }, ourUsername (state) { return state.loggedIn && state.loggedIn.username }, @@ -568,132 +484,6 @@ const getters = { const nameB = getters.ourContactProfiles[usernameB].displayName || usernameB return nameA.normalize().toUpperCase() > nameB.normalize().toUpperCase() ? 1 : -1 }) - }, - ourGroupDirectMessages (state, getters) { - const currentGroupDirectMessages = {} - for (const chatRoomId of Object.keys(getters.ourDirectMessages)) { - const chatRoomState = state[chatRoomId] - const directMessageSettings = getters.ourDirectMessages[chatRoomId] - - // NOTE: skip DMs whose chatroom contracts are not synced yet - if (!chatRoomState || !chatRoomState.members?.[getters.ourIdentityContractId]) { - continue - } - // NOTE: direct messages should be filtered to the ones which are visible and of active group members - const members = Object.keys(chatRoomState.members) - const partners = members - .filter(memberID => memberID !== getters.ourIdentityContractId) - .sort((p1, p2) => { - const p1JoinedDate = new Date(chatRoomState.members[p1].joinedDate).getTime() - const p2JoinedDate = new Date(chatRoomState.members[p2].joinedDate).getTime() - return p1JoinedDate - p2JoinedDate - }) - const hasActiveMember = partners.some(memberID => Object.keys(getters.groupProfiles).includes(memberID)) - if (directMessageSettings.visible && hasActiveMember) { - // NOTE: lastJoinedParter is chatroom member who has joined the chatroom for the last time. - // His profile picture can be used as the picture of the direct message - // possibly with the badge of the number of partners. - const lastJoinedPartner = partners[partners.length - 1] - currentGroupDirectMessages[chatRoomId] = { - ...directMessageSettings, - members, - partners, - lastJoinedPartner, - // TODO: The UI should display display names, usernames and (in the future) - // identity contract IDs differently in some way (e.g., font, font size, - // prefix (@), etc.) to make it impossible (or at least obvious) to impersonate - // users (e.g., 'user1' changing their display name to 'user2') - title: partners.map(cID => getters.userDisplayNameFromID(cID)).join(', '), - picture: getters.ourContactProfiles[lastJoinedPartner]?.picture - } - } - } - return currentGroupDirectMessages - }, - // NOTE: this getter is used to find the ID of the direct message in the current group - // with the name[s] of partner[s]. Normally it's more useful to find direct message - // by the partners instead of contractID - ourGroupDirectMessageFromUserIds (state, getters) { - return (partners) => { // NOTE: string | string[] - if (typeof partners === 'string') { - partners = [partners] - } - const currentGroupDirectMessages = getters.ourGroupDirectMessages - return Object.keys(currentGroupDirectMessages).find(chatRoomId => { - const cPartners = currentGroupDirectMessages[chatRoomId].partners - return cPartners.length === partners.length && union(cPartners, partners).length === partners.length - }) - } - }, - isDirectMessage (state, getters) { - // NOTE: identity contract could not be synced at the time of calling this getter - return chatRoomId => !!getters.ourGroupDirectMessages[chatRoomId || getters.currentChatRoomId] - }, - isJoinedChatRoom (state, getters) { - return (chatRoomId: string, memberID?: string) => !!state[chatRoomId]?.members?.[memberID || getters.ourIdentityContractId] - }, - currentChatRoomId (state, getters) { - return state.currentChatRoomIDs[state.currentGroupId] || null - }, - currentChatVm (state, getters) { - return state?.[getters.currentChatRoomId]?._vm || null - }, - currentChatRoomScrollPosition (state, getters) { - return state.chatRoomScrollPosition[getters.currentChatRoomId] // undefined means to the latest - }, - ourUnreadMessages (state, getters) { - return state.chatRoomUnread - }, - currentChatRoomReadUntil (state, getters) { - // NOTE: Optional Chaining (?) is necessary when user viewing the chatroom which he is not part of - return getters.ourUnreadMessages[getters.currentChatRoomId]?.readUntil // undefined means to the latest - }, - chatRoomUnreadMessages (state, getters) { - return (chatRoomId: string) => { - // NOTE: Optional Chaining (?) is necessary when user tries to get mentions of the chatroom which he is not part of - return getters.ourUnreadMessages[chatRoomId]?.messages || [] - } - }, - chatRoomUnreadMentions (state, getters) { - return (chatRoomId: string) => { - // NOTE: Optional Chaining (?) is necessary when user tries to get mentions of the chatroom which he is not part of - return (getters.ourUnreadMessages[chatRoomId]?.messages || []).filter(m => m.type === MESSAGE_TYPES.TEXT) - } - }, - groupUnreadMessages (state, getters) { - return (groupID: string) => Object.keys(getters.ourUnreadMessages) - .filter(cID => getters.isDirectMessage(cID) || Object.keys(state[groupID]?.chatRooms || {}).includes(cID)) - .map(cID => getters.ourUnreadMessages[cID].messages.length) - .reduce((sum, n) => sum + n, 0) - }, - groupIdFromChatRoomId (state, getters) { - return (chatRoomId: string) => Object.keys(state.contracts) - .find(cId => state.contracts[cId].type === 'gi.contracts/group' && - Object.keys(state[cId].chatRooms).includes(chatRoomId)) - }, - chatRoomsInDetail (state, getters) { - const chatRoomsInDetail = merge({}, getters.getGroupChatRooms) - for (const contractID in chatRoomsInDetail) { - const chatRoom = state[contractID] - if (chatRoom && chatRoom.attributes && - chatRoom.members[state.loggedIn.identityContractID]) { - chatRoomsInDetail[contractID] = { - ...chatRoom.attributes, - id: contractID, - unreadMessagesCount: getters.chatRoomUnreadMessages(contractID).length, - joined: true - } - } else { - const { name, privacyLevel } = chatRoomsInDetail[contractID] - chatRoomsInDetail[contractID] = { id: contractID, name, privacyLevel, joined: false } - } - } - return chatRoomsInDetail - }, - chatRoomMembersInSort (state, getters) { - return getters.groupMembersSorted - .map(member => ({ contractID: member.contractID, username: member.username, displayName: member.displayName })) - .filter(member => !!getters.chatRoomMembers[member.contractID]) || [] } } @@ -703,7 +493,8 @@ const store: any = new Vuex.Store({ getters, modules: { notifications: notificationModule, - settings: settingsModule + settings: settingsModule, + chatroom: chatroomModule }, strict: false // we're intentionally modifying state outside of commits })