diff options
author | DorraJaouad <dorra.jaoued7@gmail.com> | 2024-01-30 11:27:00 +0100 |
---|---|---|
committer | DorraJaouad <dorra.jaoued7@gmail.com> | 2024-01-31 17:40:30 +0100 |
commit | 0f775f6d5f86737a84e726a57e4c57e840d77c41 (patch) | |
tree | dbe6003944a8c1aab1c7a398d49fad9a3e2f37f6 | |
parent | f69f70ce29a60dfe39a7935b28e1b661bcdb9e9d (diff) |
Move parsing to utilities , add tests and change style
Signed-off-by: DorraJaouad <dorra.jaoued7@gmail.com>
-rw-r--r-- | src/components/ConversationSettings/EditableTextField.vue | 11 | ||||
-rw-r--r-- | src/components/MessagesList/MessagesGroup/Message/MessageButtonsBar/MessageButtonsBar.vue | 30 | ||||
-rw-r--r-- | src/components/NewMessage/NewMessage.vue | 10 | ||||
-rw-r--r-- | src/stores/__tests__/chatExtras.spec.js | 35 | ||||
-rw-r--r-- | src/stores/chatExtras.js | 26 | ||||
-rw-r--r-- | src/utils/textParse.js | 68 |
6 files changed, 135 insertions, 45 deletions
diff --git a/src/components/ConversationSettings/EditableTextField.vue b/src/components/ConversationSettings/EditableTextField.vue index b8515f739..76912b4fb 100644 --- a/src/components/ConversationSettings/EditableTextField.vue +++ b/src/components/ConversationSettings/EditableTextField.vue @@ -85,6 +85,8 @@ import NcRichContenteditable from '@nextcloud/vue/dist/Components/NcRichContente import NcRichText from '@nextcloud/vue/dist/Components/NcRichText.js' import Tooltip from '@nextcloud/vue/dist/Directives/Tooltip.js' +import { parseSpecialSymbols } from '../../utils/textParse.js' + export default { name: 'EditableTextField', components: { @@ -232,12 +234,9 @@ export default { if (!this.canSubmit) { return } - // Remove leading/trailing whitespaces. - // FIXME upstream: https://github.com/nextcloud-libraries/nextcloud-vue/issues/4492 - const temp = document.createElement('textarea') - temp.innerHTML = this.text.replace(/&/gmi, '&') - this.text = temp.value.replace(/\r\n|\n|\r/gm, '\n').replace(/&/gmi, '&') - .replace(/</gmi, '<').replace(/>/gmi, '>').replace(/§/gmi, '§').trim() + + // Parse special symbols + this.text = parseSpecialSymbols(this.text) // Submit text this.$emit('submit-text', this.text) diff --git a/src/components/MessagesList/MessagesGroup/Message/MessageButtonsBar/MessageButtonsBar.vue b/src/components/MessagesList/MessagesGroup/Message/MessageButtonsBar/MessageButtonsBar.vue index e71b65a0e..d0972cedf 100644 --- a/src/components/MessagesList/MessagesGroup/Message/MessageButtonsBar/MessageButtonsBar.vue +++ b/src/components/MessagesList/MessagesGroup/Message/MessageButtonsBar/MessageButtonsBar.vue @@ -67,7 +67,8 @@ </NcActionText> <!-- Edited message timestamp --> <NcActionText v-if="lastEditTimestamp" - :name="lastEditActorDisplayName"> + class="edit-timestamp" + :name="lastEditActorLabel"> <template #icon> <ClockEditOutline :size="16" /> </template> @@ -306,6 +307,7 @@ import { getMessageReminder, removeMessageReminder, setMessageReminder } from '. import { copyConversationLinkToClipboard } from '../../../../../services/urlService.js' import { useIntegrationsStore } from '../../../../../stores/integrations.js' import { useReactionsStore } from '../../../../../stores/reactions.js' +import { parseMentions } from '../../../../../utils/textParse.js' const EmojiIndex = new EmojiIndexFactory(data) const supportReminders = getCapabilities()?.spreed?.features?.includes('remind-me-later') @@ -527,8 +529,7 @@ export default { return false } - return ((moment(this.timestamp * 1000).add(6, 'h')) > moment() - || canDeleteMessageUnlimited) + return (canDeleteMessageUnlimited || (moment(this.timestamp * 1000).add(6, 'h')) > moment()) && (this.messageType === 'comment' || this.messageType === 'voice-message') && !this.isDeleting && (this.isMyMsg @@ -672,6 +673,12 @@ export default { apiVersion: 'v3', } }, + + lastEditActorLabel() { + return t('spreed', 'Edited by {actor}', { + actor: this.lastEditActorDisplayName, + }) + }, }, watch: { @@ -694,15 +701,7 @@ export default { }, async handleCopyMessageText() { - let parsedText = this.message - - for (const [key, value] of Object.entries(this.messageParameters)) { - if (value?.type === 'call') { - parsedText = parsedText.replace(new RegExp(`{${key}}`, 'g'), '@all') - } else if (value?.type === 'user') { - parsedText = parsedText.replace(new RegExp(`{${key}}`, 'g'), `@${value.id}`) - } - } + const parsedText = parseMentions(this.message, this.messageParameters) try { await navigator.clipboard.writeText(parsedText) @@ -851,6 +850,9 @@ export default { }, editMessage() { + if (!canEditMessage) { + return + } this.$emit('edit') }, }, @@ -867,4 +869,8 @@ export default { background: no-repeat center var(--icon-triangle-e-dark); } } + +.edit-timestamp :deep(.action-text__longtext-wrapper) { + padding: 0; +} </style> diff --git a/src/components/NewMessage/NewMessage.vue b/src/components/NewMessage/NewMessage.vue index 0ed1550ec..c0fcfdc89 100644 --- a/src/components/NewMessage/NewMessage.vue +++ b/src/components/NewMessage/NewMessage.vue @@ -226,6 +226,7 @@ import { useChatExtrasStore } from '../../stores/chatExtras.js' import { useSettingsStore } from '../../stores/settings.js' import { fetchClipboardContent } from '../../utils/clipboard.js' import { isDarkTheme } from '../../utils/isDarkTheme.js' +import { parseSpecialSymbols } from '../../utils/textParse.js' const disableKeyboardShortcuts = OCP.Accessibility.disableKeyboardShortcuts() const supportTypingStatus = getCapabilities()?.spreed?.config?.chat?.['typing-privacy'] !== undefined @@ -461,7 +462,8 @@ export default { }, showMentionEditHint() { - return this.chatEditInput?.includes('@') + const mentionPattern = /(^|\s)@/ + return mentionPattern.test(this.chatEditInput) }, }, @@ -614,12 +616,8 @@ export default { } } - // FIXME upstream: https://github.com/nextcloud-libraries/nextcloud-vue/issues/4492 if (this.hasText) { - const temp = document.createElement('textarea') - temp.innerHTML = this.text.replace(/&/gmi, '&') - this.text = temp.value.replace(/&/gmi, '&').replace(/</gmi, '<') - .replace(/>/gmi, '>').replace(/§/gmi, '§') + this.text = parseSpecialSymbols(this.text) } if (this.upload) { diff --git a/src/stores/__tests__/chatExtras.spec.js b/src/stores/__tests__/chatExtras.spec.js index ee8900222..c3ae0a162 100644 --- a/src/stores/__tests__/chatExtras.spec.js +++ b/src/stores/__tests__/chatExtras.spec.js @@ -161,4 +161,39 @@ describe('chatExtrasStore', () => { expect(chatExtrasStore.chatInput['token-1']).not.toBeDefined() }) }) + + describe('text parsing', () => { + it('should render mentions properly when editing message', () => { + // Arrange + const parameters = { + 'mention-call1': { type: 'call', name: 'Conversation101' }, + 'mention-user1': { type: 'user', name: 'Alice Joel', id: 'alice' }, + } + // Act + chatExtrasStore.setChatEditInput({ + token: 'token-1', + text: 'Hello {mention-call1} and {mention-user1}', + parameters + }) + // Assert + expect(chatExtrasStore.getChatEditInput('token-1')).toBe('Hello @all and @alice') + }) + + it('should store chat input without escaping special symbols', () => { + // Arrange + const message = 'These are special symbols & < > §' + // Act + chatExtrasStore.setChatInput({ token: 'token-1', text: message }) + // Assert + expect(chatExtrasStore.getChatInput('token-1')).toBe('These are special symbols & < > §') + }) + it('should remove leading/trailing whitespaces', () => { + // Arrange + const message = ' Many whitespaces ' + // Act + chatExtrasStore.setChatInput({ token: 'token-1', text: message }) + // Assert + expect(chatExtrasStore.getChatInput('token-1')).toBe('Many whitespaces') + }) + }) }) diff --git a/src/stores/chatExtras.js b/src/stores/chatExtras.js index c733fe05c..d060b4ec5 100644 --- a/src/stores/chatExtras.js +++ b/src/stores/chatExtras.js @@ -25,6 +25,7 @@ import { defineStore } from 'pinia' import Vue from 'vue' import { getUserAbsence } from '../services/participantsService.js' +import { parseSpecialSymbols, parseMentions } from '../utils/textParse.js' /** * @typedef {string} Token @@ -136,12 +137,7 @@ export const useChatExtrasStore = defineStore('chatExtras', { * @param {string} payload.text The string to store */ setChatInput({ token, text }) { - // FIXME upstream: https://github.com/nextcloud-libraries/nextcloud-vue/issues/4492 - const temp = document.createElement('textarea') - temp.innerHTML = text.replace(/&/gmi, '&') - const parsedText = temp.value.replace(/&/gmi, '&').replace(/</gmi, '<') - .replace(/>/gmi, '>').replace(/§/gmi, '§') - + const parsedText = parseSpecialSymbols(text) Vue.set(this.chatInput, token, parsedText) }, @@ -156,21 +152,9 @@ export const useChatExtrasStore = defineStore('chatExtras', { setChatEditInput({ token, text, parameters = {} }) { let parsedText = text - // Handle mentions - if (parameters.length !== 0) { - for (const [key, value] of Object.entries(parameters)) { - if (value?.type === 'call') { - parsedText = parsedText.replace(new RegExp(`{${key}}`, 'g'), '@all') - } else if (value?.type === 'user') { - parsedText = parsedText.replace(new RegExp(`{${key}}`, 'g'), `@${value.id}`) - } - } - } - // FIXME upstream: https://github.com/nextcloud-libraries/nextcloud-vue/issues/4492 - const temp = document.createElement('textarea') - temp.innerHTML = parsedText.replace(/&/gmi, '&') - parsedText = temp.value.replace(/&/gmi, '&').replace(/</gmi, '<') - .replace(/>/gmi, '>').replace(/§/gmi, '§') + // Handle mentions and special symbols + parsedText = parseMentions(parsedText, parameters) + parsedText = parseSpecialSymbols(parsedText) Vue.set(this.chatEditInput, token, parsedText) }, diff --git a/src/utils/textParse.js b/src/utils/textParse.js new file mode 100644 index 000000000..f13ef0215 --- /dev/null +++ b/src/utils/textParse.js @@ -0,0 +1,68 @@ +/** + * @copyright Copyright (c) 2024 Dorra Jaouad <dorra.jaoued1@gmail.com> + * + * @author Dorra Jaouad <dorra.jaoued1@gmail.com> + * @author Maksim Sukharev <antreesy.web@gmail.com> + * + * @license AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +/** + * Parse message text to return proper formatting for mentions + * + * @param {string} text The string to parse + * @param {object} parameters The parameters that contain the mentions + * @return {string} + */ +function parseMentions(text, parameters) { + if (Object.keys(parameters).some(key => key.startsWith('mention'))) { + for (const [key, value] of Object.entries(parameters)) { + let mention = '' + if (value?.type === 'call') { + mention = '@all' + } else if (value?.type === 'user') { + mention = value.id.includes(' ') ? `@"${value.id}"` : `@${value.id}` + } + if (mention) { + text = text.replace(new RegExp(`{${key}}`, 'g'), mention) + } + } + } + return text +} + +/** + * Parse special symbols in text like & < > § + * FIXME upstream: https://github.com/nextcloud-libraries/nextcloud-vue/issues/4492 + * + * @param {string} text The string to parse + * @return {string} + */ +function parseSpecialSymbols(text) { + const temp = document.createElement('textarea') + temp.innerHTML = text.replace(/&/gmi, '&') + text = temp.value.replace(/&/gmi, '&').replace(/</gmi, '<') + .replace(/>/gmi, '>').replace(/§/gmi, '§') + .replace(/^\s+|\s+$/g, '') // remove trailing and leading whitespaces + .replace(/\r\n|\n|\r/gm, '\n') // remove line breaks + return text +} + +export { + parseSpecialSymbols, + parseMentions, +} |