From 7d24ef79328495d0f0092e5caddf6a29a1ac63de Mon Sep 17 00:00:00 2001 From: SebinSong Date: Thu, 15 Feb 2024 13:05:14 +1300 Subject: [PATCH 1/8] add chatroom/vuexModule.js with some boilerplate code in there --- frontend/model/chatroom/vuexModule.js | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) 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..e20dae9a2f --- /dev/null +++ b/frontend/model/chatroom/vuexModule.js @@ -0,0 +1,18 @@ +'use strict' + +import { cloneDeep } from '@model/contracts/shared/giLodash.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 = {} + +export default ({ + state: () => cloneDeep(defaultState), + getters +}: Object) From 91a8b73e58a1b35dd476f547054de25df09f4a98 Mon Sep 17 00:00:00 2001 From: SebinSong Date: Sun, 18 Feb 2024 12:37:36 +1300 Subject: [PATCH 2/8] fix a linter error --- frontend/model/chatroom/vuexModule.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/model/chatroom/vuexModule.js b/frontend/model/chatroom/vuexModule.js index e20dae9a2f..d1ef2e4129 100644 --- a/frontend/model/chatroom/vuexModule.js +++ b/frontend/model/chatroom/vuexModule.js @@ -6,7 +6,7 @@ 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 } + chatNotificationSettings: {} // { messageNotification: MESSAGE_NOTIFY_SETTINGS, messageSound: MESSAGE_NOTIFY_SETTINGS } } // getters From 2044db5e3f313e009b233a1ee8abb335c10354a5 Mon Sep 17 00:00:00 2001 From: SebinSong Date: Sun, 18 Feb 2024 14:06:45 +1300 Subject: [PATCH 3/8] move chat&DM related getters in state.js to the chatroom vuexModule --- frontend/model/chatroom/vuexModule.js | 152 +++++++++++++++++++++++- frontend/model/contracts/manifests.json | 6 +- 2 files changed, 151 insertions(+), 7 deletions(-) diff --git a/frontend/model/chatroom/vuexModule.js b/frontend/model/chatroom/vuexModule.js index d1ef2e4129..466f50642d 100644 --- a/frontend/model/chatroom/vuexModule.js +++ b/frontend/model/chatroom/vuexModule.js @@ -1,7 +1,7 @@ 'use strict' -import { cloneDeep } from '@model/contracts/shared/giLodash.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 @@ -10,9 +10,153 @@ const defaultState = { } // getters -const 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) { + return (chatRoomId: string, memberID?: string) => !!state[chatRoomId]?.members?.[memberID || getters.ourIdentityContractId] + }, + 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, 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 = { + +} export default ({ state: () => cloneDeep(defaultState), - getters + getters, + mutations }: Object) diff --git a/frontend/model/contracts/manifests.json b/frontend/model/contracts/manifests.json index e8ee471d0f..48d119f706 100644 --- a/frontend/model/contracts/manifests.json +++ b/frontend/model/contracts/manifests.json @@ -1,7 +1,7 @@ { "manifests": { - "gi.contracts/chatroom": "z9brRu3VMifmYkk97FijV5myjFAC5xPTYr2itAA5R8B5ygam3WZe", - "gi.contracts/group": "z9brRu3VG462ZBcaa6yEmybw3aZztTfbanPuc6Dk5c7pNL8nBmYH", - "gi.contracts/identity": "z9brRu3VVFhuow21YeqtEreuVwurDzwV65QSuD39QqRE5MZoqADs" + "gi.contracts/chatroom": "z9brRu3VXcbQ8j8sZKgGtmBWrmhRPWYyYPCAjrwUFvteX93Hn8MV", + "gi.contracts/group": "z9brRu3VFeizdYVsxnbF4NfhqNedjVbrhNgd3Mzu9frebx1erEXB", + "gi.contracts/identity": "z9brRu3VU9rsSSW22TcEUGV3yKi9zXCfDcMGfGefgie2yUDszkpb" } } From 457c2d73c7d11b7c75072cd767f25bcdd074419c Mon Sep 17 00:00:00 2001 From: SebinSong Date: Sun, 18 Feb 2024 14:21:16 +1300 Subject: [PATCH 4/8] move all the chatroom-relateed mutations to the vuexModules --- frontend/model/chatroom/vuexModule.js | 55 +++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/frontend/model/chatroom/vuexModule.js b/frontend/model/chatroom/vuexModule.js index 466f50642d..7bad37e568 100644 --- a/frontend/model/chatroom/vuexModule.js +++ b/frontend/model/chatroom/vuexModule.js @@ -1,5 +1,7 @@ '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 = { @@ -152,7 +154,60 @@ const getters = { // 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 && state[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 ({ From e33b4d813797141e4b617c824f53c967c47d2066 Mon Sep 17 00:00:00 2001 From: SebinSong Date: Sun, 18 Feb 2024 18:08:30 +1300 Subject: [PATCH 5/8] add chatroom vuexModule to store in state.js --- frontend/model/chatroom/vuexModule.js | 8 +- frontend/model/contracts/chatroom.js | 6 +- frontend/model/contracts/group.js | 2 +- frontend/model/contracts/manifests.json | 4 +- frontend/model/state.js | 225 ++---------------------- 5 files changed, 28 insertions(+), 217 deletions(-) diff --git a/frontend/model/chatroom/vuexModule.js b/frontend/model/chatroom/vuexModule.js index 7bad37e568..8f5fa92f30 100644 --- a/frontend/model/chatroom/vuexModule.js +++ b/frontend/model/chatroom/vuexModule.js @@ -87,11 +87,11 @@ const 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] + isJoinedChatRoom (state, getters, rootState) { + return (chatRoomId: string, memberID?: string) => !!rootState[chatRoomId]?.members?.[memberID || getters.ourIdentityContractId] }, - currentChatVm (state, getters) { - return state?.[getters.currentChatRoomId]?._vm || null + currentChatVm (state, getters, rootState) { + return rootState?.[getters.currentChatRoomId]?._vm || null }, currentChatRoomScrollPosition (state, getters) { return state.chatRoomScrollPosition[getters.currentChatRoomId] // undefined means to the latest 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 553da0209a..4120e270b0 100644 --- a/frontend/model/contracts/group.js +++ b/frontend/model/contracts/group.js @@ -1412,7 +1412,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/contracts/manifests.json b/frontend/model/contracts/manifests.json index 48d119f706..34302a1d3c 100644 --- a/frontend/model/contracts/manifests.json +++ b/frontend/model/contracts/manifests.json @@ -1,7 +1,7 @@ { "manifests": { - "gi.contracts/chatroom": "z9brRu3VXcbQ8j8sZKgGtmBWrmhRPWYyYPCAjrwUFvteX93Hn8MV", - "gi.contracts/group": "z9brRu3VFeizdYVsxnbF4NfhqNedjVbrhNgd3Mzu9frebx1erEXB", + "gi.contracts/chatroom": "z9brRu3VFwXtjWpeQfjWBUGxB6TK9gMRDareQhjwjN8mGztv24wr", + "gi.contracts/group": "z9brRu3VJVrG1wk9QwHLBPCmzBGKSVMKJWJzMu4ujfjyRkMmK39f", "gi.contracts/identity": "z9brRu3VU9rsSSW22TcEUGV3yKi9zXCfDcMGfGefgie2yUDszkpb" } } diff --git a/frontend/model/state.js b/frontend/model/state.js index f8204a5117..59d2f5e458 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 { MESSAGE_TYPES, 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), @@ -72,22 +70,23 @@ sbp('sbp/selectors/register', { // state.notifications = [] // } + const chatroomState = state.chatroom // 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 = [] + for (const chatRoomId in chatroomState.chatRoomUnread) { + if (!chatroomState.chatRoomUnread[chatRoomId].messages) { + chatroomState.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)) + if (chatroomState.chatRoomUnread[chatRoomId].mentions) { + chatroomState.chatRoomUnread[chatRoomId].mentions.forEach(m => { + chatroomState.chatRoomUnread[chatRoomId].messages.push(Object.assign({ type: MESSAGE_TYPES.TEXT }, m)) }) - Vue.delete(state.chatRoomUnread[chatRoomId], 'mentions') + Vue.delete(chatroomState.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)) + if (chatroomState.chatRoomUnread[chatRoomId].others) { + chatroomState.chatRoomUnread[chatRoomId].others.forEach(o => { + chatroomState.chatRoomUnread[chatRoomId].messages.push(Object.assign({ type: MESSAGE_TYPES.INTERACTIVE }, o)) }) - Vue.delete(state.chatRoomUnread[chatRoomId], 'others') + Vue.delete(chatroomState.chatRoomUnread[chatRoomId], 'others') } } }, @@ -113,58 +112,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 +152,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 +504,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 +513,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 }) From c0b8f07431979eb993fd0b67fa36135e25c06e10 Mon Sep 17 00:00:00 2001 From: SebinSong Date: Mon, 19 Feb 2024 10:42:38 +1300 Subject: [PATCH 6/8] a small bug-fix for re-login process --- frontend/model/state.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/model/state.js b/frontend/model/state.js index 59d2f5e458..fe6e1d81c8 100644 --- a/frontend/model/state.js +++ b/frontend/model/state.js @@ -70,7 +70,7 @@ sbp('sbp/selectors/register', { // state.notifications = [] // } - const chatroomState = state.chatroom + const chatroomState = state.chatroom || {} // TODO: need to remove the whole content after we release 0.2.* for (const chatRoomId in chatroomState.chatRoomUnread) { if (!chatroomState.chatRoomUnread[chatRoomId].messages) { From 414447f57f77a0728bf5698bf7c65d58814d82c2 Mon Sep 17 00:00:00 2001 From: SebinSong Date: Wed, 21 Feb 2024 11:16:32 +1300 Subject: [PATCH 7/8] typo fix for the cypress failure --- frontend/model/chatroom/vuexModule.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/model/chatroom/vuexModule.js b/frontend/model/chatroom/vuexModule.js index 8f5fa92f30..10abbadfea 100644 --- a/frontend/model/chatroom/vuexModule.js +++ b/frontend/model/chatroom/vuexModule.js @@ -161,7 +161,7 @@ const mutations = { 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 && state[groupId]) { // set defaultChatRoomId as the current chatroomId of current group + } 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) From f95234b9bdc15354fb26090f5c99e2d593d65fc3 Mon Sep 17 00:00:00 2001 From: Greg Slepak Date: Fri, 23 Feb 2024 09:10:38 -0800 Subject: [PATCH 8/8] remove postUpgradeVerification code (resetting groups anyway) --- frontend/model/state.js | 22 +--------------------- 1 file changed, 1 insertion(+), 21 deletions(-) diff --git a/frontend/model/state.js b/frontend/model/state.js index fe6e1d81c8..02eae48788 100644 --- a/frontend/model/state.js +++ b/frontend/model/state.js @@ -8,7 +8,7 @@ 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_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, cloneDeep, debounce } from '@model/contracts/shared/giLodash.js' import { unadjustedDistribution, adjustedDistribution } from '@model/contracts/shared/distribution/distribution.js' @@ -69,26 +69,6 @@ sbp('sbp/selectors/register', { // if (!state.notifications) { // state.notifications = [] // } - - const chatroomState = state.chatroom || {} - // TODO: need to remove the whole content after we release 0.2.* - for (const chatRoomId in chatroomState.chatRoomUnread) { - if (!chatroomState.chatRoomUnread[chatRoomId].messages) { - chatroomState.chatRoomUnread[chatRoomId].messages = [] - } - if (chatroomState.chatRoomUnread[chatRoomId].mentions) { - chatroomState.chatRoomUnread[chatRoomId].mentions.forEach(m => { - chatroomState.chatRoomUnread[chatRoomId].messages.push(Object.assign({ type: MESSAGE_TYPES.TEXT }, m)) - }) - Vue.delete(chatroomState.chatRoomUnread[chatRoomId], 'mentions') - } - if (chatroomState.chatRoomUnread[chatRoomId].others) { - chatroomState.chatRoomUnread[chatRoomId].others.forEach(o => { - chatroomState.chatRoomUnread[chatRoomId].messages.push(Object.assign({ type: MESSAGE_TYPES.INTERACTIVE }, o)) - }) - Vue.delete(chatroomState.chatRoomUnread[chatRoomId], 'others') - } - } }, 'state/vuex/save': async function () { const state = store.state