Skip to content

Commit

Permalink
Notifications without Vuex (#2365)
Browse files Browse the repository at this point in the history
* Notifications without Vuex

* Bugfixes

* Notification status

* Notification status

* Feedback

* Add comment

* Fix generic proposals

* Add notification hash to error

* Fix non-deleted notifications
  • Loading branch information
corrideat authored Oct 4, 2024
1 parent 8328cfa commit a8cbe02
Show file tree
Hide file tree
Showing 11 changed files with 85 additions and 23 deletions.
2 changes: 1 addition & 1 deletion docs/Style-Guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@ banner-scoped(ref='formMsg')
```

```js
import L, { LError } from '@view-utils/translations.js'
import { L, LError } from '@view-utils/translations.js'
import BannerScoped from '@components/banners/BannerScoped.vue'

submit () {
Expand Down
14 changes: 5 additions & 9 deletions frontend/controller/actions/group.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ import { VOTE_FOR } from '@model/contracts/shared/voting/rules.js'
import sbp from '@sbp/sbp'
import {
ACCEPTED_GROUP,
LEFT_GROUP,
JOINED_GROUP,
JOINED_CHATROOM,
LEFT_GROUP,
LOGOUT,
REPLACE_MODAL
} from '@utils/events.js'
Expand All @@ -42,18 +42,14 @@ import { CONTRACT_HAS_RECEIVED_KEYS, EVENT_HANDLED } from '~/shared/domains/chel
import type { Key } from '../../../shared/domains/chelonia/crypto.js'
import { CURVE25519XSALSA20POLY1305, EDWARDS25519SHA512BATCH, keyId, keygen, serializeKey } from '../../../shared/domains/chelonia/crypto.js'
import type { GIActionParams } from './types.js'
import { REMOVE_NOTIFICATION } from '~/frontend/model/notifications/mutationKeys.js'
import { createInvite, encryptedAction } from './utils.js'
import { extractProposalData } from '@model/notifications/utils.js'

sbp('okTurtles.events/on', LEFT_GROUP, ({ identityContractID, groupContractID }) => {
const rootState = sbp('state/vuex/state')
if (rootState.loggedIn?.identityContractID !== identityContractID) return
// NOTE: remove all notifications whose scope is in this group
// TODO: FIND ANOTHER WAY OF DOING THIS WITHOUT ROOTGETTERS
for (const notification of sbp('state/vuex/getters').notificationsByGroup(groupContractID)) {
sbp('state/vuex/commit', REMOVE_NOTIFICATION, notification.hash)
}
const rootState = sbp('chelonia/rootState')
if (!rootState.notifications || rootState.loggedIn?.identityContractID !== identityContractID) return
const notificationHashes = rootState.notifications.items.filter(item => item.groupID === groupContractID).map(item => item.hash)
sbp('gi.notifications/remove', notificationHashes)
})

export default (sbp('sbp/selectors/register', {
Expand Down
4 changes: 2 additions & 2 deletions frontend/controller/actions/identity-kv.js
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,7 @@ export default (sbp('sbp/selectors/register', {
'gi.actions/identity/kv/loadNotificationStatus': () => {
return sbp('okTurtles.eventQueue/queueEvent', KV_QUEUE, async () => {
const status = await sbp('gi.actions/identity/kv/fetchNotificationStatus')
sbp('state/vuex/commit', 'setNotificationStatus', status)
sbp('gi.notifications/setNotificationStatus', status)
})
},
'gi.actions/identity/kv/addNotificationStatus': (notification: Object) => {
Expand Down Expand Up @@ -279,7 +279,7 @@ export default (sbp('sbp/selectors/register', {
hashes = [hashes]
}
return sbp('okTurtles.eventQueue/queueEvent', KV_QUEUE, async () => {
const { notifications } = sbp('state/vuex/getters')
const notifications = sbp('chelonia/rootState').notifications.items
const getUpdatedNotificationStatus = async () => {
const currentData = await sbp('gi.actions/identity/kv/fetchNotificationStatus')
let isUpdated = false
Expand Down
4 changes: 4 additions & 0 deletions frontend/model/contracts/group.js
Original file line number Diff line number Diff line change
Expand Up @@ -1680,6 +1680,10 @@ sbp('chelonia/defineContract', {
}

if (memberID === identityContractID) {
// NOTE: removing all notifications whose scope is in this group
// is handled by the LEFT_GROUP event, which is emitted by the
// identity contract.

// The following detects whether we're in the process of joining, and if
// we are, it doesn't remove the contract and calls /join to complete
// the joining process.
Expand Down
48 changes: 42 additions & 6 deletions frontend/model/notifications/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@

import sbp from '@sbp/sbp'
import type { Notification, NotificationData, NotificationTemplate } from './types.flow.js'
import * as keys from './mutationKeys.js'
import templates from './templates.js'
import { makeNotificationHash } from './utils.js'
import { CHELONIA_STATE_MODIFIED, NOTIFICATION_EMITTED, NOTIFICATION_REMOVED, NOTIFICATION_STATUS_LOADED } from '~/frontend/utils/events.js'

/*
* NOTE: do not refactor occurences of `sbp('state/vuex/state')` by defining a shared constant in the
Expand Down Expand Up @@ -39,17 +39,53 @@ sbp('sbp/selectors/register', {
timestamp: data.createdDate ? new Date(data.createdDate).getTime() : Date.now(),
type
}
const rootState = sbp('chelonia/rootState')
if (!rootState.notifications) {
rootState.notifications = { items: [], status: {} }
}
if (rootState.notifications.items.some(item => item.hash === notification.hash)) {
// We cannot throw here, as this code might be called from within a contract side effect.
return console.error('[gi.notifications/emit] This notification is already in the store.', notification.hash)
}
const index = rootState.notifications.items.findLastIndex(item => item.timestamp < notification.timestamp)
rootState.notifications.items.splice(Math.max(0, index), 0, notification)
sbp('okTurtles.events/emit', CHELONIA_STATE_MODIFIED)
sbp('gi.actions/identity/kv/addNotificationStatus', notification)
sbp('state/vuex/commit', keys.ADD_NOTIFICATION, notification)
sbp('okTurtles.events/emit', NOTIFICATION_EMITTED, notification)
},
'gi.notifications/markAsRead' (notification: Notification) {
sbp('gi.actions/identity/kv/markNotificationStatusRead', notification.hash)
},
'gi.notifications/markAllAsRead' (groupID: string) {
const notifications = groupID
? sbp('state/vuex/getters').unreadGroupNotificationsFor(groupID)
: sbp('state/vuex/getters').currentUnreadNotifications
const hashes = notifications.map(item => item.hash)
const rootState = sbp('chelonia/rootState')
if (!rootState.notifications) return
const hashes = rootState.notifications.items.filter(item => {
return !item.read && (!groupID || !item.groupID || item.groupID === groupID)
}).map(item => item.hash)
sbp('gi.actions/identity/kv/markNotificationStatusRead', hashes)
},
'gi.notifications/remove' (hashes: string | string[]) {
if (!Array.isArray(hashes)) hashes = [hashes]
const rootState = sbp('chelonia/rootState')
if (!rootState.notifications) return
const hashesSet = new Set(hashes)
const indices = rootState.notifications.items.map((item, index) => {
if (hashesSet.has(item.hash)) {
hashesSet.delete(item.hash)
return index
}
return false
}).filter((v) => v !== false).sort().map((v, i) => v - i)
indices.forEach((index) => rootState.notifications.items.splice(index, 1))
sbp('okTurtles.events/emit', NOTIFICATION_REMOVED, hashes)
},
'gi.notifications/setNotificationStatus' (status) {
const rootState = sbp('chelonia/rootState')
if (!rootState.notifications) {
rootState.notifications = { items: [], status: {} }
}
rootState.notifications.status = status
sbp('okTurtles.events/emit', CHELONIA_STATE_MODIFIED)
sbp('okTurtles.events/emit', NOTIFICATION_STATUS_LOADED, status)
}
})
16 changes: 15 additions & 1 deletion frontend/model/notifications/vuexModule.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,26 @@
'use strict'

import sbp from '@sbp/sbp'
import Vue from 'vue'
import { cloneDeep } from '~/frontend/model/contracts/shared/giLodash.js'
import * as keys from './mutationKeys.js'
import './selectors.js'
import { MAX_AGE_READ, MAX_AGE_UNREAD } from './storageConstants.js'
import type { Notification } from './types.flow.js'
import { age, compareOnTimestamp, isNew, isOlder } from './utils.js'
import { NOTIFICATION_EMITTED, NOTIFICATION_REMOVED, NOTIFICATION_STATUS_LOADED } from '~/frontend/utils/events.js'

sbp('okTurtles.events/on', NOTIFICATION_EMITTED, (notification) => {
sbp('state/vuex/commit', keys.ADD_NOTIFICATION, notification)
})

sbp('okTurtles.events/on', NOTIFICATION_REMOVED, (hashes) => {
hashes.forEach(hash => sbp('state/vuex/commit', keys.REMOVE_NOTIFICATION, hash))
})

sbp('okTurtles.events/on', NOTIFICATION_STATUS_LOADED, (status) => {
sbp('state/vuex/commit', 'setNotificationStatus', status)
})

const defaultState = {
items: [], status: {}
Expand Down Expand Up @@ -101,7 +115,7 @@ const mutations = {
[keys.ADD_NOTIFICATION] (state, notification: Notification) {
if (state.items.some(item => item.hash === notification.hash)) {
// We cannot throw here, as this code might be called from within a contract side effect.
return console.error('This notification is already in the store.')
return console.error('[ADD_NOTIFICATION] This notification is already in the store.', notification.hash)
}
state.items.push(notification)
// Sort items in chronological order, newest items first.
Expand Down
8 changes: 7 additions & 1 deletion frontend/setupChelonia.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { groupContractsByType, syncContractsInOrder } from './controller/actions
import { PUBSUB_INSTANCE } from './controller/instance-keys.js'
import manifests from './model/contracts/manifests.json'
import { SETTING_CHELONIA_STATE, SETTING_CURRENT_USER } from './model/database.js'
import { CHATROOM_USER_STOP_TYPING, CHATROOM_USER_TYPING, KV_EVENT, LOGIN_COMPLETE, LOGOUT, OFFLINE, ONLINE, RECONNECTING, RECONNECTION_FAILED } from './utils/events.js'
import { CHATROOM_USER_STOP_TYPING, CHATROOM_USER_TYPING, CHELONIA_STATE_MODIFIED, KV_EVENT, LOGIN_COMPLETE, LOGOUT, OFFLINE, ONLINE, RECONNECTING, RECONNECTION_FAILED } from './utils/events.js'

// This function is tasked with most common tasks related to setting up Chelonia
// for Group Income. If Chelonia is running in a service worker, the service
Expand Down Expand Up @@ -183,6 +183,12 @@ const setupChelonia = async (): Promise<*> => {
})
})

sbp('okTurtles.events/on', CHELONIA_STATE_MODIFIED, () => {
saveChelonia().catch(e => {
console.error('CHELONIA_STATE_MODIFIED handler: Error saving Chelonia state', e)
})
})

sbp('okTurtles.events/on', LOGOUT, () => {
// TODO: [SW] This is to be done by the SW
logoutInProgress = true
Expand Down
6 changes: 6 additions & 0 deletions frontend/utils/events.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,9 @@ export const NAMESPACE_REGISTRATION = 'namespace-registration'
export const KV_QUEUE = 'kv-queue'

export const PWA_INSTALLABLE = 'pwa-installable'

export const CHELONIA_STATE_MODIFIED = 'chelonia-state-modified'

export const NOTIFICATION_EMITTED = 'notification-emitted'
export const NOTIFICATION_REMOVED = 'notification-removed'
export const NOTIFICATION_STATUS_LOADED = 'notification-status-loaded'
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export default {
this.toggleTooltip()
},
markAllNotificationsAsRead () {
sbp('gi.notifications/markAllAsRead')
sbp('gi.notifications/markAllAsRead', this.currentGroupId)
}
},
computed: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export default {
})
},
markAllNotificationsAsRead () {
sbp('gi.notifications/markAllAsRead')
sbp('gi.notifications/markAllAsRead', this.currentGroupId)
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion frontend/views/containers/proposals/GenericProposal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ import Tooltip from '@components/Tooltip.vue'
import BannerScoped from '@components/banners/BannerScoped.vue'
import { validationMixin } from 'vuelidate'
import { required } from 'vuelidate/lib/validators'
import L from '~/frontend/common/translations.js'
import { L } from '~/frontend/common/translations.js'
import validationsDebouncedMixins from '@view-utils/validationsDebouncedMixins.js'
export default ({
Expand Down

0 comments on commit a8cbe02

Please sign in to comment.