From 11b7d29cee09eddd1195829958d37a03aa14527a Mon Sep 17 00:00:00 2001 From: 1aerostorm Date: Thu, 17 Oct 2024 10:32:41 +0300 Subject: [PATCH] HF 30 - Private groups - Mentions, fix mark messages --- .../modules/groups/GroupMembers.jsx | 3 + src/components/modules/groups/MyGroups.jsx | 5 + src/components/pages/Messages.jsx | 93 +++++++++++++------ src/redux/FetchDataSaga.js | 3 + src/redux/GlobalReducer.js | 16 ++-- src/redux/TransactionSaga.js | 4 +- src/utils/MessageUtils.js | 26 ++++-- src/utils/Normalizators.js | 24 +++-- src/utils/groups.js | 16 ++-- src/utils/mentions.js | 18 ++++ 10 files changed, 147 insertions(+), 61 deletions(-) create mode 100644 src/utils/mentions.js diff --git a/src/components/modules/groups/GroupMembers.jsx b/src/components/modules/groups/GroupMembers.jsx index 633b8810..3bbde6f7 100644 --- a/src/components/modules/groups/GroupMembers.jsx +++ b/src/components/modules/groups/GroupMembers.jsx @@ -11,6 +11,7 @@ import AccountName from 'app/components/elements/common/AccountName' import Input from 'app/components/elements/common/Input'; import GroupMember from 'app/components/elements/groups/GroupMember' import LoadingIndicator from 'app/components/elements/LoadingIndicator' +import MarkNotificationRead from 'app/components/elements/MarkNotificationRead' import { getRoleInGroup, getGroupMeta, getGroupTitle } from 'app/utils/groups' export async function validateMembersStep(values, errors) { @@ -253,6 +254,8 @@ class GroupMembers extends React.Component { {this._renderMemberTypeSwitch()} } + {(username && showPendings) ? : null} } diff --git a/src/components/modules/groups/MyGroups.jsx b/src/components/modules/groups/MyGroups.jsx index a9a588e6..f818bf34 100644 --- a/src/components/modules/groups/MyGroups.jsx +++ b/src/components/modules/groups/MyGroups.jsx @@ -12,6 +12,7 @@ import user from 'app/redux/UserReducer' import DropdownMenu from 'app/components/elements/DropdownMenu' import Icon from 'app/components/elements/Icon' import LoadingIndicator from 'app/components/elements/LoadingIndicator' +import MarkNotificationRead from 'app/components/elements/MarkNotificationRead' import { showLoginDialog } from 'app/components/dialogs/LoginDialog' import { getGroupLogo, getGroupMeta, getRoleInGroup } from 'app/utils/groups' @@ -244,6 +245,8 @@ class MyGroups extends React.Component { } + const { username } = this.props + return

{tt('my_groups_jsx.title')}

