Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Save chatroom logs in KV store #1998

Merged
merged 43 commits into from
Jun 10, 2024
Merged
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
630d5ff
chore: do not open new tab when user is signed in
Silver-IT Apr 19, 2024
ed59351
chore: do not open new tab when user is signed in
Silver-IT Apr 19, 2024
1fbfcf4
Merge branch 'master' into 1942-missing-dm-notifications-in-brave
Silver-IT May 8, 2024
c0d004f
chore: do not open new tab when user is signed in
Silver-IT Apr 19, 2024
e0a3fab
Merge branch 'master' into 1942-missing-dm-notifications-in-brave
Silver-IT May 9, 2024
eef2079
chore: do not open new tab when user is signed in
Silver-IT Apr 19, 2024
08dbf97
Merge branch 'master' into 1942-missing-dm-notifications-in-brave
Silver-IT May 13, 2024
fb6a390
feat: created chatRoomLogs state
Silver-IT May 14, 2024
9eed149
feat: made chatroom logs utility functions
Silver-IT May 14, 2024
9c00b68
feat: moved chatRoomLogs utility functions from chatroom to identity
Silver-IT May 14, 2024
b0b5fdc
feat: updated function setChatRoomReadUntil
Silver-IT May 14, 2024
2056521
chore: do not open new tab when user is signed in
Silver-IT Apr 19, 2024
9aeeb38
feat: update chatroom contract using chatRoomLogs utility function
Silver-IT May 15, 2024
ad3488f
feat: updated chatRoomLogs utility functions considering multiple ses…
Silver-IT May 15, 2024
5a44e92
feat: display isNew indicator according to the updated createdHeight
Silver-IT May 16, 2024
5499806
feat: implement storing chatroom logs in kv
Silver-IT May 16, 2024
0f84db7
feat: add all chatRoomLogs utility functions in a queue
Silver-IT May 16, 2024
80fa2ee
feat: retry 10 times until the chatroomlogs utility function succeed
Silver-IT May 17, 2024
15a182a
feat: resolved conflicts
Silver-IT May 17, 2024
5512e44
chore: revert unrelated changes
Silver-IT May 17, 2024
70938ae
chore: do not open new tab when user is signed in
Silver-IT Apr 19, 2024
6543afa
feat: simplified chatRoomLogs unreadMessages
Silver-IT May 28, 2024
031d98c
feat: simplified chatRoomLogs by deleting deletedHeight of readUntil
Silver-IT May 28, 2024
54498af
fix: resolved conflicts
Silver-IT May 28, 2024
90c29ec
feat: improved to search users by keyword
Silver-IT May 30, 2024
7d824ef
feat: improved variable names
Silver-IT May 30, 2024
df337d5
fix: error regarding cypress within
Silver-IT May 30, 2024
ad54aea
fix: handle errors in loadChatRoomUnreadMessages
Silver-IT May 30, 2024
80f2d7e
Update KV store set (#2027)
corrideat May 30, 2024
bd8bb78
feat: remove requireToSuccess function and used onconflict in kv/set
Silver-IT May 31, 2024
e070b40
fix: lint error and error in setting lastLoggedIn
Silver-IT May 31, 2024
bb36d22
chore: tiny update and travis retry
Silver-IT May 31, 2024
1b58d71
chore: added comment and travis retry
Silver-IT May 31, 2024
5430fa0
fix: some errors in using variable names
Silver-IT May 31, 2024
4e640b5
feat: test unread messages count in cypress
Silver-IT May 31, 2024
7b7e6ad
fix: error when rejoining group
Silver-IT May 31, 2024
c578c61
fix: set renderingChatRoomId before initiate state in ChatMain.vue
Silver-IT Jun 3, 2024
878a184
chore: added comment and travis retry
Silver-IT Jun 4, 2024
5481d17
feat: wait invocations to run before logout in cypress
Silver-IT Jun 4, 2024
cd1332f
feat: improved manage chatroom_events queue
Silver-IT Jun 6, 2024
cce59c4
chore: added comment and travis retry
Silver-IT Jun 6, 2024
86f40a4
fix: error in groupDirectMessages getter when groupID is null
Silver-IT Jun 9, 2024
04c6556
chore: added comment and travis retry
Silver-IT Jun 9, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion frontend/common/translations.js
Original file line number Diff line number Diff line change
Expand Up @@ -133,13 +133,14 @@ export default function L (

export function LError (error: Error): {|reportError: any|} {
let url = `/app/dashboard?modal=UserSettingsModal&tab=application-logs&errorMsg=${encodeURI(error.message)}`
const target = !sbp('state/vuex/state').loggedIn ? 'target="_blank"' : ''
if (!sbp('state/vuex/state').loggedIn) {
url = 'https://github.com/okTurtles/group-income/issues'
}
return {
reportError: L('"{errorMsg}". You can {a_}report the error{_a}.', {
errorMsg: error.message,
'a_': `<a class="link" target="_blank" href="${url}">`,
'a_': `<a class="link" ${target} href="${url}">`,
'_a': '</a>'
})
}
Expand Down
5 changes: 3 additions & 2 deletions frontend/controller/actions/group.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
SWITCH_GROUP,
JOINED_GROUP
} from '@utils/events.js'
import { KV_KEYS } from '@utils/constants.js'
import { imageUpload } from '@utils/image.js'
import { GIMessage } from '~/shared/domains/chelonia/chelonia.js'
import { findKeyIdByName } from '~/shared/domains/chelonia/utils.js'
Expand Down Expand Up @@ -958,9 +959,9 @@ export default (sbp('sbp/selectors/register', {

// Wait for any pending operations (e.g., sync) to finish
await sbp('chelonia/queueInvocation', contractID, async () => {
const current = await sbp('chelonia/kv/get', contractID, 'lastLoggedIn')?.data || {}
const current = (await sbp('chelonia/kv/get', contractID, KV_KEYS.LAST_LOGGED_IN))?.data || {}
current[userID] = now
await sbp('chelonia/kv/set', contractID, 'lastLoggedIn', current, {
await sbp('chelonia/kv/set', contractID, KV_KEYS.LAST_LOGGED_IN, current, {
encryptionKeyId: sbp('chelonia/contract/currentKeyIdByName', contractID, 'cek'),
signingKeyId: sbp('chelonia/contract/currentKeyIdByName', contractID, 'csk')
})
Expand Down
123 changes: 121 additions & 2 deletions frontend/controller/actions/identity.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import { has, omit } from '@model/contracts/shared/giLodash.js'
import sbp from '@sbp/sbp'
import { imageUpload, objectURLtoBlob } from '@utils/image.js'
import { SETTING_CURRENT_USER } from '~/frontend/model/database.js'
import { LOGIN, LOGIN_ERROR, LOGOUT } from '~/frontend/utils/events.js'
import { LOGIN, LOGIN_ERROR, LOGOUT, UNREAD_MESSAGES_QUEUE } from '~/frontend/utils/events.js'
import { KV_KEYS } from '~/frontend/utils/constants.js'
import { GIMessage } from '~/shared/domains/chelonia/GIMessage.js'
import { boxKeyPair, buildRegisterSaltRequest, computeCAndHc, decryptContractSalt, hash, hashPassword, randomNonce } from '~/shared/zkpp.js'
import { findKeyIdByName } from '~/shared/domains/chelonia/utils.js'
Expand All @@ -21,6 +22,22 @@ import type { Key } from '../../../shared/domains/chelonia/crypto.js'
import { handleFetchResult } from '../utils/misc.js'
import { encryptedAction } from './utils.js'

const requireToSuccess = async (fnToSuccess) => {
if (typeof fnToSuccess !== 'function') {
return
}

let failureCount = 0
do {
try {
return await fnToSuccess()
} catch (err) {
console.error('[requireToSuccess:]', err)
failureCount++
}
} while (failureCount < 10)
}

Copy link
Member

@taoeffect taoeffect May 30, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Silver-IT This function can now be removed once #2027 is merged into this PR. Could you please have a look at that PR and merge it when ready?

export default (sbp('sbp/selectors/register', {
'gi.actions/identity/retrieveSalt': async (username: string, passwordFn: () => string) => {
const r = randomNonce()
Expand Down Expand Up @@ -357,7 +374,7 @@ export default (sbp('sbp/selectors/register', {
return index === -1 ? contractSyncPriorityList.length : index
}

// loading the website instead of stalling out.
// loading the website instead of stalling out.
try {
if (!state) {
// Make sure we don't unsubscribe from our own identity contract
Expand Down Expand Up @@ -391,6 +408,11 @@ export default (sbp('sbp/selectors/register', {
}
}

// NOTE: update chatRoomUnreadMessages to the latest one we do this here
// just after the identity contract is synced because
// while syncing the chatroom contract it could be necessary to update chatRoomUnreadMessages
await sbp('gi.actions/identity/loadChatRoomUnreadMessages')

try {
// $FlowFixMe[incompatible-call]
await Promise.all(Object.entries(contractIDs).sort(([a], [b]) => {
Expand Down Expand Up @@ -770,6 +792,103 @@ export default (sbp('sbp/selectors/register', {
})
}
},
'gi.actions/identity/fetchChatRoomUnreadMessages': async () => {
const { ourIdentityContractId } = sbp('state/vuex/getters')
return (await sbp('chelonia/kv/get', ourIdentityContractId, KV_KEYS.UNREAD_MESSAGES))?.data || {}
},
'gi.actions/identity/saveChatRoomUnreadMessages': (contractID: string, data: Object) => {
const { ourIdentityContractId } = sbp('state/vuex/getters')

return sbp('chelonia/kv/set', ourIdentityContractId, KV_KEYS.UNREAD_MESSAGES, data, {
encryptionKeyId: sbp('chelonia/contract/currentKeyIdByName', ourIdentityContractId, 'cek'),
signingKeyId: sbp('chelonia/contract/currentKeyIdByName', ourIdentityContractId, 'csk')
})
},
'gi.actions/identity/loadChatRoomUnreadMessages': () => {
return sbp('okTurtles.eventQueue/queueEvent', UNREAD_MESSAGES_QUEUE, () => {
return requireToSuccess(async () => {
const currentChatRoomUnreadMessages = await sbp('gi.actions/identity/fetchChatRoomUnreadMessages')
sbp('state/vuex/commit', 'setUnreadMessages', currentChatRoomUnreadMessages)
})
})
},
'gi.actions/identity/initChatRoomUnreadMessages': (contractID: string) => {
return sbp('okTurtles.eventQueue/queueEvent', UNREAD_MESSAGES_QUEUE, () => {
return requireToSuccess(async () => {
const currentChatRoomUnreadMessages = await sbp('gi.actions/identity/fetchChatRoomUnreadMessages')

if (!currentChatRoomUnreadMessages[contractID]) {
currentChatRoomUnreadMessages[contractID] = { readUntil: null, unreadMessages: [] }
await sbp('gi.actions/identity/saveChatRoomUnreadMessages', contractID, currentChatRoomUnreadMessages)
}
})
})
},
'gi.actions/identity/setChatRoomReadUntil': ({ contractID, messageHash, createdHeight }: {
contractID: string, messageHash: string, createdHeight: number
}) => {
return sbp('okTurtles.eventQueue/queueEvent', UNREAD_MESSAGES_QUEUE, () => {
return requireToSuccess(async () => {
const currentChatRoomUnreadMessages = await sbp('gi.actions/identity/fetchChatRoomUnreadMessages')

const exReadUntil = currentChatRoomUnreadMessages[contractID].readUntil
if (exReadUntil === null || exReadUntil.createdHeight < createdHeight) {
const exUnreadMessages = currentChatRoomUnreadMessages[contractID]?.unreadMessages || []
currentChatRoomUnreadMessages[contractID] = {
readUntil: { messageHash, createdHeight },
unreadMessages: exUnreadMessages.filter(msg => msg.createdHeight > createdHeight)
}

await sbp('gi.actions/identity/saveChatRoomUnreadMessages', contractID, currentChatRoomUnreadMessages)
}
})
})
},
'gi.actions/identity/addChatRoomUnreadMessage': ({ contractID, messageHash, createdHeight }: {
contractID: string, messageHash: string, createdHeight: number
}) => {
return sbp('okTurtles.eventQueue/queueEvent', UNREAD_MESSAGES_QUEUE, () => {
return requireToSuccess(async () => {
const currentChatRoomUnreadMessages = await sbp('gi.actions/identity/fetchChatRoomUnreadMessages')

// NOTE: should ignore to add unreadMessages before joining chatroom
if (currentChatRoomUnreadMessages[contractID].readUntil) {
const index = currentChatRoomUnreadMessages[contractID].unreadMessages.findIndex(msg => msg.messageHash === messageHash)
if (index < 0) {
currentChatRoomUnreadMessages[contractID].unreadMessages.push({ messageHash, createdHeight })
await sbp('gi.actions/identity/saveChatRoomUnreadMessages', contractID, currentChatRoomUnreadMessages)
}
}
})
})
},
'gi.actions/identity/removeChatRoomUnreadMessage': ({ contractID, messageHash }: {
contractID: string, messageHash: string
}) => {
return sbp('okTurtles.eventQueue/queueEvent', UNREAD_MESSAGES_QUEUE, () => {
return requireToSuccess(async () => {
const currentChatRoomUnreadMessages = await sbp('gi.actions/identity/fetchChatRoomUnreadMessages')

// NOTE: should ignore to delete unreadMessages before joining chatroom
if (currentChatRoomUnreadMessages[contractID].readUntil) {
const index = currentChatRoomUnreadMessages[contractID].unreadMessages.findIndex(msg => msg.messageHash === messageHash)
if (index >= 0) {
currentChatRoomUnreadMessages[contractID].unreadMessages.splice(index, 1)
await sbp('gi.actions/identity/saveChatRoomUnreadMessages', contractID, currentChatRoomUnreadMessages)
}
}
})
})
},
'gi.actions/identity/deleteChatRoomUnreadMessages': ({ contractID }: { contractID: string }) => {
return sbp('okTurtles.eventQueue/queueEvent', UNREAD_MESSAGES_QUEUE, () => {
return requireToSuccess(async () => {
const currentChatRoomUnreadMessages = await sbp('gi.actions/identity/fetchChatRoomUnreadMessages')
delete currentChatRoomUnreadMessages[contractID]
await sbp('gi.actions/identity/saveChatRoomUnreadMessages', contractID, currentChatRoomUnreadMessages)
})
})
},
...encryptedAction('gi.actions/identity/saveFileDeleteToken', L('Failed to save delete tokens for the attachments.')),
...encryptedAction('gi.actions/identity/removeFileDeleteToken', L('Failed to remove delete tokens for the attachments.'))
}): string[])
20 changes: 13 additions & 7 deletions frontend/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import './model/notifications/periodicNotifications.js'
import notificationsMixin from './model/notifications/mainNotificationsMixin.js'
import { showNavMixin } from './views/utils/misc.js'
import FaviconBadge from './utils/faviconBadge.js'
import { KV_KEYS } from './utils/constants.js'

const { Vue, L } = Common

Expand Down Expand Up @@ -129,7 +130,10 @@ async function startApp () {
'chelonia/contract/disconnect',
'gi.actions/identity/removeFiles',
'gi.actions/chatroom/join',
'chelonia/contract/hasKeysToPerformOperation'
'chelonia/contract/hasKeysToPerformOperation',
'gi.actions/identity/initChatRoomUnreadMessages', 'gi.actions/identity/deleteChatRoomUnreadMessages',
'gi.actions/identity/setChatRoomReadUntil',
'gi.actions/identity/addChatRoomUnreadMessage', 'gi.actions/identity/removeChatRoomUnreadMessage'
],
allowedDomains: ['okTurtles.data', 'okTurtles.events', 'okTurtles.eventQueue', 'gi.db', 'gi.contracts'],
preferSlim: true,
Expand Down Expand Up @@ -238,11 +242,13 @@ async function startApp () {
}
},
[NOTIFICATION_TYPE.KV] ([key, data]) {
switch (key) {
case 'lastLoggedIn': {
const rootState = sbp('state/vuex/state')
Vue.set(rootState.lastLoggedIn, data.contractID, data.data)
}
const rootState = sbp('state/vuex/state')
const { contractID, data: value } = data

if (key === KV_KEYS.LAST_LOGGED_IN && value) {
Vue.set(rootState.lastLoggedIn, contractID, value)
} else if (key === KV_KEYS.UNREAD_MESSAGES && value) {
sbp('state/vuex/commit', 'setUnreadMessages', value)
}
}
}
Expand Down Expand Up @@ -405,7 +411,7 @@ async function startApp () {
ourUnreadMessagesCount () {
return Object.keys(this.ourUnreadMessages)
// TODO: need to remove the '|| []' after we release 0.2.*
.map(cId => (this.ourUnreadMessages[cId].messages || []).length)
.map(cId => (this.ourUnreadMessages[cId].unreadMessages || []).length)
.reduce((a, b) => a + b, 0)
},
shouldSetBadge () {
Expand Down
66 changes: 24 additions & 42 deletions frontend/model/chatroom/vuexModule.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@
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, CHATROOM_PRIVACY_LEVEL } from '@model/contracts/shared/constants.js'
import { MESSAGE_NOTIFY_SETTINGS, CHATROOM_PRIVACY_LEVEL } from '@model/contracts/shared/constants.js'
import { KV_KEYS } from '@utils/constants.js'
const defaultState = {
currentChatRoomIDs: {}, // { [groupID]: currentChatRoomId }
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 }
unreadMessages: null, // [chatRoomID]: { readUntil: { messageHash, createdHeight }, unreadMessages: [{ messageHash, createdHeight }]}
chatNotificationSettings: {} // { [chatRoomID]: { messageNotification: MESSAGE_NOTIFY_SETTINGS, messageSound: MESSAGE_NOTIFY_SETTINGS } }
}

// getters
Expand All @@ -27,6 +28,9 @@ const getters = {
}
}, state.chatNotificationSettings || {})
},
ourUnreadMessages (state) {
return state.unreadMessages || {}
},
directMessagesByGroup (state, getters, rootState) {
return groupID => {
const currentGroupDirectMessages = {}
Expand All @@ -53,16 +57,25 @@ const getters = {
// 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]
const lastMsgTimeStamp = chatRoomState.messages?.length > 0
? new Date(chatRoomState.messages[chatRoomState.messages.length - 1].datetime).getTime()
: 0

currentGroupDirectMessages[chatRoomID] = {
...directMessageSettings,
members,
partners,
partners: partners.map(memberID => ({
contractID: memberID,
username: getters.usernameFromID(memberID),
displayName: getters.userDisplayNameFromID(memberID)
})),
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(', '),
lastMsgTimeStamp,
picture: getters.ourContactProfilesById[lastJoinedPartner]?.picture
}
}
Expand All @@ -83,7 +96,7 @@ const getters = {
}
const currentGroupDirectMessages = getters.ourGroupDirectMessages
return Object.keys(currentGroupDirectMessages).find(chatRoomID => {
const cPartners = currentGroupDirectMessages[chatRoomID].partners
const cPartners = currentGroupDirectMessages[chatRoomID].partners.map(partner => partner.contractID)
return cPartners.length === partners.length && union(cPartners, partners).length === partners.length
})
}
Expand All @@ -101,23 +114,14 @@ const getters = {
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)
return getters.ourUnreadMessages[chatRoomID]?.unreadMessages || []
}
},
groupUnreadMessages (state, getters, rootState) {
Expand All @@ -126,7 +130,7 @@ const getters = {
const isGroupChatroom = cID => Object.keys(state[groupID]?.chatRooms || {}).includes(cID)
return Object.keys(getters.ourUnreadMessages)
.filter(cID => isGroupDirectMessage(cID) || isGroupChatroom(cID))
.map(cID => getters.ourUnreadMessages[cID].messages.length)
.map(cID => getters.ourUnreadMessages[cID].unreadMessages.length)
.reduce((sum, n) => sum + n, 0)
}
},
Expand Down Expand Up @@ -193,37 +197,15 @@ const mutations = {
Vue.set(state.currentChatRoomIDs, rootState.currentGroupId, null)
}
},
setUnreadMessages (state, value) {
Vue.set(state, KV_KEYS.UNREAD_MESSAGES, value)
},
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]) {
Expand Down
Loading
Loading