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 all 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
6 changes: 3 additions & 3 deletions frontend/controller/actions/group.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
PROPOSAL_INVITE_MEMBER,
PROPOSAL_PROPOSAL_SETTING_CHANGE,
PROPOSAL_REMOVE_MEMBER,
PROPOSAL_VARIANTS,
STATUS_EXPIRING,
STATUS_OPEN
} from '@model/contracts/shared/constants.js'
import { merge, omit, randomIntFromRange } from '@model/contracts/shared/giLodash.js'
Expand Down Expand Up @@ -851,7 +851,7 @@ export default (sbp('sbp/selectors/register', {
const { proposals } = params.data
await sendMessage({
...omit(params, ['options', 'data', 'action', 'hooks']),
data: proposals.map(p => p.proposalId),
data: { proposalIds: proposals.map(p => p.proposalId) },
hooks: {
prepublish: params.hooks?.prepublish,
postpublish: null
Expand All @@ -869,7 +869,7 @@ export default (sbp('sbp/selectors/register', {
type: MESSAGE_TYPES.INTERACTIVE,
proposal: {
...proposal,
variant: PROPOSAL_VARIANTS.EXPIRING
variant: STATUS_EXPIRING
}
},
hooks: {
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 }, { 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 @@ -1889,12 +1919,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 @@ -1953,13 +1981,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)
})
}
}
})
11 changes: 1 addition & 10 deletions frontend/model/contracts/shared/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export const MAX_GROUP_MEMBER_COUNT = 150 // Dunbar's number (https://en.wikiped
export const STATUS_OPEN = 'open'
export const STATUS_PASSED = 'passed'
export const STATUS_FAILED = 'failed'
export const STATUS_EXPIRING = 'expiring' // Only useful to notify users that the proposals are expiring
export const STATUS_EXPIRED = 'expired'
export const STATUS_CANCELLED = 'cancelled'

Expand All @@ -55,7 +56,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 @@ -101,15 +101,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
46 changes: 34 additions & 12 deletions frontend/model/contracts/shared/functions.js
Original file line number Diff line number Diff line change
Expand Up @@ -210,30 +210,52 @@ 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, // this indicates that the text contains escaped characters
forChat: true // this indicates that the function is being used for messages inside chatroom
}
): 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))
]

const { escaped, forChat } = options
const regEx = escaped
? new RegExp(`(?<=\\s|^)(${possibleMentions.join('|')})(?=[^\\w\\d]|$)`)
: new RegExp(`(${possibleMentions.join('|')})`)

const swap = (t) => {
if (t.startsWith(CHATROOM_MEMBER_MENTION_SPECIAL_CHAR)) {
// swap member mention
const userID = t.slice(1)
const prefix = forChat ? CHATROOM_MEMBER_MENTION_SPECIAL_CHAR : ''
const body = forChat ? usernameFromID(userID) : userDisplayNameFromID(userID)
return prefix + body
} else if (t.startsWith(CHATROOM_CHANNEL_MENTION_SPECIAL_CHAR)) {
// swap channel mention
const channelID = t.slice(1)
const prefix = forChat ? CHATROOM_CHANNEL_MENTION_SPECIAL_CHAR : ''
return prefix + getChatroomNameById(channelID)
}
return t
}

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
})
.split(regEx)
.map(t => possibleMentions.includes(t) ? swap(t) : t)
.join('')
}
12 changes: 8 additions & 4 deletions frontend/model/contracts/shared/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,12 @@ import {
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, POLL_TYPES
CHATROOM_TYPES,
CHATROOM_PRIVACY_LEVEL,
MESSAGE_TYPES,
MESSAGE_NOTIFICATIONS,
POLL_TYPES,
STATUS_EXPIRING
} from './constants.js'

// group.js related
Expand All @@ -31,14 +35,14 @@ 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
text: string,
proposal: objectMaybeOf({
proposalId: string,
proposalType: string,
expires_date_ms: number,
createdDate: string,
creatorID: string,
variant: unionOf(...Object.values(PROPOSAL_VARIANTS).map(v => literalOf(v)))
variant: unionOf([STATUS_EXPIRING].map(v => literalOf(v))) // NOTE: only expiring proposals could be notified at the moment
Copy link
Member

Choose a reason for hiding this comment

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

@Silver-IT Please feel free to add all of the types of proposals here, as we want to be able to notify the #general chatroom of anything related to proposals (being opened, closed, etc.).

Copy link
Member Author

Choose a reason for hiding this comment

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

I was planning to add all types while I would be working on Issue #2138 after this PR is merged. Do you want me to do it in this PR?

Copy link
Member

Choose a reason for hiding this comment

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

OK, we can save it for a separate PR 👍

}),
notification: objectMaybeOf({
type: unionOf(...Object.values(MESSAGE_NOTIFICATIONS).map(v => literalOf(v))),
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