@@ -251,6 +254,8 @@ class MyGroups extends React.Component { {button} {groups} {hasGroups ?
: null} + {username ? : null}
} } diff --git a/src/components/pages/Messages.jsx b/src/components/pages/Messages.jsx index c503fbae..31bd07ee 100644 --- a/src/components/pages/Messages.jsx +++ b/src/components/pages/Messages.jsx @@ -29,6 +29,7 @@ import g from 'app/redux/GlobalReducer' import transaction from 'app/redux/TransactionReducer' import user from 'app/redux/UserReducer' import { getRoleInGroup, opGroup } from 'app/utils/groups' +import { parseMentions } from 'app/utils/mentions' import { getProfileImage, } from 'app/utils/NormalizeProfile'; import { normalizeContacts, normalizeMessages, cacheMyOwnMsg } from 'app/utils/Normalizators'; import { fitToPreview } from 'app/utils/ImageUtils'; @@ -53,7 +54,7 @@ class Messages extends React.Component { }; this.cachedProfileImages = {}; this.windowFocused = true; - this.newMessages = 0; + this.newMessages = {} if (process.env.MOBILE_APP) { this.stopService() } @@ -71,21 +72,43 @@ class Messages extends React.Component { return the_group ? the_group.name : '' } - markMessages() { - const { messages } = this.state; - if (!messages.length) return; + markMessages = () => { + const { messages } = this.props + if (!messages || !messages.size) return + + const msgs = messages.toJS() const { account, accounts, } = this.props; const to = this.getToAcc() - let OPERATIONS = golos.messages.makeDatedGroups(messages, (message_object, idx) => { - return message_object.toMark && !message_object._offchain; + const isGroup = this.isGroup() + + let OPERATIONS = golos.messages.makeDatedGroups(msgs, (msg, idx) => { + if (msg._offchain) return false + if (msg.read_date.startsWith('19')) { + if (!isGroup) { + return msg.to === account.name + } else { + if (msg.to === account.name) return true + } + } + if (isGroup && msg.mentions.includes(account.name)) { + return true + } + return false }, (group, indexes, results) => { - const json = JSON.stringify(['private_mark_message', { - from: accounts[to].name, - to: account.name, + const op = { + from: isGroup ? '' : accounts[to].name, + to: isGroup ? '' : account.name, ...group, - }]); + } + if (isGroup) { + op.extensions = [[0, { + group: to, + requester: account.name, + }]] + } + const json = JSON.stringify(['private_mark_message', op]) return ['custom_json', { id: 'private_message', @@ -93,25 +116,26 @@ class Messages extends React.Component { json, } ]; - }, messages.length - 1, -1); + }, 0, msgs.length); this.props.sendOperations(account, accounts[to], OPERATIONS); } - markMessages2 = debounce(this.markMessages, 1000); + markMessages2 = debounce(this.markMessages, 1000) - flashMessage() { - ++this.newMessages; + flashMessage(nonce) { + this.newMessages[nonce] = true - let title = this.newMessages; - const plural = this.newMessages % 10; + const count = Object.keys(this.newMessages).length + let title = count + const plural = count % 10 if (plural === 1) { - if (this.newMessages === 11) + if (count === 11) title += tt('messages.new_message5'); else title += tt('messages.new_message1'); - } else if ((plural === 2 || plural === 3 || plural === 4) && (this.newMessages < 10 || this.newMessages > 20)) { + } else if ((plural === 2 || plural === 3 || plural === 4) && (count < 10 || count > 20)) { title += tt('messages.new_message234'); } else { title += tt('messages.new_message5'); @@ -220,18 +244,20 @@ class Messages extends React.Component { //alert(scope + ' ' + type + op +' ' + timestamp) const isDonate = type === 'donate' const toAcc = this.getToAcc() - const group = opGroup(op) + const { group } = opGroup(op) let updateMessage = group === this.state.to || (!group && (op.from === toAcc || op.to === toAcc)) const isMine = username === op.from; if (type === 'private_message') { if (op.update) { this.props.messageEdited(op, timestamp, updateMessage, isMine); - } else if (this.nonce !== op.nonce) { + } else { this.props.messaged(op, timestamp, updateMessage, isMine); - this.nonce = op.nonce - if (!isMine && !this.windowFocused) { - this.flashMessage(); + if (this.nonce !== op.nonce) { + this.nonce = op.nonce + if (!isMine && !this.windowFocused) { + this.flashMessage(op.nonce) + } } } } else if (type === 'private_delete_message') { @@ -357,6 +383,7 @@ class Messages extends React.Component { } updateData() } + this.markMessages2() } componentWillUnmount() { @@ -808,7 +835,7 @@ class Messages extends React.Component {
- +
@@ -864,7 +891,7 @@ class Messages extends React.Component { } let user_menu = [ - {link: '#', onClick: openMyGroups, icon: 'voters', value: tt('g.groups') + (isSmall ? (' @' + username) : '') }, + {link: '#', onClick: openMyGroups, icon: 'voters', value: tt('g.groups') + (isSmall ? (' @' + username) : ''), addon: }, {link: accountLink, extLink: 'blogs', icon: 'new/blogging', value: tt('g.blog'), addon: }, {link: mentionsLink, extLink: 'blogs', icon: 'new/mention', value: tt('g.mentions'), addon: }, {link: donatesLink, extLink: 'wallet', icon: 'editor/coin', value: tt('g.rewards'), addon: }, @@ -906,7 +933,7 @@ class Messages extends React.Component {
- +
{!isSmall ?
@@ -953,11 +980,11 @@ class Messages extends React.Component { handleFocusChange = isFocused => { this.windowFocused = isFocused; if (!isFocused) { - if (this.newMessages) { + if (Object.keys(this.newMessages).length) { flash(); } } else { - this.newMessages = 0; + this.newMessages = {} unflash(); } } @@ -1191,6 +1218,11 @@ export default withRouter(connect( message = {...message, ...replyingMessage}; } + let mentions = [] + if (group) { + mentions = parseMentions(message) + } + let data = null try { data = await golos.messages.encodeMsg({ group, @@ -1214,7 +1246,6 @@ export default withRouter(connect( update: editInfo ? true : false, encrypted_message: data.encrypted_message, } - //alert(JSON.stringify(opData)) if (group) { let requester @@ -1226,9 +1257,11 @@ export default withRouter(connect( opData.extensions = [[0, { group: group.name, - requester + requester, + mentions }]] } + //alert(JSON.stringify(opData)) cacheMyOwnMsg(opData, group, message) diff --git a/src/redux/FetchDataSaga.js b/src/redux/FetchDataSaga.js index a5a556f5..d152040a 100644 --- a/src/redux/FetchDataSaga.js +++ b/src/redux/FetchDataSaga.js @@ -243,6 +243,9 @@ export function* fetchMyGroups({ payload: { account } }) { } }) groups = [...groupsOwn, ...groups] + groups.sort((a, b) => { + return b.pendings - a.pendings + }) yield put(g.actions.receiveMyGroups({ groups })) } catch (err) { diff --git a/src/redux/GlobalReducer.js b/src/redux/GlobalReducer.js index a17099b0..57f975f3 100644 --- a/src/redux/GlobalReducer.js +++ b/src/redux/GlobalReducer.js @@ -93,8 +93,9 @@ export default createModule({ message.donates = '0.000 GOLOS' message.donates_uia = 0 } - const group = opGroup(message) + const { group, mentions } = opGroup(message) message.group = group + message.mentions = mentions let new_state = state; let messages_update = message.nonce; @@ -199,15 +200,14 @@ export default createModule({ ) => { let new_state = state; let messages_update = message.nonce || Math.random(); + const { requester } = opGroup(message) if (updateMessage) { new_state = new_state.updateIn(['messages'], List(), messages => { - return processDatedGroup(message, messages, (messages, idx) => { - let msg = messages.get(idx) + return processDatedGroup(message, messages, (msg, idx) => { msg = msg.set('read_date', timestamp) - const msgs = messages.set(idx, msg) - return { msgs } + return { updated: msg } }); }); } @@ -260,10 +260,8 @@ export default createModule({ new_state = new_state.updateIn(['messages'], List(), messages => { - return processDatedGroup(message, messages, (messages, idx) => { - let msg = messages.get(idx) - const msgs = messages.delete(idx) - return { msgs, fixIdx: idx - 1 } + return processDatedGroup(message, messages, (msg, idx) => { + return { updated: null, fixIdx: idx - 1 } }); }) } diff --git a/src/redux/TransactionSaga.js b/src/redux/TransactionSaga.js index a97727c8..7a8410b4 100644 --- a/src/redux/TransactionSaga.js +++ b/src/redux/TransactionSaga.js @@ -46,14 +46,16 @@ function* preBroadcast_custom_json({operation}) { const idx = msgs.findIndex(i => i.get('nonce') === json[1].nonce); if (idx === -1) { let group = '' + let mentions = [] const exts = json[1].extensions || [] for (const [key, val ] of exts) { if (key === 0) { group = val.group + mentions = val.mentions break } } - const newMsg = messageOpToObject(json[1], group) + const newMsg = messageOpToObject(json[1], group, mentions) msgs = msgs.insert(0, fromJS(newMsg)) } else { messages_update = json[1].nonce; diff --git a/src/utils/MessageUtils.js b/src/utils/MessageUtils.js index 8089fac0..f69da6d1 100644 --- a/src/utils/MessageUtils.js +++ b/src/utils/MessageUtils.js @@ -6,12 +6,20 @@ export function displayQuoteMsg(body) { } export function processDatedGroup(group, messages, for_each) { + let deleteIt if (group.nonce) { const idx = messages.findIndex(i => i.get('nonce') === group.nonce); if (idx !== -1) { messages = messages.update(idx, (msg) => { - return for_each(msg, idx); - }); + const { updated, fixIdx } = for_each(msg, idx) + if (!updated) { + deleteIt = idx + } + return updated || msg + }) + if (deleteIt !== undefined) { + messages = messages.delete(idx) + } } } else { let inRange = false; @@ -26,15 +34,19 @@ export function processDatedGroup(group, messages, for_each) { break; } if (inRange) { - const updated = for_each(messages, idx) - if (updated) { - const { msgs, fixIdx } = updated - if (msgs) { - messages = msgs + deleteIt = undefined + messages = messages.update(idx, (msg) => { + const { updated, fixIdx } = for_each(msg, idx) + if (!updated) { + deleteIt = idx } if (fixIdx !== undefined) { idx = fixIdx } + return updated || msg + }) + if (deleteIt !== undefined) { + messages = messages.delete(idx) } } } diff --git a/src/utils/Normalizators.js b/src/utils/Normalizators.js index 40a92385..3fdad45a 100644 --- a/src/utils/Normalizators.js +++ b/src/utils/Normalizators.js @@ -90,7 +90,7 @@ const loadFromCache = (msg, contact = false) => { return false } -export function messageOpToObject(op, group) { +export function messageOpToObject(op, group, mentions = []) { const obj = { nonce: op.nonce, checksum: op.checksum, @@ -103,7 +103,8 @@ export function messageOpToObject(op, group) { receive_date: zeroDate, encrypted_message: op.encrypted_message, donates: '0.000 GOLOS', - donates_uia: 0 + donates_uia: 0, + mentions, } return obj } @@ -219,14 +220,21 @@ export async function normalizeMessages(messages, accounts, currentUser, to) { msg.author = msg.from; msg.date = new Date(msg.create_date + 'Z'); - if (!isGroup) { - if (msg.to === currentAcc.name) { - if (msg.read_date.startsWith('19')) { - msg.toMark = true; + if (msg.read_date.startsWith('19')) { + if (!isGroup) { + if (msg.to === currentAcc.name) { + msg.toMark = true + } else { + msg.unread = true } } else { - if (msg.read_date.startsWith('19')) { - msg.unread = true; + if (msg.to === currentAcc.name) { + msg.toMark = true + } else if (msg.to) { + msg.unread = true + } + if (!msg.toMark && msg.mentions.includes(currentAcc.name)) { + msg.toMark = true } } } diff --git a/src/utils/groups.js b/src/utils/groups.js index fd2878ae..b99994d9 100644 --- a/src/utils/groups.js +++ b/src/utils/groups.js @@ -55,23 +55,27 @@ const getRoleInGroup = (group, username) => { const opGroup = (op) => { let group = '' - if (!op) return group + let requester = '' + let mentions = [] + if (!op) return { group, requester, mentions } const { extensions, memo } = op if (extensions) { for (const ext of extensions) { - if (ext && ext[0] === 0) { - group = (ext[1] && ext[1].group) || group + if (ext && ext[0] === 0 && ext[1]) { + group = ext[1].group || group + mentions = ext[1].mentions || mentions + requester = ext[1].requester || requester } } } - if (group) return group + if (group) return { group, requester, mentions } if (memo) { // donate const { target } = memo if (target && target.group) { - return target.group + group = target.group } } - return group + return { group, requester, mentions } } export { diff --git a/src/utils/mentions.js b/src/utils/mentions.js new file mode 100644 index 00000000..916cd594 --- /dev/null +++ b/src/utils/mentions.js @@ -0,0 +1,18 @@ + +const accountNameRegEx = /^@[a-z0-9.-]+$/ + +// TODO: can be renderMsg which also supports links, and rendering +export function parseMentions(message) { + let mentions = new Set() + const { body } = message + const lines = body.split('\n') + for (const line of lines) { + const words = line.split(' ') + for (let word of words) { + if (word.length > 3 && accountNameRegEx.test(word)) { + mentions.add(word.slice(1)) + } + } + } + return [...mentions] +}