Browse Source

Merge pull request #11374 from nextcloud/feat/11205/edit-message-frontend

Feat(messages): Edit messages
pull/11399/head
Dorra 2 years ago
committed by GitHub
parent
commit
ae3de4c8eb
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 15
      src/components/ChatView.vue
  2. 36
      src/components/MessagesList/MessagesGroup/Message/Message.vue
  3. 66
      src/components/MessagesList/MessagesGroup/Message/MessageButtonsBar/MessageButtonsBar.vue
  4. 36
      src/components/MessagesList/MessagesGroup/MessagesGroup.vue
  5. 11
      src/components/MessagesList/MessagesList.vue
  6. 115
      src/components/NewMessage/NewMessage.vue
  7. 30
      src/components/Quote.vue
  8. 7
      src/services/messagesService.js
  9. 8
      src/store/messagesStore.js
  10. 25
      src/stores/__tests__/chatExtras.spec.js
  11. 47
      src/stores/chatExtras.js

15
src/components/ChatView.vue

@ -86,6 +86,7 @@ import TransitionWrapper from './TransitionWrapper.vue'
import { CONVERSATION } from '../constants.js'
import { EventBus } from '../services/EventBus.js'
import { useChatExtrasStore } from '../stores/chatExtras.js'
export default {
@ -108,6 +109,12 @@ export default {
},
},
setup() {
return {
chatExtrasStore: useChatExtrasStore(),
}
},
data() {
return {
isChatScrolledToBottom: true,
@ -126,6 +133,10 @@ export default {
return !userName && this.isGuest
},
isEditingMessage() {
return this.chatExtrasStore.getMessageIdToEdit(this.token) !== undefined
},
dropHintText() {
if (this.isGuest) {
return t('spreed', 'You need to be logged in to upload files')
@ -155,7 +166,7 @@ export default {
watch: {
container(value) {
this.containerId = value
}
},
},
mounted() {
@ -166,7 +177,7 @@ export default {
methods: {
handleDragOver(event) {
if (event.dataTransfer.types.includes('Files')) {
if (event.dataTransfer.types.includes('Files') && !this.isEditingMessage) {
this.isDraggingOver = true
}
},

36
src/components/MessagesList/MessagesGroup/Message/Message.vue

@ -178,6 +178,7 @@ the main body of the message as well as a quote.
:sent-icon-tooltip="sentIconTooltip"
@show-translate-dialog="isTranslateDialogOpen = true"
@reply="handleReply"
@edit="handleEdit"
@delete="handleDelete" />
<div v-else-if="showCombinedSystemMessageToggle"
class="message-buttons-bar">
@ -407,6 +408,26 @@ export default {
type: String,
default: '',
},
lastEditActorDisplayName: {
type: String,
default: '',
},
lastEditActorId: {
type: String,
default: '',
},
lastEditActorType: {
type: String,
default: '',
},
lastEditTimestamp: {
type: Number,
default: 0,
},
},
emits: ['toggle-combined-system-message'],
@ -660,6 +681,10 @@ export default {
containsCodeBlocks() {
return this.message.includes('```')
},
isFileShareOnly() {
return Object.keys(Object(this.messageParameters)).some(key => key.startsWith('file')) && this.message === '{file}'
},
},
watch: {
@ -756,6 +781,17 @@ export default {
EventBus.$emit('focus-chat-input')
},
handleEdit() {
this.chatExtrasStore.setMessageIdToEdit(this.token, this.id)
if (this.isFileShareOnly) {
this.chatExtrasStore.setChatEditInput({ token: this.token, text: '' })
} else {
this.chatExtrasStore.setChatEditInput({ token: this.token, text: this.message })
}
EventBus.$emit('editing-message')
EventBus.$emit('focus-chat-input')
},
async handleDelete() {
this.isDeleting = true
try {

66
src/components/MessagesList/MessagesGroup/Message/MessageButtonsBar/MessageButtonsBar.vue

@ -48,7 +48,8 @@
@open="onMenuOpen"
@close="onMenuClose">
<template v-if="submenu === null">
<NcActionButton>
<!-- Message timestamp -->
<NcActionText>
<template #icon>
<span v-if="showCommonReadIcon"
:title="commonReadIconTooltip"
@ -63,7 +64,20 @@
<ClockOutline v-else :size="16" />
</template>
{{ messageDateTime }}
</NcActionButton>
</NcActionText>
<!-- Edited message timestamp -->
<NcActionButtonGroup v-if="messageObject.lastEditTimestamp">
<NcActionText>
<template #icon>
<ClockEditOutline :size="16" />
</template>
{{ messageObject.lastEditActorDisplayName }}
</NcActionText>
<NcActionText>
{{ editedDateTime }}
</NcActionText>
</NcActionButtonGroup>
<NcActionSeparator />
<NcActionButton v-if="supportReminders"
class="action--nested"
@ -73,8 +87,6 @@
</template>
{{ t('spreed', 'Set reminder') }}
</NcActionButton>
<NcActionSeparator />
<NcActionButton v-if="isPrivateReplyable"
close-after-click
@click.stop="handlePrivateReply">
@ -83,6 +95,15 @@
</template>
{{ t('spreed', 'Reply privately') }}
</NcActionButton>
<NcActionButton v-if="isEditable"
:aria-label="t('spreed', 'Edit message')"
close-after-click
@click.stop="editMessage">
<template #icon>
<Pencil :size="20" />
</template>
{{ t('spreed', 'Edit message') }}
</NcActionButton>
<NcActionButton v-if="!isFileShareOnly"
close-after-click
@click.stop="handleCopyMessageText">
@ -258,6 +279,7 @@ import ArrowLeft from 'vue-material-design-icons/ArrowLeft.vue'
import CalendarClock from 'vue-material-design-icons/CalendarClock.vue'
import Check from 'vue-material-design-icons/Check.vue'
import CheckAll from 'vue-material-design-icons/CheckAll.vue'
import ClockEditOutline from 'vue-material-design-icons/ClockEditOutline.vue'
import ClockOutline from 'vue-material-design-icons/ClockOutline.vue'
import CloseCircleOutline from 'vue-material-design-icons/CloseCircleOutline.vue'
import ContentCopy from 'vue-material-design-icons/ContentCopy.vue'
@ -267,6 +289,7 @@ import EyeOffOutline from 'vue-material-design-icons/EyeOffOutline.vue'
import File from 'vue-material-design-icons/File.vue'
import Note from 'vue-material-design-icons/NoteEditOutline.vue'
import OpenInNewIcon from 'vue-material-design-icons/OpenInNew.vue'
import Pencil from 'vue-material-design-icons/Pencil.vue'
import Plus from 'vue-material-design-icons/Plus.vue'
import Reply from 'vue-material-design-icons/Reply.vue'
import Share from 'vue-material-design-icons/Share.vue'
@ -277,10 +300,12 @@ import { showError, showSuccess } from '@nextcloud/dialogs'
import moment from '@nextcloud/moment'
import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton.js'
import NcActionButtonGroup from '@nextcloud/vue/dist/Components/NcActionButtonGroup.js'
import NcActionInput from '@nextcloud/vue/dist/Components/NcActionInput.js'
import NcActionLink from '@nextcloud/vue/dist/Components/NcActionLink.js'
import NcActions from '@nextcloud/vue/dist/Components/NcActions.js'
import NcActionSeparator from '@nextcloud/vue/dist/Components/NcActionSeparator.js'
import NcActionText from '@nextcloud/vue/dist/Components/NcActionText.js'
import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
import NcEmojiPicker from '@nextcloud/vue/dist/Components/NcEmojiPicker.js'
@ -300,10 +325,12 @@ export default {
components: {
MessageForwarder,
NcActionButtonGroup,
NcActionButton,
NcActionInput,
NcActionLink,
NcActionSeparator,
NcActionText,
NcActions,
NcButton,
NcEmojiPicker,
@ -315,6 +342,7 @@ export default {
CloseCircleOutline,
Check,
CheckAll,
ClockEditOutline,
ClockOutline,
ContentCopy,
DeleteIcon,
@ -323,6 +351,7 @@ export default {
File,
Note,
OpenInNewIcon,
Pencil,
Plus,
Reply,
Share,
@ -447,7 +476,7 @@ export default {
},
},
emits: ['delete', 'update:isActionMenuOpen', 'update:isEmojiPickerOpen', 'update:isReactionsMenuOpen', 'update:isForwarderOpen', 'show-translate-dialog', 'reply'],
emits: ['delete', 'update:isActionMenuOpen', 'update:isEmojiPickerOpen', 'update:isReactionsMenuOpen', 'update:isForwarderOpen', 'show-translate-dialog', 'reply', 'edit'],
setup() {
const reactionsStore = useReactionsStore()
@ -482,8 +511,21 @@ export default {
return this.getMessagesListScroller()
},
isModifiable() {
return !this.isConversationReadOnly && this.conversation.participantType !== PARTICIPANT.TYPE.GUEST
},
isEditable() {
if (!this.isModifiable || this.isObjectShare
|| (!this.$store.getters.isModerator && !this.isMyMsg)) {
return false
}
return (moment(this.timestamp * 1000).add(1, 'd')) > moment()
},
isDeleteable() {
if (this.isConversationReadOnly || this.conversation.participantType === PARTICIPANT.TYPE.GUEST) {
if (!this.isModifiable) {
return false
}
@ -523,6 +565,10 @@ export default {
return this.isFileShare && this.message === '{file}'
},
isObjectShare() {
return Object.keys(Object(this.messageParameters)).some(key => key.startsWith('object'))
},
isCurrentGuest() {
return this.$store.getters.getActorType() === 'guests'
},
@ -560,6 +606,10 @@ export default {
return moment(this.timestamp * 1000).format('lll')
},
editedDateTime() {
return moment(this.messageObject.lastEditTimestamp * 1000).format('lll')
},
reminderOptions() {
const currentDateTime = moment()
@ -791,6 +841,10 @@ export default {
setCustomReminder() {
this.setReminder(this.customReminderDateTime.valueOf())
},
editMessage() {
this.$emit('edit')
},
},
}
</script>

36
src/components/MessagesList/MessagesGroup/MessagesGroup.vue

@ -32,6 +32,9 @@
<ul class="messages">
<li class="messages__author" aria-level="4">
{{ actorDisplayName }}
<div v-if="lastEditActorDisplayName">
{{ getLastEditor }}
</div>
</li>
<Message v-for="(message, index) of messages"
:key="message.id"
@ -88,6 +91,24 @@ export default {
type: [String, Number],
default: 0,
},
lastEditTimestamp: {
type: Number,
default: 0,
},
lastEditActorId: {
type: String,
default: '',
},
lastEditActorType: {
type: String,
default: '',
},
lastEditActorDisplayName: {
type: String,
default: ''
},
},
setup() {
@ -133,6 +154,19 @@ export default {
return displayName
},
getLastEditor() {
if (this.lastEditActorId === this.actorId && this.lastEditActorType === this.actorType) {
// TRANSLATORS Edited by the author of the message themselves
return t('spreed', '(edited)')
} else if (this.lastEditActorId === this.$store.getters.getActorId()
&& this.lastEditActorType === this.$store.getters.getActorType()) {
return t('spreed', '(edited by you)')
} else {
return t('spreed', '(edited by {moderator})', { moderator: this.lastEditActorDisplayName })
}
},
disableMenu() {
// disable the menu if accessing the conversation as guest
// or the message sender is a bridged user
@ -184,6 +218,8 @@ export default {
}
&__author {
display: flex;
gap: 4px;
padding: 4px 0 0 8px;
color: var(--color-text-maxcontrast);
}

11
src/components/MessagesList/MessagesList.vue

@ -284,6 +284,7 @@ export default {
EventBus.$on('scroll-chat-to-bottom-if-sticky', this.scrollToBottomIfSticky)
EventBus.$on('focus-message', this.focusMessage)
EventBus.$on('route-change', this.onRouteChange)
EventBus.$on('message-edited', this.handleMessageEdited)
subscribe('networkOffline', this.handleNetworkOffline)
subscribe('networkOnline', this.handleNetworkOnline)
window.addEventListener('focus', this.onWindowFocus)
@ -303,6 +304,7 @@ export default {
EventBus.$on('scroll-chat-to-bottom-if-sticky', this.scrollToBottomIfSticky)
EventBus.$off('focus-message', this.focusMessage)
EventBus.$off('route-change', this.onRouteChange)
EventBus.$off('message-edited', this.handleMessageEdited)
this.$store.dispatch('cancelLookForNewMessages', { requestId: this.chatIdentifier })
this.destroying = true
@ -356,6 +358,7 @@ export default {
areGroupsIdentical(group1, group2) {
if (group1.messages.length !== group2.messages.length
|| JSON.stringify(group1.messages) !== JSON.stringify(group2.messages)
|| group1.dateSeparator !== group2.dateSeparator
|| group1.previousMessageId !== group2.previousMessageId
|| group1.nextMessageId !== group2.nextMessageId) {
@ -396,6 +399,10 @@ export default {
return false // No previous message
}
if (!!message1.lastEditTimestamp || !!message2.lastEditTimestamp) {
return false // Edited messages are not grouped
}
if (message1.actorType === ATTENDEE.ACTOR_TYPE.BOTS // Don't group messages of commands and bots
&& message1.actorId !== ATTENDEE.CHANGELOG_BOT_ID) { // Apart from the changelog bot
return false
@ -627,6 +634,10 @@ export default {
}
},
handleMessageEdited(id) {
this.messagesGroupedByAuthor = this.prepareMessagesGroups(this.messagesList)
},
/**
* Fetches the messages of a conversation given the conversation token. Triggers
* a long-polling request for new messages.

115
src/components/NewMessage/NewMessage.vue

@ -82,8 +82,10 @@
</template>
</NcButton>
</div>
<div v-if="parentMessage" class="new-message-form__quote">
<Quote is-new-message-quote v-bind="parentMessage" />
<div v-if="parentMessage || messageToEdit" class="new-message-form__quote">
<Quote v-bind="messageToEdit ?? parentMessage"
:can-cancel="!!parentMessage"
:edit-message="!!messageToEdit" />
</div>
<NcRichContenteditable ref="richContenteditable"
v-shortkey.once="$options.disableKeyboardShortcuts ? null : ['c']"
@ -110,6 +112,29 @@
@recording="handleRecording"
@audio-file="handleAudioFile" />
<!-- Edit -->
<template v-else-if="messageToEdit">
<NcButton type="tertiary"
native-type="submit"
:title="t('spreed', 'Cancel editing')"
:aria-label="t('spreed', 'Cancel editing')"
@click="handleAbortEdit">
<template #icon>
<CloseIcon :size="20" />
</template>
</NcButton>
<NcButton :disabled="disabledEdit"
type="tertiary"
native-type="submit"
:title="t('spreed', 'Edit message')"
:aria-label="t('spreed', 'Edit message')"
@click="handleEdit">
<template #icon>
<CheckIcon :size="20" />
</template>
</NcButton>
</template>
<!-- Send buttons -->
<template v-else>
<NcActions v-if="!broadcast"
@ -163,6 +188,8 @@
<script>
import BellOff from 'vue-material-design-icons/BellOff.vue'
import CheckIcon from 'vue-material-design-icons/Check.vue'
import CloseIcon from 'vue-material-design-icons/Close.vue'
import EmoticonOutline from 'vue-material-design-icons/EmoticonOutline.vue'
import Send from 'vue-material-design-icons/Send.vue'
@ -188,6 +215,7 @@ import { CONVERSATION, PARTICIPANT, PRIVACY } from '../../constants.js'
import { EventBus } from '../../services/EventBus.js'
import { shareFile } from '../../services/filesSharingServices.js'
import { searchPossibleMentions } from '../../services/mentionsService.js'
import { editMessage } from '../../services/messagesService.js'
import { useChatExtrasStore } from '../../stores/chatExtras.js'
import { useSettingsStore } from '../../stores/settings.js'
import { fetchClipboardContent } from '../../utils/clipboard.js'
@ -217,6 +245,8 @@ export default {
Quote,
// Icons
BellOff,
CheckIcon,
CloseIcon,
EmoticonOutline,
Send,
},
@ -320,6 +350,10 @@ export default {
return this.isReadOnly || this.noChatPermission || !this.currentConversationIsJoined || this.isRecordingAudio
},
disabledEdit() {
return this.disabled || this.text === this.messageToEdit.message || this.text === ''
},
placeholderText() {
if (this.isReadOnly) {
return t('spreed', 'This conversation has been locked')
@ -338,6 +372,11 @@ export default {
return parentId && this.$store.getters.message(this.token, parentId)
},
messageToEdit() {
const messageToEditId = this.chatExtrasStore.getMessageIdToEdit(this.token)
return messageToEditId && this.$store.getters.message(this.token, messageToEditId)
},
currentUserIsGuest() {
return this.$store.getters.getUserId() === null
},
@ -382,11 +421,11 @@ export default {
},
showAttachmentsMenu() {
return this.canShareFiles && !this.broadcast && !this.upload
return this.canShareFiles && !this.broadcast && !this.upload && !this.messageToEdit
},
showAudioRecorder() {
return !this.hasText && this.canUploadFiles && !this.broadcast && !this.upload
return !this.hasText && this.canUploadFiles && !this.broadcast && !this.upload && !this.messageToEdit
},
showTypingStatus() {
@ -408,7 +447,11 @@ export default {
isMobileDevice() {
return /Android|iPhone|iPad|iPod/i.test(navigator.userAgent)
}
},
chatEditInput() {
return this.chatExtrasStore.getChatEditInput(this.token)
},
},
watch: {
@ -417,14 +460,41 @@ export default {
},
text(newValue) {
this.chatExtrasStore.setChatInput({ token: this.token, text: newValue })
if (this.messageToEdit) {
this.chatExtrasStore.setChatEditInput({ token: this.token, text: newValue })
} else {
this.chatExtrasStore.setChatInput({ token: this.token, text: newValue })
}
},
messageToEdit(newValue) {
if (newValue) {
this.text = this.chatExtrasStore.getChatEditInput(this.token)
this.chatExtrasStore.removeParentIdToReply(this.token)
} else {
this.text = this.chatExtrasStore.getChatInput(this.token)
}
},
parentMessage(newValue) {
if (newValue && this.messageToEdit) {
this.chatExtrasStore.removeMessageIdToEdit(this.token)
}
},
chatInput(newValue) {
if (this.text !== newValue) {
this.text = newValue
}
},
token: {
immediate: true,
handler(token) {
if (token) {
this.text = this.chatExtrasStore.getChatInput(token)
this.text = this.messageToEdit
? this.chatExtrasStore.getChatEditInput(token)
: this.chatExtrasStore.getChatInput(token)
} else {
this.text = ''
}
@ -510,6 +580,14 @@ export default {
* @param {object} options the submit options
*/
async handleSubmit(options) {
// Submit event has enter key listener
// Handle edit here too
if (this.messageToEdit) {
if (!this.disabledEdit) {
this.handleEdit()
}
return
}
if (OC.debug && this.text.startsWith('/spam ')) {
const pattern = /^\/spam (\d+) messages$/i
const match = pattern.exec(this.text)
@ -595,6 +673,20 @@ export default {
}
},
async handleEdit() {
try {
const response = await editMessage({
token: this.token,
messageId: this.messageToEdit.id,
updatedMessage: this.text.trim()
})
this.$store.dispatch('processMessage', { token: this.token, message: response.data.ocs.data })
this.chatExtrasStore.removeMessageIdToEdit(this.token)
} catch {
this.$emit('failure')
}
},
sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms))
},
@ -657,6 +749,9 @@ export default {
* @param {ClipboardEvent} e native paste event
*/
async handlePastedFiles(e) {
if (this.messageToEdit) {
return
}
e.preventDefault()
// Prevent a new call of this.handleFiles if already called
if (this.clipboardTimeStamp === e.timeStamp) {
@ -836,7 +931,11 @@ export default {
// Remove stored absence status
this.chatExtrasStore.removeUserAbsence(this.token)
}
}
},
handleAbortEdit() {
this.chatExtrasStore.removeMessageIdToEdit(this.token)
},
},
}
</script>

30
src/components/Quote.vue

@ -40,6 +40,10 @@ components.
:size="AVATAR.SIZE.EXTRA_SMALL"
disable-menu />
{{ getDisplayName }}
<div v-if="editMessage" class="quote__main__edit-hint">
<PencilIcon :size="20" />
{{ t('spreed', '(editing)') }}
</div>
</div>
<!-- file preview-->
<NcRichText v-if="isFileShareMessage"
@ -51,10 +55,10 @@ components.
<p dir="auto">{{ shortenedQuoteMessage }}</p>
</blockquote>
</div>
<div v-if="isNewMessageQuote" class="quote__main__right">
<div v-if="canCancel" class="quote__main__right">
<NcButton type="tertiary"
:aria-label="cancelQuoteLabel"
@click="handleAbortReply">
@click="handleAbort">
<template #icon>
<Close :size="20" />
</template>
@ -65,6 +69,7 @@ components.
<script>
import Close from 'vue-material-design-icons/Close.vue'
import PencilIcon from 'vue-material-design-icons/Pencil.vue'
import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
import NcRichText from '@nextcloud/vue/dist/Components/NcRichText.js'
@ -84,6 +89,7 @@ export default {
NcButton,
Close,
NcRichText,
PencilIcon,
},
props: {
actorId: {
@ -136,7 +142,12 @@ export default {
* If the quote component is used in the `NewMessage` component we display
* the remove button.
*/
isNewMessageQuote: {
canCancel: {
type: Boolean,
default: false,
},
editMessage: {
type: Boolean,
default: false,
},
@ -249,8 +260,12 @@ export default {
},
},
methods: {
handleAbortReply() {
this.chatExtrasStore.removeParentIdToReply(this.token)
handleAbort() {
if (this.editMessage) {
this.chatExtrasStore.removeMessageIdToEdit(this.token)
} else {
this.chatExtrasStore.removeParentIdToReply(this.token)
}
EventBus.$emit('focus-chat-input')
},
@ -321,6 +336,11 @@ export default {
text-align: start;
}
}
&__edit-hint {
display: flex;
align-items: center;
gap: 4px;
}
}
&__right {
flex: 0 0 44px;

7
src/services/messagesService.js

@ -178,6 +178,12 @@ const getReactionsDetails = async function(token, messageId) {
return axios.get(generateOcsUrl('apps/spreed/api/v1/reaction/{token}/{messageId}', { token, messageId }))
}
const editMessage = async function({ token, messageId, updatedMessage }, options) {
return axios.put(generateOcsUrl('apps/spreed/api/v1/chat/{token}/{messageId}', { token, messageId }), {
message: updatedMessage,
}, options)
}
const getTranslationLanguages = async function() {
return axios.get(generateOcsUrl('/translation/languages'))
}
@ -201,6 +207,7 @@ export {
addReactionToMessage,
removeReactionFromMessage,
getReactionsDetails,
editMessage,
getTranslationLanguages,
translateText,
}

8
src/store/messagesStore.js

@ -32,6 +32,7 @@ import {
CONVERSATION,
} from '../constants.js'
import { fetchNoteToSelfConversation } from '../services/conversationsService.js'
import { EventBus } from '../services/EventBus.js'
import {
deleteMessage,
updateLastReadMessage,
@ -529,11 +530,16 @@ const actions = {
&& (message.systemMessage === 'message_deleted'
|| message.systemMessage === 'reaction'
|| message.systemMessage === 'reaction_deleted'
|| message.systemMessage === 'reaction_revoked')) {
|| message.systemMessage === 'reaction_revoked'
|| message.systemMessage === 'message_edited')) {
// If parent message is presented in store already, we update it
const parentInStore = context.getters.message(token, message.parent.id)
if (Object.keys(parentInStore).length !== 0) {
context.commit('addMessage', { token, message: message.parent })
if (message.systemMessage === 'message_edited') {
EventBus.$emit('message-edited')
return
}
}
const reactionsStore = useReactionsStore()

25
src/stores/__tests__/chatExtras.spec.js

@ -117,6 +117,31 @@ describe('chatExtrasStore', () => {
})
})
describe('current edit input message', () => {
it('sets current edit input message', () => {
// Act
chatExtrasStore.setChatEditInput({ token: 'token-1', text: 'This is an edited message' })
chatExtrasStore.setMessageIdToEdit('token-1', 'id-1')
// Assert
expect(chatExtrasStore.getChatEditInput('token-1')).toStrictEqual('This is an edited message')
expect(chatExtrasStore.getMessageIdToEdit('id-1')).toBe(undefined)
})
it('clears current edit input message', () => {
// Arrange
chatExtrasStore.setChatEditInput({ token: 'token-1', text: 'This is an edited message' })
chatExtrasStore.setMessageIdToEdit('token-1', 'id-1')
// Act
chatExtrasStore.removeMessageIdToEdit('token-1')
// Assert
expect(chatExtrasStore.chatEditInput['token-1']).not.toBeDefined()
expect(chatExtrasStore.getChatEditInput('token-1')).toBe('')
})
})
describe('purge store', () => {
it('clears store for provided token', async () => {
// Arrange

47
src/stores/chatExtras.js

@ -48,6 +48,8 @@ export const useChatExtrasStore = defineStore('chatExtras', {
absence: {},
parentToReply: {},
chatInput: {},
messageIdToEdit: {},
chatEditInput: {},
}),
getters: {
@ -60,6 +62,14 @@ export const useChatExtrasStore = defineStore('chatExtras', {
getChatInput: (state) => (token) => {
return state.chatInput[token] ?? ''
},
getChatEditInput: (state) => (token) => {
return state.chatEditInput[token] ?? ''
},
getMessageIdToEdit: (state) => (token) => {
return state.messageIdToEdit[token]
},
},
actions: {
@ -135,6 +145,43 @@ export const useChatExtrasStore = defineStore('chatExtras', {
Vue.set(this.chatInput, token, parsedText)
},
/**
* Add a message text that is being edited to the store for a given conversation token
*
* @param {object} payload action payload
* @param {string} payload.token The conversation token
* @param {string} payload.text The string to store
*/
setChatEditInput({ token, text }) {
// FIXME upstream: https://github.com/nextcloud-libraries/nextcloud-vue/issues/4492
const temp = document.createElement('textarea')
temp.innerHTML = text.replace(/&/gmi, '&amp;')
const parsedText = temp.value.replace(/&amp;/gmi, '&').replace(/&lt;/gmi, '<')
.replace(/&gt;/gmi, '>').replace(/&sect;/gmi, '§')
Vue.set(this.chatEditInput, token, parsedText)
},
/**
* Add a message id that is being edited to the store
*
* @param {string} token The conversation token
* @param {number} id The id of message
*/
setMessageIdToEdit(token, id) {
Vue.set(this.messageIdToEdit, token, id)
},
/**
* Remove a message id that is being edited to the store
*
* @param {string} token The conversation token
*/
removeMessageIdToEdit(token) {
Vue.delete(this.chatEditInput, token)
Vue.delete(this.messageIdToEdit, token)
},
/**
* Remove a current input value from the store for a given conversation token
*

Loading…
Cancel
Save