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

Improve to manage notifications and proposals #2046

Merged
merged 16 commits into from
Jun 28, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
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
35 changes: 1 addition & 34 deletions frontend/controller/actions/group.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,12 @@ import {
INVITE_EXPIRES_IN_DAYS,
INVITE_INITIAL_CREATOR,
MAX_GROUP_MEMBER_COUNT,
MESSAGE_TYPES,
PROFILE_STATUS,
PROPOSAL_GENERIC,
PROPOSAL_GROUP_SETTING_CHANGE,
PROPOSAL_INVITE_MEMBER,
PROPOSAL_PROPOSAL_SETTING_CHANGE,
PROPOSAL_REMOVE_MEMBER,
PROPOSAL_VARIANTS,
STATUS_OPEN
} from '@model/contracts/shared/constants.js'
import { merge, omit, randomIntFromRange } from '@model/contracts/shared/giLodash.js'
Expand Down Expand Up @@ -843,38 +841,6 @@ export default (sbp('sbp/selectors/register', {
// inside of the exception handler :-(
}
},
...encryptedAction('gi.actions/group/notifyExpiringProposals', L('Failed to notify expiring proposals.'), async function (sendMessage, params) {
const { proposals } = params.data
await sendMessage({
...omit(params, ['options', 'data', 'action', 'hooks']),
data: proposals.map(p => p.proposalId),
hooks: {
prepublish: params.hooks?.prepublish,
postpublish: null
}
})

const rootState = sbp('state/vuex/state')
const { generalChatRoomId } = rootState[params.contractID]

for (const proposal of proposals) {
await sbp('gi.actions/chatroom/addMessage', {
...omit(params, ['options', 'contractID', 'data', 'hooks']),
contractID: generalChatRoomId,
data: {
type: MESSAGE_TYPES.INTERACTIVE,
proposal: {
...proposal,
variant: PROPOSAL_VARIANTS.EXPIRING
}
},
hooks: {
prepublish: params.hooks?.prepublish,
postpublish: null
}
})
}
}),
'gi.actions/group/displayMincomeChangedPrompt': async function ({ data }: GIActionParams) {
const { withGroupCurrency } = sbp('state/vuex/getters')
const promptOptions = data.increased
Expand Down Expand Up @@ -979,6 +945,7 @@ export default (sbp('sbp/selectors/register', {
...encryptedAction('gi.actions/group/updateSettings', L('Failed to update group settings.')),
...encryptedAction('gi.actions/group/updateAllVotingRules', (params, e) => L('Failed to update voting rules. {codeError}', { codeError: e.message })),
...encryptedAction('gi.actions/group/markProposalsExpired', L('Failed to mark proposals expired.')),
...encryptedAction('gi.actions/group/notifyExpiringProposals', L('Failed to notify expiring proposals.')),
...encryptedAction('gi.actions/group/updateDistributionDate', L('Failed to update group distribution date.')),
...((process.env.NODE_ENV === 'development' || process.env.CI) && {
...encryptedAction('gi.actions/group/forceDistributionDate', L('Failed to force distribution date.'))
Expand Down
118 changes: 74 additions & 44 deletions frontend/model/contracts/group.js
Original file line number Diff line number Diff line change
Expand Up @@ -848,13 +848,16 @@ sbp('chelonia/defineContract', {
const payment = state.payments[data.paymentHash]

if (loggedIn.identityContractID === payment.data.toMemberID) {
sbp('gi.contracts/group/emitNotificationAfterSyncing', [contractID, innerSigningContractID], 'PAYMENT_RECEIVED', {
createdDate: meta.createdDate,
groupID: contractID,
creatorID: innerSigningContractID,
paymentHash: data.paymentHash,
amount: getters.withGroupCurrency(payment.data.amount)
})
sbp('gi.contracts/group/emitNotificationsAfterSyncing', [contractID, innerSigningContractID], [{
notificationName: 'PAYMENT_RECEIVED',
notificationData: {
createdDate: meta.createdDate,
groupID: contractID,
creatorID: innerSigningContractID,
paymentHash: data.paymentHash,
amount: getters.withGroupCurrency(payment.data.amount)
}
}])
}
}
}
Expand All @@ -872,12 +875,15 @@ sbp('chelonia/defineContract', {
const { loggedIn } = sbp('state/vuex/state')

if (data.toMemberID === loggedIn.identityContractID) {
sbp('gi.contracts/group/emitNotificationAfterSyncing', [contractID, innerSigningContractID], 'PAYMENT_THANKYOU_SENT', {
createdDate: meta.createdDate,
groupID: contractID,
fromMemberID: innerSigningContractID,
toMemberID: data.toMemberID
})
sbp('gi.contracts/group/emitNotificationsAfterSyncing', [contractID, innerSigningContractID], [{
notificationName: 'PAYMENT_THANKYOU_SENT',
notificationData: {
createdDate: meta.createdDate,
groupID: contractID,
fromMemberID: innerSigningContractID,
toMemberID: data.toMemberID
}
}])
}
}
},
Expand Down Expand Up @@ -937,12 +943,15 @@ sbp('chelonia/defineContract', {
const myProfile = getters.groupProfile(loggedIn.identityContractID)

if (isActionOlderThanUser(contractID, height, myProfile)) {
sbp('gi.contracts/group/emitNotificationAfterSyncing', [contractID, innerSigningContractID], 'NEW_PROPOSAL', {
createdDate: meta.createdDate,
groupID: contractID,
creatorID: innerSigningContractID,
subtype: typeToSubTypeMap[data.proposalType]
})
sbp('gi.contracts/group/emitNotificationsAfterSyncing', [contractID, innerSigningContractID], [{
notificationName: 'NEW_PROPOSAL',
notificationData: {
createdDate: meta.createdDate,
groupID: contractID,
creatorID: innerSigningContractID,
subtype: typeToSubTypeMap[data.proposalType]
}
}])
}
}
},
Expand Down Expand Up @@ -1024,11 +1033,25 @@ sbp('chelonia/defineContract', {
}
},
'gi.contracts/group/notifyExpiringProposals': {
validate: actionRequireActiveMember(arrayOf(string)),
validate: actionRequireActiveMember(objectOf({
proposalIds: arrayOf(string)
})),
Comment on lines +1036 to +1038
Copy link
Member Author

Choose a reason for hiding this comment

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

To make data meaningful, I've updated the type from array to object.

process ({ data, meta, contractID }, { state }) {
for (const proposalId of data) {
for (const proposalId of data.proposalIds) {
Vue.set(state.proposals[proposalId], 'notifiedBeforeExpire', true)
}
},
sideEffect ({ data, contractID }, { state }) {
const notifications = []
for (const proposalId of data.proposalIds) {
const proposal = state.proposals[proposalId]
notifications.push({
notificationName: 'PROPOSAL_EXPIRING',
notificationData: { groupID: contractID, proposal, proposalId }
})
}

sbp('gi.contracts/group/emitNotificationsAfterSyncing', contractID, notifications)
}
},
'gi.contracts/group/removeMember': {
Expand Down Expand Up @@ -1197,11 +1220,14 @@ sbp('chelonia/defineContract', {
const myProfile = profiles[userID]

if (isActionOlderThanUser(contractID, height, myProfile)) {
sbp('gi.notifications/emit', 'MEMBER_ADDED', { // emit a notification for a member addition.
createdDate: meta.createdDate,
groupID: contractID,
memberID: innerSigningContractID
})
sbp('gi.contracts/group/emitNotificationsAfterSyncing', [], [{
notificationName: 'MEMBER_ADDED',
notificationData: {
createdDate: meta.createdDate,
groupID: contractID,
memberID: innerSigningContractID
}
}])
}
}).catch((e) => {
console.error(`Error subscribing to identity contract ${innerSigningContractID} of group member for group ${contractID}`, e)
Expand Down Expand Up @@ -1687,9 +1713,10 @@ sbp('chelonia/defineContract', {
const { loggedIn } = sbp('state/vuex/state')
const { createdDate } = meta
if (isActionOlderThanUser(contractID, height, state.profiles[loggedIn.identityContractID])) {
sbp('gi.contracts/group/emitNotificationAfterSyncing', contractID, 'PROPOSAL_CLOSED', {
createdDate, groupID: contractID, proposal
})
sbp('gi.contracts/group/emitNotificationsAfterSyncing', contractID, [{
notificationName: 'PROPOSAL_CLOSED',
notificationData: { createdDate, groupID: contractID, proposal }
}])
}
},
'gi.contracts/group/sendMincomeChangedNotification': async function (contractID, meta, data, height, innerSigningContractID) {
Expand Down Expand Up @@ -1732,13 +1759,16 @@ sbp('chelonia/defineContract', {
})
}

sbp('gi.contracts/group/emitNotificationAfterSyncing', [contractID, innerSigningContractID], 'MINCOME_CHANGED', {
groupID: contractID,
creatorID: innerSigningContractID,
to: toAmount,
memberType,
increased: mincomeIncreased
})
sbp('gi.contracts/group/emitNotificationsAfterSyncing', [contractID, innerSigningContractID], [{
notificationName: 'MINCOME_CHANGED',
notificationData: {
groupID: contractID,
creatorID: innerSigningContractID,
to: toAmount,
memberType,
increased: mincomeIncreased
}
}])
}
},
'gi.contracts/group/joinGroupChatrooms': async function (contractID, chatRoomID, memberID) {
Expand Down Expand Up @@ -1884,12 +1914,10 @@ sbp('chelonia/defineContract', {
if (!proposalHash) {
// NOTE: Do not make notification when the member is removed by proposal
const memberRemovedThemselves = memberID === innerSigningContractID
const notificationName = memberRemovedThemselves ? 'MEMBER_LEFT' : 'MEMBER_REMOVED'
sbp('gi.contracts/group/emitNotificationAfterSyncing', memberID, notificationName, {
createdDate: meta.createdDate,
groupID: contractID,
memberID
})
sbp('gi.contracts/group/emitNotificationsAfterSyncing', memberID, [{
notificationName: memberRemovedThemselves ? 'MEMBER_LEFT' : 'MEMBER_REMOVED',
notificationData: { createdDate: meta.createdDate, groupID: contractID, memberID }
}])
}

Promise.resolve()
Expand Down Expand Up @@ -1948,13 +1976,15 @@ sbp('chelonia/defineContract', {
console.warn(`removeForeignKeys: ${e.name} error thrown:`, e)
})
},
'gi.contracts/group/emitNotificationAfterSyncing': async (contractIDs, notificationName, notificationData) => {
'gi.contracts/group/emitNotificationsAfterSyncing': async (contractIDs, notifications) => {
Copy link
Member Author

Choose a reason for hiding this comment

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

Updated function to be able to emit multiple notifications.

const listOfIds = typeof contractIDs === 'string' ? [contractIDs] : contractIDs
for (const id of listOfIds) {
await sbp('chelonia/contract/wait', id)
}

sbp('gi.notifications/emit', notificationName, notificationData)
notifications.forEach(({ notificationName, notificationData }) => {
sbp('gi.notifications/emit', notificationName, notificationData)
})
}
}
})
10 changes: 0 additions & 10 deletions frontend/model/contracts/shared/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@ export const CHATROOM_MEMBER_MENTION_SPECIAL_CHAR = '@'
export const CHATROOM_CHANNEL_MENTION_SPECIAL_CHAR = '#'

// chatroom events
export const CHATROOM_MESSAGE_ACTION = 'chatroom-message-action'
Copy link
Member Author

Choose a reason for hiding this comment

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

We don't use this action anymore unless we use the contracts of old versions. I remember this action was removed in the PR #1521 where @corrideat implemented E2E Protocol.

export const MESSAGE_RECEIVE = 'message-receive'
export const MESSAGE_SEND = 'message-send'

Expand Down Expand Up @@ -98,15 +97,6 @@ export const MESSAGE_VARIANTS = {
FAILED: 'failed'
}

export const PROPOSAL_VARIANTS = {
CREATED: 'created',
EXPIRING: 'expiring',
ACCEPTED: 'accepted',
REJECTED: 'rejected',
CANCELLED: 'cancelled',
EXPIRED: 'expired'
}

export const MESSAGE_NOTIFY_SETTINGS = {
ALL_MESSAGES: 'all-messages',
DIRECT_MESSAGES: 'direct-messages',
Expand Down
34 changes: 20 additions & 14 deletions frontend/model/contracts/shared/functions.js
Original file line number Diff line number Diff line change
Expand Up @@ -210,30 +210,36 @@ export function makeMentionFromUserID (userID: string): {
}
}

export function makeChannelMention (string: string): string {
return `${CHATROOM_CHANNEL_MENTION_SPECIAL_CHAR}${string}`
export function makeChannelMention (channelName: string): string {
return `${CHATROOM_CHANNEL_MENTION_SPECIAL_CHAR}${channelName}`
}

export function swapMentionIDForDisplayname (text: string): string {
export function swapMentionIDForDisplayname (
text: string,
options: Object = { escaped: true, forChat: true }
Copy link
Member Author

Choose a reason for hiding this comment

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

Added options argument and made this function more dynamic.

escaped: this indicates that the text contains escaped characters
forChat: this indicates that the function is being used for messages inside chatroom

Copy link
Member

Choose a reason for hiding this comment

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

Please add some inline comments to the code to explain what these parameters are along with some examples of which situations you would want to use one versus the other.

): string {
const {
chatRoomsInDetail,
ourContactProfilesById,
getChatroomNameById,
usernameFromID
usernameFromID,
userDisplayNameFromID
} = sbp('state/vuex/getters')
const possibleMentions = [
...Object.keys(ourContactProfilesById).map(u => makeMentionFromUserID(u).me).filter(v => !!v),
...Object.values(chatRoomsInDetail).map((details: any) => makeChannelMention(details.id))
]

return text
.split(new RegExp(`(?<=\\s|^)(${possibleMentions.join('|')})(?=[^\\w\\d]|$)`))
.map(t => {
return possibleMentions.includes(t)
? t[0] === CHATROOM_MEMBER_MENTION_SPECIAL_CHAR
? t[0] + usernameFromID(t.slice(1))
: t[0] + getChatroomNameById(t.slice(1))
: t
})
.join('')
const { escaped, forChat } = options
const regEx = escaped
? new RegExp(`(?<=\\s|^)(${possibleMentions.join('|')})(?=[^\\w\\d]|$)`)
: new RegExp(`(${possibleMentions.join('|')})`)

return text.split(regEx).map(t => {
return possibleMentions.includes(t)
? t[0] === CHATROOM_MEMBER_MENTION_SPECIAL_CHAR
? forChat ? t[0] + usernameFromID(t.slice(1)) : userDisplayNameFromID(t.slice(1))
: (forChat ? t[0] : '') + getChatroomNameById(t.slice(1))
: t
}).join('')
Copy link
Member

Choose a reason for hiding this comment

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

This is written in a far more confusing manner now. Can you please add some parenthesis around the nested ?:'s to make it clearer?

}
15 changes: 2 additions & 13 deletions frontend/model/contracts/shared/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,7 @@ import {
objectOf, objectMaybeOf, arrayOf, unionOf,
object, string, optional, number, mapOf, literalOf
} from '~/frontend/model/contracts/misc/flowTyper.js'
import {
CHATROOM_TYPES, CHATROOM_PRIVACY_LEVEL,
MESSAGE_TYPES, MESSAGE_NOTIFICATIONS, PROPOSAL_VARIANTS
} from './constants.js'
import { CHATROOM_TYPES, CHATROOM_PRIVACY_LEVEL, MESSAGE_TYPES, MESSAGE_NOTIFICATIONS } from './constants.js'

// group.js related

Expand All @@ -31,15 +28,7 @@ export const chatRoomAttributesType: any = objectOf({

export const messageType: any = objectMaybeOf({
type: unionOf(...Object.values(MESSAGE_TYPES).map(v => literalOf(v))),
text: string, // message text | notificationType when type if NOTIFICATION
proposal: objectMaybeOf({
proposalId: string,
proposalType: string,
expires_date_ms: number,
createdDate: string,
creatorID: string,
variant: unionOf(...Object.values(PROPOSAL_VARIANTS).map(v => literalOf(v)))
}),
text: string,
notification: objectMaybeOf({
type: unionOf(...Object.values(MESSAGE_NOTIFICATIONS).map(v => literalOf(v))),
params: mapOf(string, string) // { username }
Expand Down
2 changes: 1 addition & 1 deletion frontend/model/notifications/mainNotificationsMixin.js
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ const periodicNotificationEntries = [
if (expiringProposals.length) {
await sbp('gi.actions/group/notifyExpiringProposals', {
contractID,
data: { proposals: expiringProposals }
data: { proposalIds: expiringProposals.map(p => p.proposalId) }
})
}

Expand Down
2 changes: 1 addition & 1 deletion frontend/model/notifications/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ sbp('sbp/selectors/register', {

// Creates the notification object in a single step.
const notification = {
avatarUserID: template.avatarUserID || sbp('state/vuex/getters').ourIdentityContractId,
...template,
avatarUserID: template.avatarUserID || sbp('state/vuex/getters').ourIdentityContractId,
Copy link
Member Author

Choose a reason for hiding this comment

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

Updated order so that avatarUserID could be overridden.

// Sets 'groupID' if this notification only pertains to a certain group.
...(template.scope === 'group' ? { groupID: data.groupID } : {}),
read: false,
Expand Down
Loading
Loading