From dfe9844fe0b48fe5b8403c0cac9e8f33492def67 Mon Sep 17 00:00:00 2001 From: silver-it Date: Wed, 5 Jun 2024 17:33:24 +0800 Subject: [PATCH 01/11] chore: removed PROPOSAL_VARIANTS enum --- frontend/controller/actions/group.js | 4 +- frontend/model/contracts/shared/constants.js | 11 +--- frontend/model/contracts/shared/types.js | 4 +- .../chatroom/MessageInteractive.vue | 66 +++++++++---------- 4 files changed, 38 insertions(+), 47 deletions(-) diff --git a/frontend/controller/actions/group.js b/frontend/controller/actions/group.js index 71d1766e32..3f11e03f7c 100644 --- a/frontend/controller/actions/group.js +++ b/frontend/controller/actions/group.js @@ -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' @@ -865,7 +865,7 @@ export default (sbp('sbp/selectors/register', { type: MESSAGE_TYPES.INTERACTIVE, proposal: { ...proposal, - variant: PROPOSAL_VARIANTS.EXPIRING + variant: STATUS_EXPIRING } }, hooks: { diff --git a/frontend/model/contracts/shared/constants.js b/frontend/model/contracts/shared/constants.js index 5b04918c1d..1bd9ba7cb0 100644 --- a/frontend/model/contracts/shared/constants.js +++ b/frontend/model/contracts/shared/constants.js @@ -33,6 +33,7 @@ export const STATUS_OPEN = 'open' export const STATUS_PASSED = 'passed' export const STATUS_FAILED = 'failed' export const STATUS_EXPIRED = 'expired' +export const STATUS_EXPIRING = 'expiring' export const STATUS_CANCELLED = 'cancelled' export const STREAK_ON_TIME_PAYMENTS = 1 @@ -52,7 +53,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' @@ -98,15 +98,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', diff --git a/frontend/model/contracts/shared/types.js b/frontend/model/contracts/shared/types.js index 35f4d6423c..2afb049440 100644 --- a/frontend/model/contracts/shared/types.js +++ b/frontend/model/contracts/shared/types.js @@ -6,7 +6,7 @@ import { } from '~/frontend/model/contracts/misc/flowTyper.js' import { CHATROOM_TYPES, CHATROOM_PRIVACY_LEVEL, - MESSAGE_TYPES, MESSAGE_NOTIFICATIONS, PROPOSAL_VARIANTS + MESSAGE_TYPES, MESSAGE_NOTIFICATIONS, STATUS_EXPIRING } from './constants.js' // group.js related @@ -38,7 +38,7 @@ export const messageType: any = objectMaybeOf({ 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: STATUS_EXPIRING is only available ATM }), notification: objectMaybeOf({ type: unionOf(...Object.values(MESSAGE_NOTIFICATIONS).map(v => literalOf(v))), diff --git a/frontend/views/containers/chatroom/MessageInteractive.vue b/frontend/views/containers/chatroom/MessageInteractive.vue index 9440c9578d..133962e026 100644 --- a/frontend/views/containers/chatroom/MessageInteractive.vue +++ b/frontend/views/containers/chatroom/MessageInteractive.vue @@ -25,7 +25,7 @@ import { PROPOSAL_REMOVE_MEMBER, PROPOSAL_PROPOSAL_SETTING_CHANGE, PROPOSAL_GENERIC, - PROPOSAL_VARIANTS + STATUS_EXPIRING } from '@model/contracts/shared/constants.js' import { getProposalDetails } from '@model/contracts/shared/functions.js' import MessageBase from './MessageBase.vue' @@ -40,27 +40,27 @@ const interactiveMessage = (proposal, baseOptions = {}) => { const options = Object.assign(proposalDetails, baseOptions) const settingChangeMessages = (options) => ({ - [PROPOSAL_VARIANTS.CREATED]: L('{from} wants to change the groups {setting}.', options), - [PROPOSAL_VARIANTS.EXPIRING]: L('Proposal from {from} to change the {setting} is expiring.', options), - [PROPOSAL_VARIANTS.ACCEPTED]: L('Proposal from {from} to change the {setting} is accepted.', options), - [PROPOSAL_VARIANTS.REJECTED]: L('Proposal from {from} to change the {setting} is rejected.', options), - [PROPOSAL_VARIANTS.EXPIRED]: L('Proposal from {from} to change the {setting} is expired.', options) + // [STATUS_OPEN]: L('{from} wants to change the groups {setting}.', options), + // [STATUS_PASSED]: L('Proposal from {from} to change the {setting} is accepted.', options), + // [STATUS_FAILED]: L('Proposal from {from} to change the {setting} is rejected.', options), + // [STATUS_EXPIRED]: L('Proposal from {from} to change the {setting} is expired.', options), + [STATUS_EXPIRING]: L('Proposal from {from} to change the {setting} is expiring.', options) }) const interactiveMessages = { [PROPOSAL_INVITE_MEMBER]: { - [PROPOSAL_VARIANTS.CREATED]: L('{from} wants to add {member} to the group.', options), - [PROPOSAL_VARIANTS.EXPIRING]: L('Proposal from {from} to add {member} is expiring.', options), - [PROPOSAL_VARIANTS.ACCEPTED]: L('Proposal from {from} to add {member} is accepted.', options), - [PROPOSAL_VARIANTS.REJECTED]: L('Proposal from {from} to add {member} is rejected.', options), - [PROPOSAL_VARIANTS.EXPIRED]: L('Proposal from {from} to add {member} is expired.', options) + // [STATUS_OPEN]: L('{from} wants to add {member} to the group.', options), + // [STATUS_PASSED]: L('Proposal from {from} to add {member} is accepted.', options), + // [STATUS_FAILED]: L('Proposal from {from} to add {member} is rejected.', options), + // [STATUS_EXPIRED]: L('Proposal from {from} to add {member} is expired.', options), + [STATUS_EXPIRING]: L('Proposal from {from} to add {member} is expiring.', options) }, [PROPOSAL_REMOVE_MEMBER]: { - [PROPOSAL_VARIANTS.CREATED]: L('{from} wants to remove {member} from the group.', options), - [PROPOSAL_VARIANTS.EXPIRING]: L('Proposal from {from} to remove {member} is expiring.', options), - [PROPOSAL_VARIANTS.ACCEPTED]: L('Proposal from {from} to remove {member} is accepted.', options), - [PROPOSAL_VARIANTS.REJECTED]: L('Proposal from {from} to add {member} is rejected.', options), - [PROPOSAL_VARIANTS.EXPIRED]: L('Proposal from {from} to add {member} is expired.', options) + // [STATUS_OPEN]: L('{from} wants to remove {member} from the group.', options), + // [STATUS_PASSED]: L('Proposal from {from} to remove {member} is accepted.', options), + // [STATUS_FAILED]: L('Proposal from {from} to add {member} is rejected.', options), + // [STATUS_EXPIRED]: L('Proposal from {from} to add {member} is expired.', options), + [STATUS_EXPIRING]: L('Proposal from {from} to remove {member} is expiring.', options) }, [PROPOSAL_GROUP_SETTING_CHANGE]: { mincomeAmount: settingChangeMessages(options), @@ -71,11 +71,11 @@ const interactiveMessage = (proposal, baseOptions = {}) => { votingSystem: settingChangeMessages(options) }, [PROPOSAL_GENERIC]: { - [PROPOSAL_VARIANTS.CREATED]: L('{from} created a proposal. "{title}"', options), - [PROPOSAL_VARIANTS.EXPIRING]: L('Proposal from {from} is expiring. "{title}"', options), - [PROPOSAL_VARIANTS.ACCEPTED]: L('Proposal from {from} is accepted. "{title}"', options), - [PROPOSAL_VARIANTS.REJECTED]: L('Proposal from {from} is rejected. "{title}"', options), - [PROPOSAL_VARIANTS.EXPIRED]: L('Proposal from {from} is expired. "{title}"', options) + // [STATUS_OPEN]: L('{from} created a proposal. "{title}"', options), + // [STATUS_PASSED]: L('Proposal from {from} is accepted. "{title}"', options), + // [STATUS_FAILED]: L('Proposal from {from} is rejected. "{title}"', options), + // [STATUS_EXPIRED]: L('Proposal from {from} is expired. "{title}"', options), + [STATUS_EXPIRING]: L('Proposal from {from} is expiring. "{title}"', options) } } @@ -84,24 +84,24 @@ const interactiveMessage = (proposal, baseOptions = {}) => { const proposalStatus = (proposal) => { const options = {} - if (proposal.variant === PROPOSAL_VARIANTS.EXPIRING) { + if (proposal.variant === STATUS_EXPIRING) { options['date'] = humanDate(proposal.expires_date_ms, { month: 'short', day: 'numeric', year: 'numeric' }) } return { - [PROPOSAL_VARIANTS.CREATED]: L('New proposal'), - [PROPOSAL_VARIANTS.EXPIRING]: L('Proposal expiring on {date}', options), - [PROPOSAL_VARIANTS.ACCEPTED]: L('Proposal Accepted'), - [PROPOSAL_VARIANTS.REJECTED]: L('Proposal rejected'), - [PROPOSAL_VARIANTS.EXPIRED]: L('Proposal expired') + // [STATUS_OPEN]: L('New proposal'), + // [STATUS_PASSED]: L('Proposal Accepted'), + // [STATUS_FAILED]: L('Proposal rejected'), + // [STATUS_EXPIRED]: L('Proposal expired'), + [STATUS_EXPIRING]: L('Proposal expiring on {date}', options) }[proposal.variant] } const proposalSeverity = { - [PROPOSAL_VARIANTS.CREATED]: 'is-info', - [PROPOSAL_VARIANTS.EXPIRING]: 'is-warning', - [PROPOSAL_VARIANTS.ACCEPTED]: 'is-success', - [PROPOSAL_VARIANTS.REJECTED]: 'is-danger', - [PROPOSAL_VARIANTS.EXPIRED]: 'is-neutral' + // [STATUS_OPEN]: 'is-info', + // [STATUS_PASSED]: 'is-success', + // [STATUS_FAILED]: 'is-danger', + // [STATUS_EXPIRED]: 'is-neutral', + [STATUS_EXPIRING]: 'is-warning' } export default ({ @@ -135,7 +135,7 @@ export default ({ } }, isYellowHorn () { - return this.proposal.variant === PROPOSAL_VARIANTS.EXPIRING + return this.proposal.variant === STATUS_EXPIRING } } }: Object) From 0f5d9c911a93c4bd1f85e559a9980d33e4df982a Mon Sep 17 00:00:00 2001 From: silver-it Date: Thu, 6 Jun 2024 08:54:34 +0800 Subject: [PATCH 02/11] feat: made notification and removed to create message when proposal is expiring --- frontend/controller/actions/group.js | 35 +-- frontend/model/contracts/group.js | 118 ++++++---- frontend/model/contracts/shared/types.js | 15 +- .../notifications/mainNotificationsMixin.js | 2 +- frontend/model/notifications/selectors.js | 2 +- frontend/model/notifications/templates.js | 15 +- .../views/containers/chatroom/ChatMain.vue | 1 - .../chatroom/MessageInteractive.vue | 122 +---------- .../chatroom/MessageInteractive.vue | 205 ++++++++++++++++++ 9 files changed, 297 insertions(+), 218 deletions(-) create mode 100644 historical/views/containers/chatroom/MessageInteractive.vue diff --git a/frontend/controller/actions/group.js b/frontend/controller/actions/group.js index 3f11e03f7c..735278a842 100644 --- a/frontend/controller/actions/group.js +++ b/frontend/controller/actions/group.js @@ -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, - STATUS_EXPIRING, STATUS_OPEN } from '@model/contracts/shared/constants.js' import { merge, omit, randomIntFromRange } from '@model/contracts/shared/giLodash.js' @@ -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: STATUS_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 @@ -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.')) diff --git a/frontend/model/contracts/group.js b/frontend/model/contracts/group.js index 4751638b6c..a038ed3932 100644 --- a/frontend/model/contracts/group.js +++ b/frontend/model/contracts/group.js @@ -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) + } + }]) } } } @@ -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 + } + }]) } } }, @@ -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] + } + }]) } } }, @@ -1024,11 +1033,25 @@ sbp('chelonia/defineContract', { } }, 'gi.contracts/group/notifyExpiringProposals': { - validate: actionRequireActiveMember(arrayOf(string)), + validate: actionRequireActiveMember(objectOf({ + proposalIds: arrayOf(string) + })), 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': { @@ -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) @@ -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) { @@ -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) { @@ -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() @@ -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) => { 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) + }) } } }) diff --git a/frontend/model/contracts/shared/types.js b/frontend/model/contracts/shared/types.js index 2afb049440..0a43922b37 100644 --- a/frontend/model/contracts/shared/types.js +++ b/frontend/model/contracts/shared/types.js @@ -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, STATUS_EXPIRING -} from './constants.js' +import { CHATROOM_TYPES, CHATROOM_PRIVACY_LEVEL, MESSAGE_TYPES, MESSAGE_NOTIFICATIONS } from './constants.js' // group.js related @@ -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(...[STATUS_EXPIRING].map(v => literalOf(v))) // NOTE: STATUS_EXPIRING is only available ATM - }), + text: string, notification: objectMaybeOf({ type: unionOf(...Object.values(MESSAGE_NOTIFICATIONS).map(v => literalOf(v))), params: mapOf(string, string) // { username } diff --git a/frontend/model/notifications/mainNotificationsMixin.js b/frontend/model/notifications/mainNotificationsMixin.js index 61b3541839..c2c043569a 100644 --- a/frontend/model/notifications/mainNotificationsMixin.js +++ b/frontend/model/notifications/mainNotificationsMixin.js @@ -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) } }) } diff --git a/frontend/model/notifications/selectors.js b/frontend/model/notifications/selectors.js index 82a46f1a67..1c6a50d505 100644 --- a/frontend/model/notifications/selectors.js +++ b/frontend/model/notifications/selectors.js @@ -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, diff --git a/frontend/model/notifications/templates.js b/frontend/model/notifications/templates.js index 460ad66812..1c4e0a35d8 100644 --- a/frontend/model/notifications/templates.js +++ b/frontend/model/notifications/templates.js @@ -172,23 +172,24 @@ export default ({ scope: 'group' } }, - PROPOSAL_EXPIRING (data: { creatorID: string, proposalType: string, proposalData: any, title?: string, proposalId: string }) { + PROPOSAL_EXPIRING (data: { proposalId: string, proposal: Object }) { + const { proposalData, proposalType } = data.proposal.data const typeToTitleMap = { [PROPOSAL_INVITE_MEMBER]: L('Member addition'), [PROPOSAL_REMOVE_MEMBER]: L('Member removal'), [PROPOSAL_GROUP_SETTING_CHANGE]: { mincomeAmount: L('Mincome change'), distributionDate: L('Distribution date change') - }[data.proposalData.setting], + }[proposalData.setting], [PROPOSAL_PROPOSAL_SETTING_CHANGE]: L('Voting rule change'), - [PROPOSAL_GENERIC]: data.title + [PROPOSAL_GENERIC]: proposalData.name } return { - avatarUserID: data.creatorID, - body: L('Proposal about to expire: {i_}"{proposalTitle}"{_i}. please vote!', { + avatarUserID: '', + body: L('Proposal about to expire: {i_}"{proposalTitle}"{_i}. Please vote!', { ...LTags('i'), - proposalTitle: typeToTitleMap[data.proposalType] + proposalTitle: typeToTitleMap[proposalType] }), level: 'info', icon: 'exclamation-triangle', @@ -197,7 +198,7 @@ export default ({ data: { proposalId: data.proposalId } } }, - PROPOSAL_CLOSED (data: { groupID: string, proposal: Object }) { + PROPOSAL_CLOSED (data: { proposal: Object }) { const { creatorID, status, type, options } = getProposalDetails(data.proposal) const bodyTemplateMap = { diff --git a/frontend/views/containers/chatroom/ChatMain.vue b/frontend/views/containers/chatroom/ChatMain.vue index d4902bb25c..bf990d448d 100644 --- a/frontend/views/containers/chatroom/ChatMain.vue +++ b/frontend/views/containers/chatroom/ChatMain.vue @@ -69,7 +69,6 @@ :attachments='message.attachments' :type='message.type' :notification='message.notification' - :proposal='message.proposal' :pollData='message.pollData' :replyingMessage='replyingMessageText(message)' :datetime='time(message.datetime)' diff --git a/frontend/views/containers/chatroom/MessageInteractive.vue b/frontend/views/containers/chatroom/MessageInteractive.vue index 133962e026..7d1c7513be 100644 --- a/frontend/views/containers/chatroom/MessageInteractive.vue +++ b/frontend/views/containers/chatroom/MessageInteractive.vue @@ -1,141 +1,37 @@ + + From 13f132dd34cb5f1751ea34ffb93d27e8d310583f Mon Sep 17 00:00:00 2001 From: silver-it Date: Thu, 6 Jun 2024 09:23:17 +0800 Subject: [PATCH 03/11] chore: removed useless state variable --- frontend/model/contracts/shared/constants.js | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/model/contracts/shared/constants.js b/frontend/model/contracts/shared/constants.js index 1bd9ba7cb0..21f843fce2 100644 --- a/frontend/model/contracts/shared/constants.js +++ b/frontend/model/contracts/shared/constants.js @@ -33,7 +33,6 @@ export const STATUS_OPEN = 'open' export const STATUS_PASSED = 'passed' export const STATUS_FAILED = 'failed' export const STATUS_EXPIRED = 'expired' -export const STATUS_EXPIRING = 'expiring' export const STATUS_CANCELLED = 'cancelled' export const STREAK_ON_TIME_PAYMENTS = 1 From 8972c6a21e678ef2f4f3eb27b6e738f079f80fed Mon Sep 17 00:00:00 2001 From: Alex Jin <57976479+Silver-IT@users.noreply.github.com> Date: Mon, 10 Jun 2024 19:53:27 +0800 Subject: [PATCH 04/11] Save notification body in Object (#2052) * PR for release v0.4.2 * feat: save notification body as object format --------- Co-authored-by: Greg Slepak --- frontend/model/contracts/shared/functions.js | 34 +-- frontend/model/notifications/templates.js | 197 ++++++++++-------- frontend/model/notifications/types.flow.js | 5 +- .../notifications/NotificationList.vue | 10 +- package.json | 2 +- 5 files changed, 146 insertions(+), 102 deletions(-) diff --git a/frontend/model/contracts/shared/functions.js b/frontend/model/contracts/shared/functions.js index abe48dbac4..d3de169068 100644 --- a/frontend/model/contracts/shared/functions.js +++ b/frontend/model/contracts/shared/functions.js @@ -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 } +): 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('') } diff --git a/frontend/model/notifications/templates.js b/frontend/model/notifications/templates.js index 1c4e0a35d8..22e8376ecb 100644 --- a/frontend/model/notifications/templates.js +++ b/frontend/model/notifications/templates.js @@ -9,19 +9,11 @@ import { L, LTags } from '@common/common.js' import { humanDate } from '@model/contracts/shared/time.js' import { STATUS_PASSED, STATUS_FAILED, STATUS_CANCELLED, STATUS_EXPIRED, - PROPOSAL_INVITE_MEMBER, PROPOSAL_REMOVE_MEMBER, + PROPOSAL_INVITE_MEMBER, PROPOSAL_REMOVE_MEMBER, CHATROOM_MEMBER_MENTION_SPECIAL_CHAR, PROPOSAL_GROUP_SETTING_CHANGE, PROPOSAL_PROPOSAL_SETTING_CHANGE, PROPOSAL_GENERIC } from '@model/contracts/shared/constants.js' import { getProposalDetails } from '@model/contracts/shared/functions.js' -const contractName = (contractID) => sbp('state/vuex/state').contracts[contractID]?.type ?? contractID -const userDisplayNameFromID = (userID) => sbp('state/vuex/getters').userDisplayNameFromID(userID) -// Note: this escaping is not intended as a protection against XSS. -// It is only done to enable correct rendering of special characters in usernames. -// To guard against XSS when rendering usernames, use the `v-safe-html` directive. -const escapeForHtml = (text) => text.replace(/[<>&]/g, (c) => ('&#' + c.codePointAt(0) + ';')) -const strong = (text) => `${escapeForHtml(text)}` - export default ({ CHELONIA_ERROR (data: { activity: string, error: Error, message: GIMessage }) { const { activity, error, message } = data @@ -29,21 +21,23 @@ export default ({ const opType = message.opType() const value = message.decryptedValue() let action - let meta if ([GIMessage.OP_ACTION_ENCRYPTED, GIMessage.OP_ACTION_UNENCRYPTED].includes(opType) && value) { action = value.action - meta = value.meta } + return { - body: L("{errName} during {activity} for '{action}' from {b_}{who}{_b} to '{contract}': '{errMsg}'", { - ...LTags('b'), - errName: error.name, - activity, - action: action ?? opType, - who: meta?.username ?? message.signingKeyId(), - contract: contractName(contractID), - errMsg: error.message ?? '?' - }), + body: { + key: "{errName} during {activity} for '{action}' from {b_}{who}{_b} to '{contract}': '{errMsg}'", + args: { + ...LTags('b'), + errName: error.name, + activity, + action: action ?? opType, + who: `${CHATROOM_MEMBER_MENTION_SPECIAL_CHAR}${message.signingKeyId()}`, + contract: sbp('state/vuex/state').contracts[contractID]?.type ?? contractID, + errMsg: error.message ?? '?' + } + }, icon: 'exclamation-triangle', level: 'danger', linkTo: `/app/dashboard?modal=UserSettingsModal&tab=application-logs&errorMsg=${encodeURI(error.message)}`, @@ -79,9 +73,10 @@ export default ({ }, CONTRIBUTION_REMINDER (data: { date: string }) { return { - body: L('Do not forget to send your pledge by {date}.', { - date: strong(data.date) - }), + body: { + key: 'Do not forget to send your pledge by {strong_}{date}{_strong}.', + args: { ...LTags('strong') } + }, icon: 'coins', level: 'info', linkTo: '/payments', @@ -90,10 +85,10 @@ export default ({ }, INCOME_DETAILS_OLD (data: { months: number, lastUpdatedDate: string }) { return { - body: L("You haven't updated your income details in more than {months} months. Would you like to review them now?", { - // Avoid displaying decimals. - months: Math.floor(data.months) - }), + body: { + key: "You haven't updated your income details in more than {months} months. Would you like to review them now?", + args: { months: Math.floor(data.months) } // Avoid displaying decimals + }, icon: 'coins', level: 'info', linkTo: '/contributions?modal=IncomeDetails', @@ -102,25 +97,31 @@ export default ({ } }, MEMBER_ADDED (data: { groupID: string, memberID: string }) { - const rootState = sbp('state/vuex/state') - const name = strong(userDisplayNameFromID(data.memberID)) - return { avatarUserID: data.memberID, - body: L('The group has a new member. Say hi to {name}!', { name }), + body: { + key: 'The group has a new member. Say hi to {strong_}{name}{_strong}!', + args: { + name: `${CHATROOM_MEMBER_MENTION_SPECIAL_CHAR}${data.memberID}`, + ...LTags('strong') + } + }, icon: 'user-plus', level: 'info', - linkTo: `/group-chat/${rootState[data.groupID]?.generalChatRoomId}`, + linkTo: `/group-chat/${sbp('state/vuex/state')[data.groupID]?.generalChatRoomId}`, scope: 'group' } }, MEMBER_LEFT (data: { groupID: string, memberID: string }) { - const name = strong(userDisplayNameFromID(data.memberID)) return { avatarUserID: data.memberID, - body: L('{name} has left your group. Contributions were updated accordingly.', { - name - }), + body: { + key: '{strong_}{name}{_strong} has left your group. Contributions were updated accordingly.', + args: { + name: `${CHATROOM_MEMBER_MENTION_SPECIAL_CHAR}${data.memberID}`, + ...LTags('strong') + } + }, icon: 'user-minus', level: 'danger', linkTo: '/contributions', @@ -128,13 +129,16 @@ export default ({ } }, MEMBER_REMOVED (data: { groupID: string, memberID: string }) { - const name = strong(userDisplayNameFromID(data.memberID)) return { avatarUserID: data.memberID, // REVIEW @mmbotelho - Not only contributions, but also proposals. - body: L('{name} was kicked out of the group. Contributions were updated accordingly.', { - name - }), + body: { + key: '{strong_}{name}{_strong} was kicked out of the group. Contributions were updated accordingly.', + args: { + name: `${CHATROOM_MEMBER_MENTION_SPECIAL_CHAR}${data.memberID}`, + ...LTags('strong') + } + }, icon: 'user-minus', level: 'danger', linkTo: '/contributions', @@ -142,14 +146,17 @@ export default ({ } }, NEW_PROPOSAL (data: { groupID: string, creatorID: string, subtype: NewProposalType }) { - const name = strong(userDisplayNameFromID(data.creatorID)) + const args = { + name: `${CHATROOM_MEMBER_MENTION_SPECIAL_CHAR}${data.creatorID}`, + ...LTags('strong') + } const bodyTemplateMap = { - ADD_MEMBER: () => L('{name} proposed to add a member to the group. Vote now!', { name }), - CHANGE_MINCOME: () => L('{name} proposed to change the group mincome. Vote now!', { name }), - CHANGE_DISTRIBUTION_DATE: () => L('{name} proposed to change the group distribution date. Vote now!', { name }), - CHANGE_VOTING_RULE: () => L('{name} proposed to change the group voting system. Vote now!', { name }), - REMOVE_MEMBER: () => L('{name} proposed to remove a member from the group. Vote now!', { name }), - GENERIC: () => L('{name} created a proposal. Vote now!', { name }) + ADD_MEMBER: () => ({ key: '{strong_}{name}{_strong} proposed to add a member to the group. Vote now!', args }), + CHANGE_MINCOME: () => ({ key: '{strong_}{name}{_strong} proposed to change the group mincome. Vote now!', args }), + CHANGE_DISTRIBUTION_DATE: () => ({ key: '{strong_}{name}{_strong} proposed to change the group distribution date. Vote now!', args }), + CHANGE_VOTING_RULE: () => ({ key: '{strong_}{name}{_strong} proposed to change the group voting system. Vote now!', args }), + REMOVE_MEMBER: () => ({ key: '{strong_}{name}{_strong} proposed to remove a member from the group. Vote now!', args }), + GENERIC: () => ({ key: '{strong_}{name}{_strong} created a proposal. Vote now!', args }) } const iconMap = { @@ -187,10 +194,10 @@ export default ({ return { avatarUserID: '', - body: L('Proposal about to expire: {i_}"{proposalTitle}"{_i}. Please vote!', { - ...LTags('i'), - proposalTitle: typeToTitleMap[proposalType] - }), + body: { + key: 'Proposal about to expire: {i_}"{proposalTitle}"{_i}. Please vote!', + args: { ...LTags('i'), proposalTitle: typeToTitleMap[proposalType] } + }, level: 'info', icon: 'exclamation-triangle', scope: 'group', @@ -200,33 +207,40 @@ export default ({ }, PROPOSAL_CLOSED (data: { proposal: Object }) { const { creatorID, status, type, options } = getProposalDetails(data.proposal) + const isMe = sbp('state/vuex/getters').ourIdentityContractId === creatorID + const statusMap = { + [STATUS_PASSED]: { icon: 'check', level: 'success', closedWith: L('accepted') }, + [STATUS_FAILED]: { icon: 'times', level: 'danger', closedWith: L('rejected') }, + [STATUS_CANCELLED]: { icon: 'times', level: 'danger', closedWith: L('cancelled') }, // TODO: define icon, level + [STATUS_EXPIRED]: { icon: 'times', level: 'danger', closedWith: L('expired') } // TODO: define icon, level + } + const args = { ...options, closedWith: statusMap[status].closedWith } + + const makeBodyBasedOnCreator = (key) => { + return isMe + ? { key: '{strong_}Your{_strong} ' + key, args } + : { + key: "{strong_}{name}'s{_strong} " + key, + args: { ...args, name: `${CHATROOM_MEMBER_MENTION_SPECIAL_CHAR}${creatorID}` } + } + } const bodyTemplateMap = { [PROPOSAL_INVITE_MEMBER]: - (opts) => L('{creator} proposal to add {member} to the group was {closedWith}.', opts), + () => makeBodyBasedOnCreator('proposal to add {member} to the group was {strong_}{closedWith}{_strong}.'), [PROPOSAL_REMOVE_MEMBER]: - (opts) => L('{creator} proposal to remove {member} from the group was {closedWith}.', opts), + () => makeBodyBasedOnCreator('proposal to remove {member} from the group was {strong_}{closedWith}{_strong}.'), [PROPOSAL_GROUP_SETTING_CHANGE]: - (opts) => L('{creator} proposal to change group\'s {setting} to {value} was {closedWith}.', opts), + () => makeBodyBasedOnCreator("proposal to change group's {setting} to {value} was {strong_}{closedWith}{_strong}."), [PROPOSAL_PROPOSAL_SETTING_CHANGE]: - (opts) => L('{creator} proposal to change group\'s {setting} was {closedWith}.', opts), // TODO: define message + () => makeBodyBasedOnCreator("proposal to change group's {setting} was {strong_}{closedWith}{_strong}."), [PROPOSAL_GENERIC]: - (opts) => L('{creator} proposal "{title}" was {closedWith}.', opts) - } - const statusMap = { - [STATUS_PASSED]: { icon: 'check', level: 'success', closedWith: L('accepted') }, - [STATUS_FAILED]: { icon: 'times', level: 'danger', closedWith: L('rejected') }, - [STATUS_CANCELLED]: { icon: 'times', level: 'danger', closedWith: L('cancelled') }, // TODO: define icon, level - [STATUS_EXPIRED]: { icon: 'times', level: 'danger', closedWith: L('expired') } // TODO: define icon, level + () => makeBodyBasedOnCreator('proposal "{title}" was {strong_}{closedWith}{_strong}.') } return { avatarUserID: creatorID, - body: bodyTemplateMap[type]({ - ...options, - creator: L('{name}\'s', { name: strong(userDisplayNameFromID(creatorID)) }), // TODO: display YOUR - closedWith: strong(statusMap[status].closedWith) - }), + body: bodyTemplateMap[type](), icon: statusMap[status].icon, level: statusMap[status].level, linkTo: '/dashboard#proposals', @@ -234,15 +248,16 @@ export default ({ } }, PAYMENT_RECEIVED (data: { creatorID: string, amount: string, paymentHash: string }) { - const { userDisplayNameFromID } = sbp('state/vuex/getters') - return { avatarUserID: data.creatorID, - body: L('{name} sent you a {amount} mincome contribution. {strong_}Review and send a thank you note.{_strong}', { - name: userDisplayNameFromID(data.creatorID), // displayName of the sender - amount: data.amount, - ...LTags('strong') - }), + body: { + key: '{strong_}{name}{_strong} sent you a {amount} mincome contribution. {strong_}Review and send a thank you note.{_strong}', + args: { + name: `${CHATROOM_MEMBER_MENTION_SPECIAL_CHAR}${data.creatorID}`, + amount: data.amount, + ...LTags('strong') + } + }, creatorID: data.creatorID, icon: '', level: 'info', @@ -253,10 +268,13 @@ export default ({ PAYMENT_THANKYOU_SENT (data: { creatorID: string, fromMemberID: string, toMemberID: string }) { return { avatarUserID: data.creatorID, - body: L('{name} sent you a {strong_}thank you note{_strong} for your contribution.', { - name: strong(userDisplayNameFromID(data.fromMemberID)), - ...LTags('strong') - }), + body: { + key: '{strong_}{name}{_strong} sent you a {strong_}thank you note{_strong} for your contribution.', + args: { + name: `${CHATROOM_MEMBER_MENTION_SPECIAL_CHAR}${data.fromMemberID}`, + ...LTags('strong') + } + }, creatorID: data.creatorID, icon: '', level: 'info', @@ -268,7 +286,10 @@ export default ({ const { withGroupCurrency } = sbp('state/vuex/getters') return { avatarUserID: data.creatorID, - body: L('The mincome has changed to {amount}.', { amount: withGroupCurrency(data.to) }), + body: { + key: 'The mincome has changed to {amount}.', + args: { amount: withGroupCurrency(data.to) } + }, creatorID: data.creatorID, icon: 'dollar-sign', level: 'info', @@ -285,16 +306,22 @@ export default ({ }, NEW_DISTRIBUTION_PERIOD (data: { creatorID: string, memberType: string }) { const distPeriod = sbp('state/vuex/getters').groupSettings?.distributionDate - const periodDisplay = humanDate(distPeriod, { month: 'short', day: 'numeric', year: 'numeric' }) + const args = { period: humanDate(distPeriod, { month: 'short', day: 'numeric', year: 'numeric' }) } const bodyTemplate = { // Display the distribution period in the notification message (issue: https://github.com/okTurtles/group-income/issues/1903) - 'pledger': L('A new distribution period ({period}) has started. Please check Payment TODOs.', { period: periodDisplay }), - 'receiver': L('A new distribution period ({period}) has started. Please update your income details if they have changed.', { period: periodDisplay }) + 'pledger': () => ({ + key: 'A new distribution period ({period}) has started. Please check Payment TODOs.', + args + }), + 'receiver': () => ({ + key: 'A new distribution period ({period}) has started. Please update your income details if they have changed.', + args + }) } return { avatarUserID: data.creatorID, - body: bodyTemplate[data.memberType], + body: bodyTemplate[data.memberType](), level: 'info', icon: 'coins', linkTo: data.memberType === 'pledger' ? '/payments' : '/contributions?modal=IncomeDetails', @@ -307,7 +334,9 @@ export default ({ }, NEAR_DISTRIBUTION_END (data: { period: string }) { return { - body: L("Less than 1 week left before the distribution period ends - don't forget to send payments!"), + body: { + key: "Less than 1 week left before the distribution period ends - don't forget to send payments!" + }, level: 'info', icon: 'coins', linkTo: '/payments', diff --git a/frontend/model/notifications/types.flow.js b/frontend/model/notifications/types.flow.js index b6d4fb1fff..89064c8df2 100644 --- a/frontend/model/notifications/types.flow.js +++ b/frontend/model/notifications/types.flow.js @@ -10,7 +10,8 @@ export type NewProposalType = export type Notification = { // Indicates which user avatar icon to display alongside the notification. +avatarUserID: string; - +body: string; + // object-formatted body will be of format { key: '', args?: { ... } } and needs to be translated by i18n + +body: string | Object; // If present, indicates in which group's notification list to display the notification. +groupID?: string; +icon: string; @@ -35,7 +36,7 @@ export type NotificationScope = 'group' | 'user' | 'app'; export type NotificationTemplate = { +avatarUserID?: string; - +body: string; + +body: string | Object; +icon: string; +level: NotificationLevel; +linkTo?: string; diff --git a/frontend/views/containers/notifications/NotificationList.vue b/frontend/views/containers/notifications/NotificationList.vue index 58c281a128..2d2fc70783 100644 --- a/frontend/views/containers/notifications/NotificationList.vue +++ b/frontend/views/containers/notifications/NotificationList.vue @@ -28,7 +28,7 @@ avatar-user(:contractID='item.avatarUserID' size='md') i(v-if='item.icon' :class='`icon-${item.icon} ${iconBg(item.level)}`') span.c-item-content - span.c-item-text(v-safe-html='item.body') + span.c-item-text(v-safe-html='renderNotificationBody(item.body)') span.c-item-date.has-text-1.has-text-small {{ ageTag(item) }} @@ -38,6 +38,7 @@ import { mapGetters } from 'vuex' import { timeSince } from '@model/contracts/shared/time.js' import AvatarUser from '@components/AvatarUser.vue' import { L } from '@common/common.js' +import { swapMentionIDForDisplayname } from '@model/contracts/shared/functions.js' export default ({ name: 'NotificationList', @@ -106,6 +107,13 @@ export default ({ }, markAsRead (item: Object): void { sbp('gi.notifications/markAsRead', item) + }, + renderNotificationBody (body: string | Object): string { + const text = typeof body === 'string' ? body : L(body.key, body.args || {}) + return swapMentionIDForDisplayname(text, { + escaped: false, + forChat: false + }) } } }: Object) diff --git a/package.json b/package.json index d5ae205969..bd3148ff13 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "group-income", - "version": "0.4.1", + "version": "0.4.2", "contractsVersion": "0.4.1", "private": true, "description": "", From 8b7de9c1e13a9be655592c7657d91554e161621d Mon Sep 17 00:00:00 2001 From: silver-it Date: Fri, 14 Jun 2024 08:55:49 +0800 Subject: [PATCH 05/11] chore: uncommented the codes from historical MessageInteractive.vue --- .../chatroom/MessageInteractive.vue | 52 ++++++++++--------- 1 file changed, 28 insertions(+), 24 deletions(-) diff --git a/historical/views/containers/chatroom/MessageInteractive.vue b/historical/views/containers/chatroom/MessageInteractive.vue index 133962e026..4620101eea 100644 --- a/historical/views/containers/chatroom/MessageInteractive.vue +++ b/historical/views/containers/chatroom/MessageInteractive.vue @@ -25,6 +25,10 @@ import { PROPOSAL_REMOVE_MEMBER, PROPOSAL_PROPOSAL_SETTING_CHANGE, PROPOSAL_GENERIC, + STATUS_OPEN, + STATUS_PASSED, + STATUS_FAILED, + STATUS_EXPIRED, STATUS_EXPIRING } from '@model/contracts/shared/constants.js' import { getProposalDetails } from '@model/contracts/shared/functions.js' @@ -40,26 +44,26 @@ const interactiveMessage = (proposal, baseOptions = {}) => { const options = Object.assign(proposalDetails, baseOptions) const settingChangeMessages = (options) => ({ - // [STATUS_OPEN]: L('{from} wants to change the groups {setting}.', options), - // [STATUS_PASSED]: L('Proposal from {from} to change the {setting} is accepted.', options), - // [STATUS_FAILED]: L('Proposal from {from} to change the {setting} is rejected.', options), - // [STATUS_EXPIRED]: L('Proposal from {from} to change the {setting} is expired.', options), + [STATUS_OPEN]: L('{from} wants to change the groups {setting}.', options), + [STATUS_PASSED]: L('Proposal from {from} to change the {setting} is accepted.', options), + [STATUS_FAILED]: L('Proposal from {from} to change the {setting} is rejected.', options), + [STATUS_EXPIRED]: L('Proposal from {from} to change the {setting} is expired.', options), [STATUS_EXPIRING]: L('Proposal from {from} to change the {setting} is expiring.', options) }) const interactiveMessages = { [PROPOSAL_INVITE_MEMBER]: { - // [STATUS_OPEN]: L('{from} wants to add {member} to the group.', options), - // [STATUS_PASSED]: L('Proposal from {from} to add {member} is accepted.', options), - // [STATUS_FAILED]: L('Proposal from {from} to add {member} is rejected.', options), - // [STATUS_EXPIRED]: L('Proposal from {from} to add {member} is expired.', options), + [STATUS_OPEN]: L('{from} wants to add {member} to the group.', options), + [STATUS_PASSED]: L('Proposal from {from} to add {member} is accepted.', options), + [STATUS_FAILED]: L('Proposal from {from} to add {member} is rejected.', options), + [STATUS_EXPIRED]: L('Proposal from {from} to add {member} is expired.', options), [STATUS_EXPIRING]: L('Proposal from {from} to add {member} is expiring.', options) }, [PROPOSAL_REMOVE_MEMBER]: { - // [STATUS_OPEN]: L('{from} wants to remove {member} from the group.', options), - // [STATUS_PASSED]: L('Proposal from {from} to remove {member} is accepted.', options), - // [STATUS_FAILED]: L('Proposal from {from} to add {member} is rejected.', options), - // [STATUS_EXPIRED]: L('Proposal from {from} to add {member} is expired.', options), + [STATUS_OPEN]: L('{from} wants to remove {member} from the group.', options), + [STATUS_PASSED]: L('Proposal from {from} to remove {member} is accepted.', options), + [STATUS_FAILED]: L('Proposal from {from} to add {member} is rejected.', options), + [STATUS_EXPIRED]: L('Proposal from {from} to add {member} is expired.', options), [STATUS_EXPIRING]: L('Proposal from {from} to remove {member} is expiring.', options) }, [PROPOSAL_GROUP_SETTING_CHANGE]: { @@ -71,10 +75,10 @@ const interactiveMessage = (proposal, baseOptions = {}) => { votingSystem: settingChangeMessages(options) }, [PROPOSAL_GENERIC]: { - // [STATUS_OPEN]: L('{from} created a proposal. "{title}"', options), - // [STATUS_PASSED]: L('Proposal from {from} is accepted. "{title}"', options), - // [STATUS_FAILED]: L('Proposal from {from} is rejected. "{title}"', options), - // [STATUS_EXPIRED]: L('Proposal from {from} is expired. "{title}"', options), + [STATUS_OPEN]: L('{from} created a proposal. "{title}"', options), + [STATUS_PASSED]: L('Proposal from {from} is accepted. "{title}"', options), + [STATUS_FAILED]: L('Proposal from {from} is rejected. "{title}"', options), + [STATUS_EXPIRED]: L('Proposal from {from} is expired. "{title}"', options), [STATUS_EXPIRING]: L('Proposal from {from} is expiring. "{title}"', options) } } @@ -88,19 +92,19 @@ const proposalStatus = (proposal) => { options['date'] = humanDate(proposal.expires_date_ms, { month: 'short', day: 'numeric', year: 'numeric' }) } return { - // [STATUS_OPEN]: L('New proposal'), - // [STATUS_PASSED]: L('Proposal Accepted'), - // [STATUS_FAILED]: L('Proposal rejected'), - // [STATUS_EXPIRED]: L('Proposal expired'), + [STATUS_OPEN]: L('New proposal'), + [STATUS_PASSED]: L('Proposal Accepted'), + [STATUS_FAILED]: L('Proposal rejected'), + [STATUS_EXPIRED]: L('Proposal expired'), [STATUS_EXPIRING]: L('Proposal expiring on {date}', options) }[proposal.variant] } const proposalSeverity = { - // [STATUS_OPEN]: 'is-info', - // [STATUS_PASSED]: 'is-success', - // [STATUS_FAILED]: 'is-danger', - // [STATUS_EXPIRED]: 'is-neutral', + [STATUS_OPEN]: 'is-info', + [STATUS_PASSED]: 'is-success', + [STATUS_FAILED]: 'is-danger', + [STATUS_EXPIRED]: 'is-neutral', [STATUS_EXPIRING]: 'is-warning' } From fea1963557dc37108240275e5a5a9868f29e29da Mon Sep 17 00:00:00 2001 From: silver-it Date: Tue, 25 Jun 2024 07:21:17 +0800 Subject: [PATCH 06/11] chore: reverted changes according to the feedbackc --- frontend/controller/actions/group.js | 37 +++- frontend/model/contracts/shared/constants.js | 1 + frontend/model/contracts/shared/functions.js | 32 ++- frontend/model/contracts/shared/types.js | 16 +- frontend/model/notifications/templates.js | 163 ++++++-------- frontend/model/notifications/types.flow.js | 5 +- .../views/containers/chatroom/ChatMain.vue | 1 + .../chatroom/MessageInteractive.vue | 126 ++++++++++- .../notifications/NotificationList.vue | 5 +- .../chatroom/MessageInteractive.vue | 209 ------------------ 10 files changed, 267 insertions(+), 328 deletions(-) delete mode 100644 historical/views/containers/chatroom/MessageInteractive.vue diff --git a/frontend/controller/actions/group.js b/frontend/controller/actions/group.js index 96889d5c8f..557e647741 100644 --- a/frontend/controller/actions/group.js +++ b/frontend/controller/actions/group.js @@ -6,13 +6,15 @@ 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, - STATUS_OPEN + STATUS_OPEN, + STATUS_EXPIRING } from '@model/contracts/shared/constants.js' import { merge, omit, randomIntFromRange } from '@model/contracts/shared/giLodash.js' import { addTimeToDate, dateToPeriodStamp, DAYS_MILLIS } from '@model/contracts/shared/time.js' @@ -842,6 +844,38 @@ 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: STATUS_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 @@ -951,7 +985,6 @@ 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.')) diff --git a/frontend/model/contracts/shared/constants.js b/frontend/model/contracts/shared/constants.js index f2cfba7b50..497ad8f61e 100644 --- a/frontend/model/contracts/shared/constants.js +++ b/frontend/model/contracts/shared/constants.js @@ -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' diff --git a/frontend/model/contracts/shared/functions.js b/frontend/model/contracts/shared/functions.js index a3e9282107..1e636e36c2 100644 --- a/frontend/model/contracts/shared/functions.js +++ b/frontend/model/contracts/shared/functions.js @@ -216,7 +216,10 @@ export function makeChannelMention (channelName: string): string { export function swapMentionIDForDisplayname ( text: string, - options: Object = { escaped: true, forChat: true } + 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, @@ -235,11 +238,24 @@ export function swapMentionIDForDisplayname ( ? 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('') + 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(regEx) + .map(t => possibleMentions.includes(t) ? swap(t) : t) + .join('') } diff --git a/frontend/model/contracts/shared/types.js b/frontend/model/contracts/shared/types.js index 0a43922b37..5c788f0e74 100644 --- a/frontend/model/contracts/shared/types.js +++ b/frontend/model/contracts/shared/types.js @@ -4,7 +4,13 @@ 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 } from './constants.js' +import { + CHATROOM_TYPES, + CHATROOM_PRIVACY_LEVEL, + MESSAGE_TYPES, + MESSAGE_NOTIFICATIONS, + STATUS_EXPIRING +} from './constants.js' // group.js related @@ -29,6 +35,14 @@ export const chatRoomAttributesType: any = objectOf({ export const messageType: any = objectMaybeOf({ type: unionOf(...Object.values(MESSAGE_TYPES).map(v => literalOf(v))), text: string, + proposal: objectMaybeOf({ + proposalId: string, + proposalType: string, + expires_date_ms: number, + createdDate: string, + creatorID: string, + 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))), params: mapOf(string, string) // { username } diff --git a/frontend/model/notifications/templates.js b/frontend/model/notifications/templates.js index a98aab59ef..82710ee88a 100644 --- a/frontend/model/notifications/templates.js +++ b/frontend/model/notifications/templates.js @@ -26,18 +26,15 @@ export default ({ } return { - body: { - key: "{errName} during {activity} for '{action}' from {b_}{who}{_b} to '{contract}': '{errMsg}'", - args: { - ...LTags('b'), - errName: error.name, - activity, - action: action ?? opType, - who: `${CHATROOM_MEMBER_MENTION_SPECIAL_CHAR}${message.signingKeyId()}`, - contract: sbp('state/vuex/state').contracts[contractID]?.type ?? contractID, - errMsg: error.message ?? '?' - } - }, + body: L("{errName} during {activity} for '{action}' from {b_}{who}{_b} to '{contract}': '{errMsg}'", { + ...LTags('b'), + errName: error.name, + activity, + action: action ?? opType, + who: `${CHATROOM_MEMBER_MENTION_SPECIAL_CHAR}${message.signingKeyId()}`, + contract: sbp('state/vuex/state').contracts[contractID]?.type ?? contractID, + errMsg: error.message ?? '?' + }), icon: 'exclamation-triangle', level: 'danger', linkTo: `/app/dashboard?modal=UserSettingsModal&tab=application-logs&errorMsg=${encodeURI(error.message)}`, @@ -73,10 +70,9 @@ export default ({ }, CONTRIBUTION_REMINDER (data: { date: string }) { return { - body: { - key: 'Do not forget to send your pledge by {strong_}{date}{_strong}.', - args: { ...LTags('strong') } - }, + body: L('Do not forget to send your pledge by {strong_}{date}{_strong}.', { + ...LTags('strong') + }), icon: 'coins', level: 'info', linkTo: '/payments', @@ -85,10 +81,9 @@ export default ({ }, INCOME_DETAILS_OLD (data: { months: number, lastUpdatedDate: string }) { return { - body: { - key: "You haven't updated your income details in more than {months} months. Would you like to review them now?", - args: { months: Math.floor(data.months) } // Avoid displaying decimals - }, + body: L("You haven't updated your income details in more than {months} months. Would you like to review them now?", { + months: Math.floor(data.months) // Avoid displaying decimals + }), icon: 'coins', level: 'info', linkTo: '/contributions?modal=IncomeDetails', @@ -99,13 +94,10 @@ export default ({ MEMBER_ADDED (data: { groupID: string, memberID: string }) { return { avatarUserID: data.memberID, - body: { - key: 'The group has a new member. Say hi to {strong_}{name}{_strong}!', - args: { - name: `${CHATROOM_MEMBER_MENTION_SPECIAL_CHAR}${data.memberID}`, - ...LTags('strong') - } - }, + body: L('The group has a new member. Say hi to {strong_}{name}{_strong}!', { + name: `${CHATROOM_MEMBER_MENTION_SPECIAL_CHAR}${data.memberID}`, + ...LTags('strong') + }), icon: 'user-plus', level: 'info', linkTo: `/group-chat/${sbp('state/vuex/state')[data.groupID]?.generalChatRoomId}`, @@ -115,13 +107,10 @@ export default ({ MEMBER_LEFT (data: { groupID: string, memberID: string }) { return { avatarUserID: data.memberID, - body: { - key: '{strong_}{name}{_strong} has left your group. Contributions were updated accordingly.', - args: { - name: `${CHATROOM_MEMBER_MENTION_SPECIAL_CHAR}${data.memberID}`, - ...LTags('strong') - } - }, + body: L('{strong_}{name}{_strong} has left your group. Contributions were updated accordingly.', { + name: `${CHATROOM_MEMBER_MENTION_SPECIAL_CHAR}${data.memberID}`, + ...LTags('strong') + }), icon: 'user-minus', level: 'danger', linkTo: '/contributions', @@ -132,13 +121,10 @@ export default ({ return { avatarUserID: data.memberID, // REVIEW @mmbotelho - Not only contributions, but also proposals. - body: { - key: '{strong_}{name}{_strong} was kicked out of the group. Contributions were updated accordingly.', - args: { - name: `${CHATROOM_MEMBER_MENTION_SPECIAL_CHAR}${data.memberID}`, - ...LTags('strong') - } - }, + body: L('{strong_}{name}{_strong} was kicked out of the group. Contributions were updated accordingly.', { + name: `${CHATROOM_MEMBER_MENTION_SPECIAL_CHAR}${data.memberID}`, + ...LTags('strong') + }), icon: 'user-minus', level: 'danger', linkTo: '/contributions', @@ -151,12 +137,12 @@ export default ({ ...LTags('strong') } const bodyTemplateMap = { - ADD_MEMBER: () => ({ key: '{strong_}{name}{_strong} proposed to add a member to the group. Vote now!', args }), - CHANGE_MINCOME: () => ({ key: '{strong_}{name}{_strong} proposed to change the group mincome. Vote now!', args }), - CHANGE_DISTRIBUTION_DATE: () => ({ key: '{strong_}{name}{_strong} proposed to change the group distribution date. Vote now!', args }), - CHANGE_VOTING_RULE: () => ({ key: '{strong_}{name}{_strong} proposed to change the group voting system. Vote now!', args }), - REMOVE_MEMBER: () => ({ key: '{strong_}{name}{_strong} proposed to remove a member from the group. Vote now!', args }), - GENERIC: () => ({ key: '{strong_}{name}{_strong} created a proposal. Vote now!', args }) + ADD_MEMBER: L('{strong_}{name}{_strong} proposed to add a member to the group. Vote now!', args), + CHANGE_MINCOME: L('{strong_}{name}{_strong} proposed to change the group mincome. Vote now!', args), + CHANGE_DISTRIBUTION_DATE: L('{strong_}{name}{_strong} proposed to change the group distribution date. Vote now!', args), + CHANGE_VOTING_RULE: L('{strong_}{name}{_strong} proposed to change the group voting system. Vote now!', args), + REMOVE_MEMBER: L('{strong_}{name}{_strong} proposed to remove a member from the group. Vote now!', args), + GENERIC: L('{strong_}{name}{_strong} created a proposal. Vote now!', args) } const iconMap = { @@ -170,7 +156,7 @@ export default ({ return { avatarUserID: data.creatorID, - body: bodyTemplateMap[data.subtype](), + body: bodyTemplateMap[data.subtype], creatorID: data.creatorID, icon: iconMap[data.subtype], level: 'info', @@ -194,10 +180,10 @@ export default ({ return { avatarUserID: '', - body: { - key: 'Proposal about to expire: {i_}"{proposalTitle}"{_i}. Please vote!', - args: { ...LTags('i'), proposalTitle: typeToTitleMap[proposalType] } - }, + body: L('Proposal about to expire: {i_}"{proposalTitle}"{_i}. Please vote!', { + ...LTags('i'), + proposalTitle: typeToTitleMap[proposalType] + }), level: 'info', icon: 'exclamation-triangle', scope: 'group', @@ -215,32 +201,28 @@ export default ({ [STATUS_CANCELLED]: { icon: 'times', level: 'danger', closedWith: L('cancelled') }, // TODO: define icon, level [STATUS_EXPIRED]: { icon: 'times', level: 'danger', closedWith: L('expired') } // TODO: define icon, level } - const args = { ...options, closedWith: statusMap[status].closedWith } - - const makeBodyBasedOnCreator = (key) => { - return isMe - ? { key: '{strong_}Your{_strong} ' + key, args } - : { - key: "{strong_}{name}'s{_strong} " + key, - args: { ...args, name: `${CHATROOM_MEMBER_MENTION_SPECIAL_CHAR}${creatorID}` } - } + const args = { + ...options, + closedWith: statusMap[status].closedWith, + name: `${CHATROOM_MEMBER_MENTION_SPECIAL_CHAR}${creatorID}` } + const bodyTemplateMap = { [PROPOSAL_INVITE_MEMBER]: - () => makeBodyBasedOnCreator('proposal to add {member} to the group was {strong_}{closedWith}{_strong}.'), + L("{strong_}{name}'s{_strong} proposal to add {member} to the group was {strong_}{closedWith}{_strong}.", args), [PROPOSAL_REMOVE_MEMBER]: - () => makeBodyBasedOnCreator('proposal to remove {member} from the group was {strong_}{closedWith}{_strong}.'), + L("{strong_}{name}'s{_strong} proposal to remove {member} from the group was {strong_}{closedWith}{_strong}.", args), [PROPOSAL_GROUP_SETTING_CHANGE]: - () => makeBodyBasedOnCreator("proposal to change group's {setting} to {value} was {strong_}{closedWith}{_strong}."), + L("{strong_}{name}'s{_strong} proposal to change group's {setting} to {value} was {strong_}{closedWith}{_strong}.", args), [PROPOSAL_PROPOSAL_SETTING_CHANGE]: - () => makeBodyBasedOnCreator("proposal to change group's {setting} was {strong_}{closedWith}{_strong}."), + L("{strong_}{name}'s{_strong} proposal to change group's {setting} was {strong_}{closedWith}{_strong}.", args), [PROPOSAL_GENERIC]: - () => makeBodyBasedOnCreator('proposal "{title}" was {strong_}{closedWith}{_strong}.') + L("{strong_}{name}'s{_strong} proposal "{title}" was {strong_}{closedWith}{_strong}.", args) } return { avatarUserID: creatorID, - body: bodyTemplateMap[type](), + body: bodyTemplateMap[type], icon: statusMap[status].icon, level: statusMap[status].level, linkTo: '/dashboard#proposals', @@ -250,14 +232,11 @@ export default ({ PAYMENT_RECEIVED (data: { creatorID: string, amount: string, paymentHash: string }) { return { avatarUserID: data.creatorID, - body: { - key: '{strong_}{name}{_strong} sent you a {amount} mincome contribution. {strong_}Review and send a thank you note.{_strong}', - args: { - name: `${CHATROOM_MEMBER_MENTION_SPECIAL_CHAR}${data.creatorID}`, - amount: data.amount, - ...LTags('strong') - } - }, + body: L('{strong_}{name}{_strong} sent you a {amount} mincome contribution. {strong_}Review and send a thank you note.{_strong}', { + name: `${CHATROOM_MEMBER_MENTION_SPECIAL_CHAR}${data.creatorID}`, + amount: data.amount, + ...LTags('strong') + }), creatorID: data.creatorID, icon: '', level: 'info', @@ -268,13 +247,10 @@ export default ({ PAYMENT_THANKYOU_SENT (data: { creatorID: string, fromMemberID: string, toMemberID: string }) { return { avatarUserID: data.fromMemberID, - body: { - key: '{strong_}{name}{_strong} sent you a {strong_}thank you note{_strong} for your contribution.', - args: { - name: `${CHATROOM_MEMBER_MENTION_SPECIAL_CHAR}${data.fromMemberID}`, - ...LTags('strong') - } - }, + body: L('{strong_}{name}{_strong} sent you a {strong_}thank you note{_strong} for your contribution.', { + name: `${CHATROOM_MEMBER_MENTION_SPECIAL_CHAR}${data.fromMemberID}`, + ...LTags('strong') + }), creatorID: data.fromMemberID, icon: '', level: 'info', @@ -286,10 +262,9 @@ export default ({ const { withGroupCurrency } = sbp('state/vuex/getters') return { avatarUserID: data.creatorID, - body: { - key: 'The mincome has changed to {amount}.', - args: { amount: withGroupCurrency(data.to) } - }, + body: L('The mincome has changed to {amount}.', { + amount: withGroupCurrency(data.to) + }), creatorID: data.creatorID, icon: 'dollar-sign', level: 'info', @@ -309,19 +284,13 @@ export default ({ const args = { period: humanDate(distPeriod, { month: 'short', day: 'numeric', year: 'numeric' }) } const bodyTemplate = { // Display the distribution period in the notification message (issue: https://github.com/okTurtles/group-income/issues/1903) - 'pledger': () => ({ - key: 'A new distribution period ({period}) has started. Please check Payment TODOs.', - args - }), - 'receiver': () => ({ - key: 'A new distribution period ({period}) has started. Please update your income details if they have changed.', - args - }) + pledger: L('A new distribution period ({period}) has started. Please check Payment TODOs.', args), + receiver: L('A new distribution period ({period}) has started. Please update your income details if they have changed.', args) } return { avatarUserID: data.creatorID, - body: bodyTemplate[data.memberType](), + body: bodyTemplate[data.memberType], level: 'info', icon: 'coins', linkTo: data.memberType === 'pledger' ? '/payments' : '/contributions?modal=IncomeDetails', @@ -334,9 +303,7 @@ export default ({ }, NEAR_DISTRIBUTION_END (data: { period: string }) { return { - body: { - key: "Less than 1 week left before the distribution period ends - don't forget to send payments!" - }, + body: L("Less than 1 week left before the distribution period ends - don't forget to send payments!"), level: 'info', icon: 'coins', linkTo: '/payments', diff --git a/frontend/model/notifications/types.flow.js b/frontend/model/notifications/types.flow.js index 89064c8df2..25999aacbf 100644 --- a/frontend/model/notifications/types.flow.js +++ b/frontend/model/notifications/types.flow.js @@ -3,6 +3,7 @@ export type NewProposalType = | 'ADD_MEMBER' | 'CHANGE_MINCOME' + | 'CHANGE_DISTRIBUTION_DATE' | 'CHANGE_VOTING_RULE' | 'REMOVE_MEMBER' | 'GENERIC'; @@ -11,7 +12,7 @@ export type Notification = { // Indicates which user avatar icon to display alongside the notification. +avatarUserID: string; // object-formatted body will be of format { key: '', args?: { ... } } and needs to be translated by i18n - +body: string | Object; + +body: string; // If present, indicates in which group's notification list to display the notification. +groupID?: string; +icon: string; @@ -36,7 +37,7 @@ export type NotificationScope = 'group' | 'user' | 'app'; export type NotificationTemplate = { +avatarUserID?: string; - +body: string | Object; + +body: string; +icon: string; +level: NotificationLevel; +linkTo?: string; diff --git a/frontend/views/containers/chatroom/ChatMain.vue b/frontend/views/containers/chatroom/ChatMain.vue index 29529283c0..83c01eb4be 100644 --- a/frontend/views/containers/chatroom/ChatMain.vue +++ b/frontend/views/containers/chatroom/ChatMain.vue @@ -69,6 +69,7 @@ :attachments='message.attachments' :type='message.type' :notification='message.notification' + :proposal='message.proposal' :pollData='message.pollData' :replyingMessage='replyingMessageText(message)' :datetime='time(message.datetime)' diff --git a/frontend/views/containers/chatroom/MessageInteractive.vue b/frontend/views/containers/chatroom/MessageInteractive.vue index 7d1c7513be..4620101eea 100644 --- a/frontend/views/containers/chatroom/MessageInteractive.vue +++ b/frontend/views/containers/chatroom/MessageInteractive.vue @@ -1,37 +1,145 @@ - - From be2ceb83d7a031b09d8e5d603e899e1fbcdfb810 Mon Sep 17 00:00:00 2001 From: silver-it Date: Tue, 25 Jun 2024 07:25:00 +0800 Subject: [PATCH 07/11] fix: tiny errors --- frontend/controller/actions/group.js | 2 +- frontend/model/contracts/shared/types.js | 2 +- frontend/model/notifications/templates.js | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/frontend/controller/actions/group.js b/frontend/controller/actions/group.js index 557e647741..9d33d8ee71 100644 --- a/frontend/controller/actions/group.js +++ b/frontend/controller/actions/group.js @@ -844,7 +844,7 @@ 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) { + ...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']), diff --git a/frontend/model/contracts/shared/types.js b/frontend/model/contracts/shared/types.js index 5c788f0e74..d33f73b3aa 100644 --- a/frontend/model/contracts/shared/types.js +++ b/frontend/model/contracts/shared/types.js @@ -35,7 +35,7 @@ export const chatRoomAttributesType: any = objectOf({ export const messageType: any = objectMaybeOf({ type: unionOf(...Object.values(MESSAGE_TYPES).map(v => literalOf(v))), text: string, - proposal: objectMaybeOf({ + proposal: objectMaybeOf({ proposalId: string, proposalType: string, expires_date_ms: number, diff --git a/frontend/model/notifications/templates.js b/frontend/model/notifications/templates.js index 82710ee88a..6762a30dc3 100644 --- a/frontend/model/notifications/templates.js +++ b/frontend/model/notifications/templates.js @@ -203,6 +203,7 @@ export default ({ } const args = { ...options, + ...LTags('strong'), closedWith: statusMap[status].closedWith, name: `${CHATROOM_MEMBER_MENTION_SPECIAL_CHAR}${creatorID}` } @@ -217,7 +218,7 @@ export default ({ [PROPOSAL_PROPOSAL_SETTING_CHANGE]: L("{strong_}{name}'s{_strong} proposal to change group's {setting} was {strong_}{closedWith}{_strong}.", args), [PROPOSAL_GENERIC]: - L("{strong_}{name}'s{_strong} proposal "{title}" was {strong_}{closedWith}{_strong}.", args) + L(`{strong_}{name}'s{_strong} proposal "{title}" was {strong_}{closedWith}{_strong}.`, args) } return { From 67405c162f9808a58e7fa2647feda250fe842a7e Mon Sep 17 00:00:00 2001 From: silver-it Date: Tue, 25 Jun 2024 07:26:26 +0800 Subject: [PATCH 08/11] fix: 2 small errors --- frontend/model/notifications/templates.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/frontend/model/notifications/templates.js b/frontend/model/notifications/templates.js index 6762a30dc3..addbfe4240 100644 --- a/frontend/model/notifications/templates.js +++ b/frontend/model/notifications/templates.js @@ -193,7 +193,6 @@ export default ({ }, PROPOSAL_CLOSED (data: { proposal: Object }) { const { creatorID, status, type, options } = getProposalDetails(data.proposal) - const isMe = sbp('state/vuex/getters').ourIdentityContractId === creatorID const statusMap = { [STATUS_PASSED]: { icon: 'check', level: 'success', closedWith: L('accepted') }, @@ -218,7 +217,7 @@ export default ({ [PROPOSAL_PROPOSAL_SETTING_CHANGE]: L("{strong_}{name}'s{_strong} proposal to change group's {setting} was {strong_}{closedWith}{_strong}.", args), [PROPOSAL_GENERIC]: - L(`{strong_}{name}'s{_strong} proposal "{title}" was {strong_}{closedWith}{_strong}.`, args) + L('{strong_}{name}\'s{_strong} proposal "{title}" was {strong_}{closedWith}{_strong}.', args) } return { From 2e710b303e113bf60bfca403e81ba488ad477905 Mon Sep 17 00:00:00 2001 From: silver-it Date: Tue, 25 Jun 2024 18:47:44 +0800 Subject: [PATCH 09/11] fix: error in using param in notifyExpiringProposal function --- frontend/controller/actions/group.js | 8 +++++--- frontend/model/notifications/mainNotificationsMixin.js | 2 +- frontend/model/notifications/templates.js | 10 ++++------ frontend/model/notifications/types.flow.js | 1 - 4 files changed, 10 insertions(+), 11 deletions(-) diff --git a/frontend/controller/actions/group.js b/frontend/controller/actions/group.js index b2675c7377..fa690b015c 100644 --- a/frontend/controller/actions/group.js +++ b/frontend/controller/actions/group.js @@ -13,8 +13,8 @@ import { PROPOSAL_INVITE_MEMBER, PROPOSAL_PROPOSAL_SETTING_CHANGE, PROPOSAL_REMOVE_MEMBER, - STATUS_OPEN, - STATUS_EXPIRING + STATUS_EXPIRING, + STATUS_OPEN } from '@model/contracts/shared/constants.js' import { merge, omit, randomIntFromRange } from '@model/contracts/shared/giLodash.js' import { addTimeToDate, dateToPeriodStamp, DAYS_MILLIS } from '@model/contracts/shared/time.js' @@ -852,7 +852,9 @@ 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 diff --git a/frontend/model/notifications/mainNotificationsMixin.js b/frontend/model/notifications/mainNotificationsMixin.js index c2c043569a..61b3541839 100644 --- a/frontend/model/notifications/mainNotificationsMixin.js +++ b/frontend/model/notifications/mainNotificationsMixin.js @@ -178,7 +178,7 @@ const periodicNotificationEntries = [ if (expiringProposals.length) { await sbp('gi.actions/group/notifyExpiringProposals', { contractID, - data: { proposalIds: expiringProposals.map(p => p.proposalId) } + data: { proposals: expiringProposals } }) } diff --git a/frontend/model/notifications/templates.js b/frontend/model/notifications/templates.js index addbfe4240..ccc53e34b9 100644 --- a/frontend/model/notifications/templates.js +++ b/frontend/model/notifications/templates.js @@ -262,9 +262,7 @@ export default ({ const { withGroupCurrency } = sbp('state/vuex/getters') return { avatarUserID: data.creatorID, - body: L('The mincome has changed to {amount}.', { - amount: withGroupCurrency(data.to) - }), + body: L('The mincome has changed to {amount}.', { amount: withGroupCurrency(data.to) }), creatorID: data.creatorID, icon: 'dollar-sign', level: 'info', @@ -281,11 +279,11 @@ export default ({ }, NEW_DISTRIBUTION_PERIOD (data: { creatorID: string, memberType: string }) { const distPeriod = sbp('state/vuex/getters').groupSettings?.distributionDate - const args = { period: humanDate(distPeriod, { month: 'short', day: 'numeric', year: 'numeric' }) } + const periodDisplay = humanDate(distPeriod, { month: 'short', day: 'numeric', year: 'numeric' }) const bodyTemplate = { // Display the distribution period in the notification message (issue: https://github.com/okTurtles/group-income/issues/1903) - pledger: L('A new distribution period ({period}) has started. Please check Payment TODOs.', args), - receiver: L('A new distribution period ({period}) has started. Please update your income details if they have changed.', args) + 'pledger': L('A new distribution period ({period}) has started. Please check Payment TODOs.', { period: periodDisplay }), + 'receiver': L('A new distribution period ({period}) has started. Please update your income details if they have changed.', { period: periodDisplay }) } return { diff --git a/frontend/model/notifications/types.flow.js b/frontend/model/notifications/types.flow.js index 25999aacbf..e7be95c00d 100644 --- a/frontend/model/notifications/types.flow.js +++ b/frontend/model/notifications/types.flow.js @@ -11,7 +11,6 @@ export type NewProposalType = export type Notification = { // Indicates which user avatar icon to display alongside the notification. +avatarUserID: string; - // object-formatted body will be of format { key: '', args?: { ... } } and needs to be translated by i18n +body: string; // If present, indicates in which group's notification list to display the notification. +groupID?: string; From 3be4f34f4f7a10dad60eb914852ee79507129cee Mon Sep 17 00:00:00 2001 From: silver-it Date: Fri, 28 Jun 2024 06:40:29 +0800 Subject: [PATCH 10/11] chore: tiny update according to the feedback --- frontend/model/notifications/templates.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/model/notifications/templates.js b/frontend/model/notifications/templates.js index 1931ab495d..20e4760cea 100644 --- a/frontend/model/notifications/templates.js +++ b/frontend/model/notifications/templates.js @@ -26,12 +26,11 @@ export default ({ } return { - body: L("{errName} during {activity} for '{action}' from {b_}{who}{_b} to '{contract}': '{errMsg}'", { + body: L("{errName} during {activity} for '{action}' to '{contract}': '{errMsg}'", { ...LTags('b'), errName: error.name, activity, action: action ?? opType, - who: `${CHATROOM_MEMBER_MENTION_SPECIAL_CHAR}${message.signingKeyId()}`, contract: sbp('state/vuex/state').contracts[contractID]?.type ?? contractID, errMsg: error.message ?? '?' }), @@ -92,6 +91,7 @@ export default ({ } }, MEMBER_ADDED (data: { groupID: string, memberID: string }) { + const rootState = sbp('state/vuex/state') return { avatarUserID: data.memberID, body: L('The group has a new member. Say hi to {strong_}{name}{_strong}!', { @@ -100,7 +100,7 @@ export default ({ }), icon: 'user-plus', level: 'info', - linkTo: `/group-chat/${sbp('state/vuex/state')[data.groupID]?.generalChatRoomId}`, + linkTo: `/group-chat/${rootState[data.groupID]?.generalChatRoomId}`, scope: 'group' } }, From 07f200ce89478fc5d83afec2d076ed554b577ee2 Mon Sep 17 00:00:00 2001 From: silver-it Date: Fri, 28 Jun 2024 08:31:12 +0800 Subject: [PATCH 11/11] chore: tiny update and travis retry --- frontend/controller/actions/group.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/frontend/controller/actions/group.js b/frontend/controller/actions/group.js index 87f7010c24..c0ef5b90e8 100644 --- a/frontend/controller/actions/group.js +++ b/frontend/controller/actions/group.js @@ -851,9 +851,7 @@ export default (sbp('sbp/selectors/register', { const { proposals } = params.data await sendMessage({ ...omit(params, ['options', 'data', 'action', 'hooks']), - data: { - proposalIds: proposals.map(p => p.proposalId) - }, + data: { proposalIds: proposals.map(p => p.proposalId) }, hooks: { prepublish: params.hooks?.prepublish, postpublish: null