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

Chelonia in SW #2357

Open
wants to merge 41 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
9b89d48
Chelonia in SW
corrideat Sep 19, 2024
bce3c5c
Debug
corrideat Sep 22, 2024
4601d99
Merge branch 'master' into feature/chelonia-in-service-worker
corrideat Sep 29, 2024
15d5f86
Merge branch 'master' into feature/chelonia-in-service-worker
corrideat Oct 4, 2024
2861605
Test fixes
corrideat Oct 6, 2024
80b1385
Fix group-chat-direct-message spec
corrideat Oct 6, 2024
693173f
Fix issue with the no-results slot being (incorrectly) used
corrideat Oct 6, 2024
ba57fb4
Changes supporting failing chat tests
corrideat Oct 7, 2024
13b4357
Fix attachments in SW
corrideat Oct 9, 2024
0a90ec4
Fix Flow fypes
corrideat Oct 13, 2024
22a0f6e
Use atomic for chatroom members
corrideat Oct 13, 2024
e5f8ed2
Chat bugfixes
corrideat Oct 14, 2024
286570a
Merge branch 'master' into feature/chelonia-in-service-worker
corrideat Oct 15, 2024
ab7d69d
Merge branch 'master' into feature/chelonia-in-service-worker
corrideat Oct 17, 2024
3c78f54
Use session storage for tab logs. Refactor logging to be more generic.
corrideat Oct 17, 2024
c8a4441
Bugfixes and removal of unnecessary selectors
corrideat Oct 17, 2024
17f3e77
SW logs
corrideat Oct 19, 2024
fe4521a
Bugfix for loading preferences (fix event handlers)
corrideat Oct 20, 2024
870d9a6
State for KV events
corrideat Oct 20, 2024
001e813
Serious banner error
corrideat Oct 20, 2024
48db75e
More consistent chelonia / vuex state use
corrideat Oct 20, 2024
d99c1e0
Remove debug logging
corrideat Oct 20, 2024
90460e7
Merge branch 'master' into feature/chelonia-in-service-worker
corrideat Oct 21, 2024
279e171
Logs UI
corrideat Oct 23, 2024
933957b
Merge branch 'master' into feature/chelonia-in-service-worker
corrideat Oct 23, 2024
ce94156
Lint
corrideat Oct 23, 2024
bdd0f84
Autologout for non-exisiting identity contracts
corrideat Oct 23, 2024
218dd9b
Merge branch 'master' into feature/chelonia-in-service-worker
corrideat Oct 24, 2024
aa12e08
Safari workaround
corrideat Oct 24, 2024
fd77e0b
Bugfix
corrideat Oct 24, 2024
18046a7
Bugfixes
corrideat Oct 25, 2024
bbb7277
Logout flow fixes
corrideat Oct 25, 2024
f7bc3d4
Last logged in event
corrideat Oct 25, 2024
b08b48b
Avoid SW logs spam
corrideat Oct 25, 2024
0f55f24
Chatroom position events
corrideat Oct 25, 2024
332e8cb
Feedback
corrideat Oct 27, 2024
6fd370f
WIP
corrideat Oct 28, 2024
fe4e55d
Stability bugfixes
corrideat Nov 2, 2024
ca116a5
Merge branch 'master' into feature/chelonia-in-service-worker
corrideat Nov 2, 2024
e4f40bc
Port notifications code
corrideat Nov 3, 2024
74b2dfa
Backend functions for sending
corrideat Nov 3, 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
85 changes: 50 additions & 35 deletions frontend/controller/actions/chatroom.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,16 @@ sbp('okTurtles.events/on', MESSAGE_RECEIVE_RAW, ({
// If newMessage is undefined, it means that an existing message is being edited
newMessage
}) => {
const getters = sbp('state/vuex/getters')
const mentions = makeMentionFromUserID(getters.ourIdentityContractId)
const state = sbp('chelonia/contract/state', contractID)
const rootState = sbp('chelonia/rootState')
const mentions = makeMentionFromUserID(rootState.loggedIn?.identityContractID)
const msgData = newMessage || data
const isMentionedMe = (!!newMessage || data.type === MESSAGE_TYPES.TEXT) && msgData.text &&
(msgData.text.includes(mentions.me) || msgData.text.includes(mentions.all))

if (!newMessage) {
const isAlreadyAdded = !!getters
.chatRoomUnreadMessages(contractID).find(m => m.messageHash === data.hash)
// TODO: rootState.unreadMessages for SW
const isAlreadyAdded = rootState.chatroom?.unreadMessages?.[contractID]?.unreadMessages.find(m => m.messageHash === data.hash)
Copy link
Member

Choose a reason for hiding this comment

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

Is it possible to keep the getter?

Copy link
Member Author

Choose a reason for hiding this comment

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

The reason that the getter isn't being used here is that although I added a wrapper for root getters, these are Vuex modules, which work a bit differently due to state partitioning. At the time, the separate state keys being used by the module weren't there. Now they are, and it would be feasible to similarly have this work.

While I think that in this instance it'd be possible to use a getter, many getters are written to rely on curentXXXId, which is problematic in the SW.


if (isAlreadyAdded && !isMentionedMe) {
sbp('gi.actions/identity/kv/removeChatRoomUnreadMessage', { contractID, messageHash: data.hash })
Expand All @@ -42,10 +43,10 @@ sbp('okTurtles.events/on', MESSAGE_RECEIVE_RAW, ({
messageHash: msgData.hash,
height: msgData.height,
text: msgData.text,
isDMOrMention: isMentionedMe || getters.chatRoomAttributes.type === CHATROOM_TYPES.DIRECT_MESSAGE,
isDMOrMention: isMentionedMe || state.attributes?.type === CHATROOM_TYPES.DIRECT_MESSAGE,
messageType: !newMessage ? MESSAGE_TYPES.TEXT : data.type,
memberID: innerSigningContractID,
chatRoomName: getters.chatRoomAttributes.name
chatRoomName: state.attributes?.name
Comment on lines +47 to +50
Copy link
Member

Choose a reason for hiding this comment

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

What is preventing us from moving the getter definitions into a file that is shared by the frontend and the SW?

That would be much preferable to this.

Copy link
Member Author

Choose a reason for hiding this comment

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

Chatroom getters are a Vuex module, which are harder to emulate without Vuex because of state partitioning. In addition, those getters rely on currentGroupId and currentChatroomId which can't be used in contracts.

Copy link
Member

@taoeffect taoeffect Oct 10, 2024

Choose a reason for hiding this comment

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

Is there nothing creative that's possible here? We can't define getters in a way that has them referring to the "the chatroom that corresponds to this state"?

i.e. "state paritioned getters" ala gettersProxy? (see bottom of chelonia.js)

Copy link
Member Author

@corrideat corrideat Oct 10, 2024

Choose a reason for hiding this comment

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

the chatroom that corresponds to this state

How would that even be possible the way getters are defined? I don't really see an issue with the lines highlighted, and I'm not even sure why we'd need a getter that returns a single property.

But going to the definition:

  chatRoomAttributes (state, getters) {
    return getters.currentChatRoomState.attributes || {}
  },

Sure, in this instance state corresponds to getters.currentChatRoomState, but I don't see how generically we can know what getters.currentChatRoomState should be (generically). In the app, it works because (remember this is a global getter) there is a currentChatRoomId. In contracts it works, because currentChatRoomState can be defined to return 'state'. In the SW, the situation is analogous to the app, but there's no currentChatRoomId.

Now, this could maybe work by having a getter that takes the contractID as a function parameter, but this requires refactoring and adds complexity. I also think such getters that are simply accessors should be avoided, as they increase code line count and complexity for no benefit.

Copy link
Member Author

@corrideat corrideat Oct 10, 2024

Choose a reason for hiding this comment

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

Thinking about it, another solution along the lines of what you suggested could be something like:

const getters = sbp('chelonia/contract/getters', 'gi.contracts/chatroom', state)
getters.currentChatRoomState

But honestly, for simple accessors such as these I don't see the benefit of adding this complexity. This also adds two other factors:

  1. If the getters are hardcoded (as in, statically imported), it increases bundle size
  2. If the getters are dynamic, they'll probably need to be async (in case the contract isn't loaded) and because of sandboxing (maybe).

Copy link
Member

Choose a reason for hiding this comment

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

@corrideat would it be possible to instantiate dynamically a new batch of getters where getters. currentChatRoomState returns a specific contract state in the SW using some sort of factory function?

I also think such getters that are simply accessors should be avoided, as they increase code line count and complexity for no benefit.

Getters are very important, as they solve a lot of DRY related problems. By using getters we can ensure that the way that data is accessed everywhere will have consistent return values (e.g. because it uses tricks like || {} etc.). Whereas those considerations would have to be thought of each time you access state data directly.

Copy link
Member

@taoeffect taoeffect Oct 10, 2024

Choose a reason for hiding this comment

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

const getters = sbp('chelonia/contract/getters', 'gi.contracts/chatroom', state)

Yeah, that's a great idea!

I'd make one slight modification: instead of passing in state, passing in contractID:

const getters = sbp('chelonia/contract/getters', contractID)

(EDIT: I don't think you need to pass in the name, that can be retrieved using contractID if needed)

Then yeah, at least we could reuse the contract getters that are already defined in the contracts!

Copy link
Member Author

Choose a reason for hiding this comment

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

Update based on today's conversation: we could use getters synchronously by importing the contract getters from this actions file. However, it'd have the downside of being 'statically hardcoded' based on whatever version is imported at build time and will increase bundle size. Doing it via Chelonia is potentially possible, but it'd need to be async in case the contract isn't cached and need to be loading and also maybe because of sandboxing.

}).catch(e => {
console.error('[action/chatroom.js] Error on messageReceivePostEffect', e)
})
Expand Down Expand Up @@ -212,8 +213,7 @@ export default (sbp('sbp/selectors/register', {
}
},
'gi.actions/chatroom/shareNewKeys': (contractID: string, newKeys) => {
const rootState = sbp('state/vuex/state')
const state = rootState[contractID]
const state = sbp('chelonia/contract/state', contractID)

const originatingContractID = state.attributes.groupContractID ? state.attributes.groupContractID : contractID

Expand Down Expand Up @@ -251,43 +251,58 @@ export default (sbp('sbp/selectors/register', {
...encryptedAction('gi.actions/chatroom/join', L('Failed to join chat channel.'), async (sendMessage, params, signingKeyId) => {
const rootState = sbp('state/vuex/state')
const userID = params.data.memberID || rootState.loggedIn.identityContractID
const userIDs = Array.isArray(userID)
? userID.map(x => x == null ? rootState.loggedIn.identityContractID : x)
: [userID]

// We need to read values from both the chatroom and the identity contracts'
// state, so we call wait to run the rest of this function after all
// operations in those contracts have completed
await sbp('chelonia/contract/wait', [params.contractID, userID])
await sbp('chelonia/contract/retain', userIDs, { ephemeral: true })
try {
await sbp('chelonia/contract/wait', params.contractID)

if (!userID || !has(rootState.contracts, userID)) {
throw new Error(`Unable to send gi.actions/chatroom/join on ${params.contractID} because user ID contract ${userID} is missing`)
}
userIDs.forEach(cID => {
if (!cID || !has(rootState.contracts, cID) || !has(rootState, cID)) {
throw new Error(`Unable to send gi.actions/chatroom/join on ${params.contractID} because user ID contract ${cID} is missing`)
}
})

const CEKid = params.encryptionKeyId || await sbp('chelonia/contract/currentKeyIdByName', params.contractID, 'cek')
const CEKid = params.encryptionKeyId || await sbp('chelonia/contract/currentKeyIdByName', params.contractID, 'cek')

const userCSKid = sbp('chelonia/contract/currentKeyIdByName', userID, 'csk')
return await sbp('chelonia/out/atomic', {
...params,
contractName: 'gi.contracts/chatroom',
data: [
const userCSKids = await Promise.all(userIDs.map(async (cID) =>
[cID, await sbp('chelonia/contract/currentKeyIdByName', cID, 'csk')]
))
return await sbp('chelonia/out/atomic', {
...params,
contractName: 'gi.contracts/chatroom',
data: [
// Add the user's CSK to the contract
[
'chelonia/out/keyAdd', {
[
'chelonia/out/keyAdd', {
// TODO: Find a way to have this wrapping be done by Chelonia directly
data: [encryptedOutgoingData(params.contractID, CEKid, {
foreignKey: `sp:${encodeURIComponent(userID)}?keyName=${encodeURIComponent('csk')}`,
id: userCSKid,
data: rootState[userID]._vm.authorizedKeys[userCSKid].data,
permissions: [GIMessage.OP_ACTION_ENCRYPTED + '#inner'],
allowedActions: '*',
purpose: ['sig'],
ringLevel: Number.MAX_SAFE_INTEGER,
name: `${userID}/${userCSKid}`
})]
}
data: userCSKids.map(([cID, cskID]: [string, string]) => encryptedOutgoingData(params.contractID, CEKid, {
foreignKey: `sp:${encodeURIComponent(cID)}?keyName=${encodeURIComponent('csk')}`,
id: cskID,
data: rootState[cID]._vm.authorizedKeys[cskID].data,
permissions: [GIMessage.OP_ACTION_ENCRYPTED + '#inner'],
allowedActions: '*',
purpose: ['sig'],
ringLevel: Number.MAX_SAFE_INTEGER,
name: `${cID}/${cskID}`
}))
}
],
...(Array.isArray(userID)
? userID.map(cID => sendMessage({ ...params, data: { memberID: cID }, returnInvocation: true }))
: [sendMessage({ ...params, returnInvocation: true })]
)
],
sendMessage({ ...params, returnInvocation: true })
],
signingKeyId
})
signingKeyId
})
} finally {
await sbp('chelonia/contract/release', userIDs, { ephemeral: true })
}
}),
...encryptedAction('gi.actions/chatroom/rename', L('Failed to rename chat channel.')),
...encryptedAction('gi.actions/chatroom/changeDescription', L('Failed to change chat channel description.')),
Expand Down
3 changes: 1 addition & 2 deletions frontend/controller/actions/group-kv.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,12 @@ export default (sbp('sbp/selectors/register', {
})
},
'gi.actions/group/kv/updateLastLoggedIn': ({ contractID, throttle }: { contractID: string, throttle: boolean }) => {
const identityContractID = sbp('chelonia/rootState').loggedIn?.identityContractID
const identityContractID = sbp('state/vuex/state').loggedIn?.identityContractID
if (!identityContractID) {
throw new Error('Unable to update lastLoggedIn without an active session')
}

if (throttle) {
// TODO: Can't use state/vuex/state
const state = sbp('state/vuex/state')
const lastLoggedIn = new Date(state.lastLoggedIn[contractID]?.[identityContractID]).getTime()

Expand Down
8 changes: 4 additions & 4 deletions frontend/controller/actions/group.js
Original file line number Diff line number Diff line change
Expand Up @@ -527,7 +527,7 @@ export default (sbp('sbp/selectors/register', {
})
},
'gi.actions/group/joinWithInviteSecret': async function (groupId: string, secret: string) {
const identityContractID = sbp('chelonia/rootState').loggedIn.identityContractID
const identityContractID = sbp('state/vuex/state').loggedIn.identityContractID

// This action (`joinWithInviteSecret`) can get invoked while there are
// events being processed in the group or identity contracts. This can cause
Expand Down Expand Up @@ -866,8 +866,8 @@ export default (sbp('sbp/selectors/register', {
// inside of the exception handler :-(
}
},
'gi.actions/group/notifyProposalStateInGeneralChatRoom': function ({ groupID, proposal }: { groupID: string, proposal: Object }) {
const { generalChatRoomId } = sbp('chelonia/rootState')[groupID]
'gi.actions/group/notifyProposalStateInGeneralChatRoom': async function ({ groupID, proposal }: { groupID: string, proposal: Object }) {
const { generalChatRoomId } = await sbp('chelonia/contract/state', groupID)
return sbp('gi.actions/chatroom/addMessage', {
contractID: generalChatRoomId,
data: { type: MESSAGE_TYPES.INTERACTIVE, proposal }
Expand Down Expand Up @@ -979,7 +979,7 @@ export default (sbp('sbp/selectors/register', {
},
...encryptedAction('gi.actions/group/leaveChatRoom', L('Failed to leave chat channel.'), async (sendMessage, params) => {
const state = await sbp('chelonia/contract/state', params.contractID)
const memberID = params.data.memberID || sbp('chelonia/rootState').loggedIn.identityContractID
const memberID = params.data.memberID || sbp('state/vuex/state').loggedIn.identityContractID
const joinedHeight = state.chatRooms[params.data.chatRoomID].members[memberID].joinedHeight

// For more efficient and correct processing, augment the leaveChatRoom
Expand Down
20 changes: 9 additions & 11 deletions frontend/controller/actions/identity-kv.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use strict'
import sbp from '@sbp/sbp'
import { KV_KEYS } from '~/frontend/utils/constants.js'
import { KV_QUEUE, ONLINE } from '~/frontend/utils/events.js'
import { KV_QUEUE, NEW_PREFERENCES, NEW_UNREAD_MESSAGES, ONLINE } from '~/frontend/utils/events.js'
import { isExpired } from '@model/notifications/utils.js'

const initNotificationStatus = (data = {}) => ({ ...data, read: false })
Expand All @@ -25,14 +25,14 @@ export default (sbp('sbp/selectors/register', {
// Using 'chelonia/rootState' here as 'state/vuex/state' is not available
// in the SW, and because, even without a SW, 'loggedIn' is not yet there
// in Vuex state when logging in
const identityContractID = sbp('chelonia/rootState').loggedIn?.identityContractID
const identityContractID = sbp('state/vuex/state').loggedIn?.identityContractID
if (!identityContractID) {
throw new Error('Unable to fetch chatroom unreadMessages without an active session')
}
return (await sbp('chelonia/kv/get', identityContractID, KV_KEYS.UNREAD_MESSAGES))?.data || {}
},
'gi.actions/identity/kv/saveChatRoomUnreadMessages': ({ data, onconflict }: { data: Object, onconflict?: Function }) => {
const identityContractID = sbp('chelonia/rootState').loggedIn?.identityContractID
const identityContractID = sbp('state/vuex/state').loggedIn?.identityContractID
if (!identityContractID) {
throw new Error('Unable to update chatroom unreadMessages without an active session')
}
Expand All @@ -51,8 +51,7 @@ export default (sbp('sbp/selectors/register', {
'gi.actions/identity/kv/loadChatRoomUnreadMessages': () => {
return sbp('okTurtles.eventQueue/queueEvent', KV_QUEUE, async () => {
const currentChatRoomUnreadMessages = await sbp('gi.actions/identity/kv/fetchChatRoomUnreadMessages')
// TODO: Can't use state/vuex/commit
sbp('state/vuex/commit', 'setUnreadMessages', currentChatRoomUnreadMessages)
sbp('okTurtles.events/emit', NEW_UNREAD_MESSAGES, currentChatRoomUnreadMessages)
})
},
'gi.actions/identity/kv/initChatRoomUnreadMessages': ({ contractID, messageHash, createdHeight }: {
Expand Down Expand Up @@ -170,14 +169,14 @@ export default (sbp('sbp/selectors/register', {
},
// Preferences
'gi.actions/identity/kv/fetchPreferences': async () => {
const identityContractID = sbp('chelonia/rootState').loggedIn?.identityContractID
const identityContractID = sbp('state/vuex/state').loggedIn?.identityContractID
if (!identityContractID) {
throw new Error('Unable to fetch preferences without an active session')
}
return (await sbp('chelonia/kv/get', identityContractID, KV_KEYS.PREFERENCES))?.data || {}
},
'gi.actions/identity/kv/savePreferences': ({ data, onconflict }: { data: Object, onconflict?: Function }) => {
const identityContractID = sbp('chelonia/rootState').loggedIn?.identityContractID
const identityContractID = sbp('state/vuex/state').loggedIn?.identityContractID
if (!identityContractID) {
throw new Error('Unable to update preferences without an active session')
}
Expand All @@ -192,8 +191,7 @@ export default (sbp('sbp/selectors/register', {
'gi.actions/identity/kv/loadPreferences': () => {
return sbp('okTurtles.eventQueue/queueEvent', KV_QUEUE, async () => {
const preferences = await sbp('gi.actions/identity/kv/fetchPreferences')
// TODO: Can't use state/vuex/commit
sbp('state/vuex/commit', 'setPreferences', preferences)
sbp('okTurtles.events/emit', NEW_PREFERENCES, preferences)
})
},
'gi.actions/identity/kv/updateDistributionBannerVisibility': ({ contractID, hidden }: { contractID: string, hidden: boolean }) => {
Expand All @@ -213,14 +211,14 @@ export default (sbp('sbp/selectors/register', {
},
// Notifications
'gi.actions/identity/kv/fetchNotificationStatus': async () => {
const identityContractID = sbp('chelonia/rootState').loggedIn?.identityContractID
const identityContractID = sbp('state/vuex/state').loggedIn?.identityContractID
if (!identityContractID) {
throw new Error('Unable to fetch notification status without an active session')
}
return (await sbp('chelonia/kv/get', identityContractID, KV_KEYS.NOTIFICATIONS))?.data || {}
},
'gi.actions/identity/kv/saveNotificationStatus': ({ data, onconflict }: { data: Object, onconflict?: Function }) => {
const identityContractID = sbp('chelonia/rootState').loggedIn?.identityContractID
const identityContractID = sbp('state/vuex/state').loggedIn?.identityContractID
if (!identityContractID) {
throw new Error('Unable to update notification status without an active session')
}
Expand Down
48 changes: 29 additions & 19 deletions frontend/controller/actions/identity.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,11 @@ import { SETTING_CHELONIA_STATE } from '@model/database.js'
import sbp from '@sbp/sbp'
import { imageUpload, objectURLtoBlob } from '@utils/image.js'
import { SETTING_CURRENT_USER } from '~/frontend/model/database.js'
import { KV_QUEUE, LOGIN, LOGOUT } from '~/frontend/utils/events.js'
import { JOINED_CHATROOM, KV_QUEUE, LOGIN, LOGOUT } from '~/frontend/utils/events.js'
import { GIMessage } from '~/shared/domains/chelonia/GIMessage.js'
import { Secret } from '~/shared/domains/chelonia/Secret.js'
import { encryptedOutgoingData, encryptedOutgoingDataWithRawKey } from '~/shared/domains/chelonia/encryptedData.js'
import { EVENT_HANDLED } from '~/shared/domains/chelonia/events.js'
// Using relative path to crypto.js instead of ~-path to workaround some esbuild bug
import type { Key } from '../../../shared/domains/chelonia/crypto.js'
import { CURVE25519XSALSA20POLY1305, EDWARDS25519SHA512BATCH, deserializeKey, keyId, keygen, serializeKey } from '../../../shared/domains/chelonia/crypto.js'
Expand Down Expand Up @@ -244,6 +245,10 @@ export default (sbp('sbp/selectors/register', {
console.debug('[gi.actions/identity/login] Scheduled call starting', identityContractID)
transientSecretKeys = transientSecretKeys.map(k => ({ key: deserializeKey(k.valueOf()), transient: true }))

// If running in a SW, start log capture here
if (typeof WorkerGlobalScope === 'function') {
await sbp('swLogs/startCapture', identityContractID)
}
await sbp('chelonia/reset', { ...cheloniaState, loggedIn: { identityContractID } })
await sbp('chelonia/storeSecretKeys', new Secret(transientSecretKeys))

Expand Down Expand Up @@ -403,15 +408,18 @@ export default (sbp('sbp/selectors/register', {
}
// Clear the file cache when logging out to preserve privacy
sbp('gi.db/filesCache/clear').catch((e) => { console.error('Error clearing file cache', e) })
// If running inside a SW, clear logs
if (typeof WorkerGlobalScope === 'function') {
// clear stored logs to prevent someone else accessing sensitve data
sbp('swLogs/pauseCapture', { wipeOut: true }).catch((e) => { console.error('Error clearing file cache', e) })
}
sbp('okTurtles.events/emit', LOGOUT)
return cheloniaState
},
'gi.actions/identity/addJoinDirectMessageKey': async (contractID, foreignContractID, keyName) => {
const keyId = await sbp('chelonia/contract/currentKeyIdByName', foreignContractID, keyName)
const CEKid = await sbp('chelonia/contract/currentKeyIdByName', contractID, 'cek')

const rootState = sbp('state/vuex/state')
const foreignContractState = rootState[foreignContractID]
const foreignContractState = sbp('chelonia/contract/state', foreignContractID)

const existingForeignKeys = await sbp('chelonia/contract/foreignKeysByContractID', contractID, foreignContractID)

Expand All @@ -438,7 +446,7 @@ export default (sbp('sbp/selectors/register', {
})
},
'gi.actions/identity/shareNewPEK': async (contractID: string, newKeys) => {
const rootState = sbp('state/vuex/state')
const rootState = sbp('chelonia/rootState')
const state = rootState[contractID]
// TODO: Also share PEK with DMs
await Promise.all(Object.keys(state.groups || {}).filter(groupID => !state.groups[groupID].hasLeft && !!rootState.contracts[groupID]).map(async groupID => {
Expand Down Expand Up @@ -491,15 +499,12 @@ export default (sbp('sbp/selectors/register', {
...encryptedAction('gi.actions/identity/setAttributes', L('Failed to set profile attributes.'), undefined, 'pek'),
...encryptedAction('gi.actions/identity/updateSettings', L('Failed to update profile settings.')),
...encryptedAction('gi.actions/identity/createDirectMessage', L('Failed to create a new direct message channel.'), async function (sendMessage, params) {
const rootState = sbp('state/vuex/state')
// TODO: Can't use rootGetters
const rootGetters = sbp('state/vuex/getters')
const partnerIDs = params.data.memberIDs
.filter(memberID => memberID !== rootGetters.ourIdentityContractId)
.map(memberID => rootGetters.ourContactProfilesById[memberID].contractID)
// NOTE: 'rootState.currentGroupId' could be changed while waiting for the sbp functions to be proceeded
// So should save it as a constant variable 'currentGroupId', and use it which can't be changed
const currentGroupId = rootState.currentGroupId
const currentGroupId = params.data.currentGroupId
const identityContractID = rootGetters.ourIdentityContractId

const message = await sbp('gi.actions/chatroom/create', {
data: {
Expand All @@ -514,11 +519,11 @@ export default (sbp('sbp/selectors/register', {
prepublish: params.hooks?.prepublish,
postpublish: null
}
}, rootState.loggedIn.identityContractID)
}, identityContractID)

// Share the keys to the newly created chatroom with ourselves
await sbp('gi.actions/out/shareVolatileKeys', {
contractID: rootState.loggedIn.identityContractID,
contractID: identityContractID,
contractName: 'gi.contracts/identity',
subjectContractID: message.contractID(),
keyIds: '*'
Expand All @@ -527,16 +532,19 @@ export default (sbp('sbp/selectors/register', {
await sbp('gi.actions/chatroom/join', {
...omit(params, ['options', 'contractID', 'data', 'hooks']),
contractID: message.contractID(),
data: {}
// 'undefined' is for ourselves
data: { memberID: [undefined, ...partnerIDs] }
})

for (const partnerID of partnerIDs) {
await sbp('gi.actions/chatroom/join', {
...omit(params, ['options', 'contractID', 'data', 'hooks']),
contractID: message.contractID(),
data: { memberID: partnerID }
})
const switchChannelAfterJoined = (contractID: string) => {
if (contractID === message.contractID()) {
if (sbp('chelonia/contract/state', message.contractID())?.members?.[identityContractID]) {
sbp('okTurtles.events/emit', JOINED_CHATROOM, { identityContractID, groupContractID: currentGroupId, chatRoomID: message.contractID() })
sbp('okTurtles.events/off', EVENT_HANDLED, switchChannelAfterJoined)
}
}
}
sbp('okTurtles.events/on', EVENT_HANDLED, switchChannelAfterJoined)

await sendMessage({
...omit(params, ['options', 'data', 'action', 'hooks']),
Expand Down Expand Up @@ -576,6 +584,8 @@ export default (sbp('sbp/selectors/register', {
hooks
})
}

return message.contractID()
}),
...encryptedAction('gi.actions/identity/joinDirectMessage', L('Failed to join a direct message.')),
...encryptedAction('gi.actions/identity/joinGroup', L('Failed to join a group.')),
Expand Down
Loading
Loading