summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMaksim Sukharev <antreesy.web@gmail.com>2023-04-24 13:35:02 +0200
committerGitHub <noreply@github.com>2023-04-24 13:35:02 +0200
commitb61866d3ec2a80b7eea038f7b8538c4db666b644 (patch)
tree98169a56fddafe9b7602680af874ae212697c838
parent26f74213b2a04d4c52c4a8e5b1a86c2952a36fb1 (diff)
parent82d3a6e3df552432ed39f658cd0a286c7a853486 (diff)
Merge pull request #9346 from nextcloud/fix/6808/mark-conversation-unread
Allow to mark conversation as unread
-rw-r--r--src/components/LeftSidebar/ConversationsList/Conversation.spec.js12
-rw-r--r--src/components/LeftSidebar/ConversationsList/Conversation.vue24
-rw-r--r--src/services/conversationsService.js15
-rw-r--r--src/store/conversationsStore.js122
-rw-r--r--src/store/conversationsStore.spec.js28
5 files changed, 141 insertions, 60 deletions
diff --git a/src/components/LeftSidebar/ConversationsList/Conversation.spec.js b/src/components/LeftSidebar/ConversationsList/Conversation.spec.js
index a6434108e..5a6ab3ad4 100644
--- a/src/components/LeftSidebar/ConversationsList/Conversation.spec.js
+++ b/src/components/LeftSidebar/ConversationsList/Conversation.spec.js
@@ -530,10 +530,22 @@ describe('Conversation.vue', () => {
expect(toggleFavoriteAction).toHaveBeenCalledWith(expect.anything(), item)
})
+ test('marks conversation as unread', async () => {
+ const markConversationUnreadAction = jest.fn().mockResolvedValueOnce()
+ testStoreConfig.modules.conversationsStore.actions.markConversationUnread = markConversationUnreadAction
+
+ const action = shallowMountAndGetAction('Mark as unread')
+ expect(action.exists()).toBe(true)
+
+ await action.find('button').trigger('click')
+
+ expect(markConversationUnreadAction).toHaveBeenCalledWith(expect.anything(), { token: item.token })
+ })
test('marks conversation as read', async () => {
const clearLastReadMessageAction = jest.fn().mockResolvedValueOnce()
testStoreConfig.modules.conversationsStore.actions.clearLastReadMessage = clearLastReadMessageAction
+ item.unreadMessages = 1
const action = shallowMountAndGetAction('Mark as read')
expect(action.exists()).toBe(true)
diff --git a/src/components/LeftSidebar/ConversationsList/Conversation.vue b/src/components/LeftSidebar/ConversationsList/Conversation.vue
index 94224fd84..1376e4997 100644
--- a/src/components/LeftSidebar/ConversationsList/Conversation.vue
+++ b/src/components/LeftSidebar/ConversationsList/Conversation.vue
@@ -62,13 +62,22 @@
@click.stop.prevent="handleCopyLink">
{{ t('spreed', 'Copy link') }}
</NcActionButton>
- <NcActionButton :close-after-click="true"
+ <NcActionButton v-if="item.unreadMessages"
+ :close-after-click="true"
@click.prevent.exact="markConversationAsRead">
<template #icon>
<EyeOutline :size="16" />
</template>
{{ t('spreed', 'Mark as read') }}
</NcActionButton>
+ <NcActionButton v-else
+ :close-after-click="true"
+ @click.prevent.exact="markConversationAsUnread">
+ <template #icon>
+ <EyeOffOutline :size="16" />
+ </template>
+ {{ t('spreed', 'Mark as unread') }}
+ </NcActionButton>
<NcActionButton :close-after-click="true"
@click.prevent.exact="showConversationSettings">
<Cog slot="icon"
@@ -116,6 +125,7 @@ import ArrowRight from 'vue-material-design-icons/ArrowRight.vue'
import Cog from 'vue-material-design-icons/Cog.vue'
import Delete from 'vue-material-design-icons/Delete.vue'
import ExitToApp from 'vue-material-design-icons/ExitToApp.vue'
+import EyeOffOutline from 'vue-material-design-icons/EyeOffOutline.vue'
import EyeOutline from 'vue-material-design-icons/EyeOutline.vue'
import Star from 'vue-material-design-icons/Star.vue'
@@ -134,14 +144,16 @@ export default {
name: 'Conversation',
components: {
+ ConversationIcon,
+ NcActionButton,
+ NcListItem,
+ // Icons
ArrowRight,
Cog,
- ConversationIcon,
Delete,
ExitToApp,
+ EyeOffOutline,
EyeOutline,
- NcActionButton,
- NcListItem,
Star,
},
@@ -350,6 +362,10 @@ export default {
this.$store.dispatch('clearLastReadMessage', { token: this.item.token })
},
+ markConversationAsUnread() {
+ this.$store.dispatch('markConversationUnread', { token: this.item.token })
+ },
+
showConversationSettings() {
emit('show-conversation-settings', { token: this.item.token })
},
diff --git a/src/services/conversationsService.js b/src/services/conversationsService.js
index 12c754aaf..05547a5a0 100644
--- a/src/services/conversationsService.js
+++ b/src/services/conversationsService.js
@@ -207,6 +207,20 @@ const clearConversationHistory = async function(token) {
}
/**
+ * Set conversation as unread
+ *
+ * @param {string} token The token of the conversation to be set as unread
+ */
+const setConversationUnread = async function(token) {
+ try {
+ const response = axios.delete(generateOcsUrl('apps/spreed/api/v1/chat/{token}/read', { token }))
+ return response
+ } catch (error) {
+ console.debug('Error while setting the conversation as unread: ', error)
+ }
+}
+
+/**
* Add a conversation to the favorites
*
* @param {string} token The token of the conversation to be favorites
@@ -441,6 +455,7 @@ export {
setConversationName,
setConversationDescription,
clearConversationHistory,
+ setConversationUnread,
setConversationPermissions,
setCallPermissions,
setMessageExpiration,
diff --git a/src/store/conversationsStore.js b/src/store/conversationsStore.js
index 69c199232..34743d6e4 100644
--- a/src/store/conversationsStore.js
+++ b/src/store/conversationsStore.js
@@ -46,6 +46,7 @@ import {
setConversationDescription,
deleteConversation,
clearConversationHistory,
+ setConversationUnread,
setNotificationLevel,
setNotificationCalls,
setConversationPermissions,
@@ -123,13 +124,13 @@ const mutations = {
* Deletes a conversation from the store.
*
* @param {object} state current store state;
- * @param {object} token the token of the conversation to delete;
+ * @param {string} token the token of the conversation to delete;
*/
deleteConversation(state, token) {
Vue.delete(state.conversations, token)
},
/**
- * Resets the store to it's original state
+ * Resets the store to its original state
*
* @param {object} state current store state;
*/
@@ -200,7 +201,7 @@ const mutations = {
const actions = {
/**
- * Add a conversation to the store and index the displayname.
+ * Add a conversation to the store and index the displayName.
*
* @param {object} context default store context;
* @param {object} conversation the conversation;
@@ -213,8 +214,8 @@ const actions = {
displayName: context.getters.getDisplayName(),
}
- // Fallback to getCurrentUser() only if if has not been set yet (as
- // getCurrentUser() needs to be overriden in public share pages as it
+ // Fallback to getCurrentUser() only if it has not been set yet (as
+ // getCurrentUser() needs to be overridden in public share pages as it
// always returns an anonymous user).
if (!currentUser.uid) {
currentUser = getCurrentUser()
@@ -252,7 +253,7 @@ const actions = {
*
* @param {object} context default store context;
* @param {object} data the wrapping object;
- * @param {object} data.token the token of the conversation to be deleted;
+ * @param {string} data.token the token of the conversation to be deleted;
*/
async deleteConversationFromServer(context, { token }) {
await deleteConversation(token)
@@ -265,7 +266,7 @@ const actions = {
*
* @param {object} context default store context;
* @param {object} data the wrapping object;
- * @param {object} data.token the token of the conversation whose history is
+ * @param {string} data.token the token of the conversation whose history is
* to be cleared;
*/
async clearConversationHistory(context, { token }) {
@@ -281,7 +282,7 @@ const actions = {
},
/**
- * Resets the store to it's original state.
+ * Resets the store to its original state.
*
* @param {object} context default store context;
*/
@@ -291,11 +292,11 @@ const actions = {
},
async toggleGuests({ commit, getters }, { token, allowGuests }) {
- const conversation = Object.assign({}, getters.conversations[token])
- if (!conversation) {
+ if (!getters.conversations[token]) {
return
}
+ const conversation = Object.assign({}, getters.conversations[token])
if (allowGuests) {
await makePublic(token)
conversation.type = CONVERSATION.TYPE.PUBLIC
@@ -308,8 +309,7 @@ const actions = {
},
async toggleFavorite({ commit, getters }, { token, isFavorite }) {
- const conversation = Object.assign({}, getters.conversations[token])
- if (!conversation) {
+ if (!getters.conversations[token]) {
return
}
@@ -319,17 +319,18 @@ const actions = {
} else {
await addToFavorites(token)
}
- conversation.isFavorite = !isFavorite
+
+ const conversation = Object.assign({}, getters.conversations[token], { isFavorite: !isFavorite })
commit('addConversation', conversation)
},
async toggleLobby({ commit, getters }, { token, enableLobby }) {
- const conversation = Object.assign({}, getters.conversations[token])
- if (!conversation) {
+ if (!getters.conversations[token]) {
return
}
+ const conversation = Object.assign({}, getters.conversations[token])
if (enableLobby) {
await changeLobbyState(token, WEBINAR.LOBBY.NON_MODERATORS)
conversation.lobbyState = WEBINAR.LOBBY.NON_MODERATORS
@@ -342,13 +343,13 @@ const actions = {
},
async setConversationName({ commit, getters }, { token, name }) {
- const conversation = Object.assign({}, getters.conversations[token])
- if (!conversation) {
+ if (!getters.conversations[token]) {
return
}
await setConversationName(token, name)
- conversation.displayName = name
+
+ const conversation = Object.assign({}, getters.conversations[token], { displayName: name })
commit('addConversation', conversation)
},
@@ -368,102 +369,107 @@ const actions = {
},
async setReadOnlyState({ commit, getters }, { token, readOnly }) {
- const conversation = Object.assign({}, getters.conversations[token])
- if (!conversation) {
+ if (!getters.conversations[token]) {
return
}
await changeReadOnlyState(token, readOnly)
- conversation.readOnly = readOnly
+
+ const conversation = Object.assign({}, getters.conversations[token], { readOnly })
commit('addConversation', conversation)
},
async setListable({ commit, getters }, { token, listable }) {
- const conversation = Object.assign({}, getters.conversations[token])
- if (!conversation) {
+ if (!getters.conversations[token]) {
return
}
await changeListable(token, listable)
- conversation.listable = listable
+
+ const conversation = Object.assign({}, getters.conversations[token], { listable })
commit('addConversation', conversation)
},
async setLobbyTimer({ commit, getters }, { token, timestamp }) {
- const conversation = Object.assign({}, getters.conversations[token])
- if (!conversation) {
+ if (!getters.conversations[token]) {
return
}
+ const conversation = Object.assign({}, getters.conversations[token], { lobbyTimer: timestamp })
+
// The backend requires the state and timestamp to be set together.
await changeLobbyState(token, conversation.lobbyState, timestamp)
- conversation.lobbyTimer = timestamp
commit('addConversation', conversation)
},
async setSIPEnabled({ commit, getters }, { token, state }) {
- const conversation = Object.assign({}, getters.conversations[token])
- if (!conversation) {
+ if (!getters.conversations[token]) {
return
}
await setSIPEnabled(token, state)
- conversation.sipEnabled = state
+
+ const conversation = Object.assign({}, getters.conversations[token], { sipEnabled: state })
commit('addConversation', conversation)
},
async setConversationProperties({ commit, getters }, { token, properties }) {
- let conversation = Object.assign({}, getters.conversations[token])
- if (!conversation) {
+ if (!getters.conversations[token]) {
return
}
- conversation = Object.assign(conversation, properties)
+ const conversation = Object.assign({}, getters.conversations[token], properties)
commit('addConversation', conversation)
},
async markConversationRead({ commit, getters }, token) {
- const conversation = Object.assign({}, getters.conversations[token])
- if (!conversation) {
+ if (!getters.conversations[token]) {
return
}
- conversation.unreadMessages = 0
- conversation.unreadMention = false
+ commit('updateUnreadMessages', { token, unreadMessages: 0, unreadMention: false })
+ },
- commit('addConversation', conversation)
+ async markConversationUnread({ commit, dispatch, getters }, { token }) {
+ if (!getters.conversations[token]) {
+ return
+ }
+
+ await setConversationUnread(token)
+ commit('updateUnreadMessages', { token, unreadMessages: 1 })
+ await dispatch('fetchConversation', { token })
},
async updateLastCommonReadMessage({ commit, getters }, { token, lastCommonReadMessage }) {
- const conversation = Object.assign({}, getters.conversations[token])
- if (!conversation) {
+ if (!getters.conversations[token]) {
return
}
- conversation.lastCommonReadMessage = lastCommonReadMessage
+ const conversation = Object.assign({}, getters.conversations[token], { lastCommonReadMessage })
commit('addConversation', conversation)
},
async updateConversationLastActive({ commit, getters }, token) {
- const conversation = Object.assign({}, getters.conversations[token])
- if (!conversation) {
+ if (!getters.conversations[token]) {
return
}
- conversation.lastActivity = (new Date().getTime()) / 1000
+ const conversation = Object.assign({}, getters.conversations[token], {
+ lastActivity: (new Date().getTime()) / 1000,
+ })
commit('addConversation', conversation)
},
async updateConversationLastMessage({ commit }, { token, lastMessage }) {
/**
- * Only use the last message as lastmessage when:
+ * Only use the last message as lastMessage when:
* 1. It's not a command reply
* 2. It's not a temporary message starting with "/" which is a user posting a command
* 3. It's not a reaction or deletion of a reaction
@@ -485,13 +491,14 @@ const actions = {
async updateConversationLastMessageFromNotification({ getters, commit }, { notification }) {
const [token, messageId] = notification.objectId.split('/')
- const conversation = { ...getters.conversation(token) }
- if (!conversation) {
+ if (!getters.conversations[token]) {
// Conversation not loaded yet, skipping
return
}
+ const conversation = Object.assign({}, getters.conversations[token])
+
const actor = notification.subjectRichParameters.user || notification.subjectRichParameters.guest || {
type: 'guest',
id: 'unknown',
@@ -549,29 +556,32 @@ const actions = {
async updateCallStateFromNotification({ getters, commit }, { notification }) {
const token = notification.objectId
- const conversation = { ...getters.conversation(token) }
- if (!conversation) {
+ if (!getters.conversations[token]) {
// Conversation not loaded yet, skipping
return
}
- conversation.hasCall = true
- conversation.callFlag = PARTICIPANT.CALL_FLAG.WITH_VIDEO
- conversation.activeSince = (new Date(notification.datetime)).getTime() / 1000
- conversation.lastActivity = conversation.activeSince
- conversation.callStartTime = conversation.activeSince
+ const activeSince = (new Date(notification.datetime)).getTime() / 1000
+
+ const conversation = Object.assign({}, getters.conversations[token], {
+ hasCall: true,
+ callFlag: PARTICIPANT.CALL_FLAG.WITH_VIDEO,
+ activeSince,
+ lastActivity: activeSince,
+ callStartTime: activeSince,
+ })
// Inaccurate but best effort from here on:
const lastMessage = {
token,
- id: 'temp' + conversation.activeSince,
+ id: 'temp' + activeSince,
actorType: 'guests',
actorId: 'unknown',
actorDisplayName: t('spreed', 'Guest'),
message: notification.subjectRich,
messageParameters: notification.subjectRichParameters,
- timestamp: conversation.activeSince,
+ timestamp: activeSince,
messageType: 'system',
systemMessage: 'call_started',
expirationTimestamp: 0,
diff --git a/src/store/conversationsStore.spec.js b/src/store/conversationsStore.spec.js
index 741fd6c6f..54a7ecde2 100644
--- a/src/store/conversationsStore.spec.js
+++ b/src/store/conversationsStore.spec.js
@@ -1,4 +1,5 @@
import { createLocalVue } from '@vue/test-utils'
+import flushPromises from 'flush-promises'
import { cloneDeep } from 'lodash'
import Vuex from 'vuex'
@@ -26,6 +27,7 @@ import {
deleteConversation,
setConversationPermissions,
setCallPermissions,
+ setConversationUnread,
} from '../services/conversationsService.js'
import storeConfig from './storeConfig.js'
@@ -47,6 +49,7 @@ jest.mock('../services/conversationsService', () => ({
deleteConversation: jest.fn(),
setConversationPermissions: jest.fn(),
setCallPermissions: jest.fn(),
+ setConversationUnread: jest.fn(),
}))
describe('conversationsStore', () => {
@@ -583,6 +586,31 @@ describe('conversationsStore', () => {
expect(changedConversation.unreadMention).toBe(false)
})
+ test('marks conversation as unread', async () => {
+ testConversation.unreadMessages = 0
+
+ const response = {
+ data: {
+ ocs: {
+ data: { ...testConversation, unreadMessages: 1 },
+ },
+ },
+ }
+ fetchConversation.mockResolvedValue(response)
+
+ store.dispatch('addConversation', testConversation)
+
+ store.dispatch('markConversationUnread', { token: testToken })
+
+ await flushPromises()
+
+ expect(setConversationUnread).toHaveBeenCalled()
+ expect(fetchConversation).toHaveBeenCalled()
+
+ const changedConversation = store.getters.conversation(testToken)
+ expect(changedConversation.unreadMessages).toBe(1)
+ })
+
test('updates last common read message', () => {
testConversation.lastCommonReadMessage = {
id: 999,