Skip to content

Commit

Permalink
Improve to manage notifications and proposals (#2046)
Browse files Browse the repository at this point in the history
* chore: removed PROPOSAL_VARIANTS enum

* feat: made notification and removed to create message when proposal is expiring

* chore: removed useless state variable

* Save notification body in Object (#2052)

* PR for release v0.4.2

* feat: save notification body as object format

---------

Co-authored-by: Greg Slepak <[email protected]>

* chore: uncommented the codes from historical MessageInteractive.vue

* chore: reverted changes according to the feedbackc

* fix: tiny errors

* fix: 2 small errors

* fix: error in using param in notifyExpiringProposal function

* chore: tiny update according to the feedback

* chore: tiny update and travis retry

---------

Co-authored-by: Greg Slepak <[email protected]>
  • Loading branch information
Silver-IT and taoeffect authored Jun 28, 2024
1 parent 18cd616 commit 41dc1f1
Show file tree
Hide file tree
Showing 10 changed files with 226 additions and 172 deletions.
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 @@ -866,7 +866,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 @@ -884,7 +884,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)
})),
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 @@ -1200,11 +1223,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 @@ -1692,9 +1718,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 @@ -1737,13 +1764,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 @@ -1894,12 +1924,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 @@ -1958,13 +1986,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) => {
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'
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
}),
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,
// Sets 'groupID' if this notification only pertains to a certain group.
...(template.scope === 'group' ? { groupID: data.groupID } : {}),
read: false,
Expand Down
Loading

0 comments on commit 41dc1f1

Please sign in to comment.