Skip to content

Commit

Permalink
#1571 - Create vuex module for chat (#1854)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
SebinSong and taoeffect authored Feb 23, 2024
1 parent ccbe124 commit 839a132
Show file tree
Hide file tree
Showing 4 changed files with 227 additions and 219 deletions.
217 changes: 217 additions & 0 deletions frontend/model/chatroom/vuexModule.js
Original file line number Diff line number Diff line change
@@ -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)
6 changes: 3 additions & 3 deletions frontend/model/contracts/chatroom.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: []
})
Expand Down Expand Up @@ -519,15 +519,15 @@ 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
})
}

// 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
Expand Down
2 changes: 1 addition & 1 deletion frontend/model/contracts/group.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
})
Expand Down
Loading

0 comments on commit 839a132

Please sign in to comment.