summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMaksim Sukharev <antreesy.web@gmail.com>2024-02-19 08:43:47 +0100
committerGitHub <noreply@github.com>2024-02-19 08:43:47 +0100
commite7faebac85f70a8fbcc50d90848eba50afb9dd94 (patch)
tree631fc59f132b5ec0d60a44db8316b5b2ad506760
parentd0c509675b5ae43bf56b69adc0742b1303e86c9f (diff)
parent88792693bda9897d23798239a21e392d49d4b13e (diff)
Merge pull request #11583 from nextcloud/update-room-selector
Improve room selector modal
-rw-r--r--src/components/LeftSidebar/ConversationsList/Conversation.spec.js6
-rw-r--r--src/components/LeftSidebar/ConversationsList/Conversation.vue118
-rw-r--r--src/components/LeftSidebar/ConversationsList/ConversationSearchResult.vue103
-rw-r--r--src/components/LeftSidebar/ConversationsList/ConversationsListVirtual.vue (renamed from src/components/LeftSidebar/ConversationsListVirtual.vue)4
-rw-r--r--src/components/LeftSidebar/ConversationsList/ConversationsSearchListVirtual.vue86
-rw-r--r--src/components/LeftSidebar/LeftSidebar.vue2
-rw-r--r--src/components/RoomSelector.spec.js211
-rw-r--r--src/components/RoomSelector.vue248
-rw-r--r--src/composables/useConversationInfo.js138
9 files changed, 606 insertions, 310 deletions
diff --git a/src/components/LeftSidebar/ConversationsList/Conversation.spec.js b/src/components/LeftSidebar/ConversationsList/Conversation.spec.js
index 773f42c87..8167a34e3 100644
--- a/src/components/LeftSidebar/ConversationsList/Conversation.spec.js
+++ b/src/components/LeftSidebar/ConversationsList/Conversation.spec.js
@@ -45,13 +45,13 @@ describe('Conversation.vue', () => {
testStoreConfig = cloneDeep(storeConfig)
messagesMock = jest.fn().mockReturnValue({})
testStoreConfig.modules.messagesStore.getters.messages = () => messagesMock
- testStoreConfig.modules.actorStore.getters.getUserId = () => jest.fn().mockReturnValue('user-id-self')
store = new Vuex.Store(testStoreConfig)
// common defaults
item = {
token: TOKEN,
actorId: 'actor-id-1',
+ actorType: ATTENDEE.ACTOR_TYPE.USERS,
participants: [
],
participantType: PARTICIPANT.TYPE.USER,
@@ -155,7 +155,7 @@ describe('Conversation.vue', () => {
})
test('displays own last chat message with "You" as author', () => {
- item.lastMessage.actorId = 'user-id-self'
+ item.lastMessage.actorId = 'actor-id-1'
testConversationLabel(item, 'You: hello')
})
@@ -174,7 +174,7 @@ describe('Conversation.vue', () => {
test('displays own last message with "You" author in one to one conversations', () => {
item.type = CONVERSATION.TYPE.ONE_TO_ONE
- item.lastMessage.actorId = 'user-id-self'
+ item.lastMessage.actorId = 'actor-id-1'
testConversationLabel(item, 'You: hello')
})
diff --git a/src/components/LeftSidebar/ConversationsList/Conversation.vue b/src/components/LeftSidebar/ConversationsList/Conversation.vue
index 8a6b72aba..3752590dc 100644
--- a/src/components/LeftSidebar/ConversationsList/Conversation.vue
+++ b/src/components/LeftSidebar/ConversationsList/Conversation.vue
@@ -138,6 +138,7 @@
<script>
+import { toRefs } from 'vue'
import { Fragment } from 'vue-frag'
import { isNavigationFailure, NavigationFailureType } from 'vue-router'
@@ -162,7 +163,8 @@ import NcListItem from '@nextcloud/vue/dist/Components/NcListItem.js'
import ConversationIcon from './../../ConversationIcon.vue'
-import { CONVERSATION, PARTICIPANT, ATTENDEE } from '../../../constants.js'
+import { useConversationInfo } from '../../../composables/useConversationInfo.js'
+import { CONVERSATION, PARTICIPANT } from '../../../constants.js'
import { copyConversationLinkToClipboard } from '../../../services/urlService.js'
export default {
@@ -215,6 +217,16 @@ export default {
emits: ['click'],
+ setup(props) {
+ const { item, isSearchResult } = toRefs(props)
+ const { counterType, conversationInformation } = useConversationInfo({ item, isSearchResult })
+
+ return {
+ counterType,
+ conversationInformation,
+ }
+ },
+
data() {
return {
isDialogOpen: false,
@@ -226,18 +238,6 @@ export default {
return this.$store.getters.getMainContainerSelector()
},
- counterType() {
- if (this.item.unreadMentionDirect || (this.item.unreadMessages !== 0 && (
- this.item.type === CONVERSATION.TYPE.ONE_TO_ONE || this.item.type === CONVERSATION.TYPE.ONE_TO_ONE_FORMER
- ))) {
- return 'highlighted'
- } else if (this.item.unreadMention) {
- return 'outlined'
- } else {
- return ''
- }
- },
-
canFavorite() {
return this.item.participantType !== PARTICIPANT.TYPE.USER_SELF_JOINED
},
@@ -266,53 +266,6 @@ export default {
return this.item.canLeaveConversation
},
- conversationInformation() {
- // temporary item while joining
- if (!this.isSearchResult && !this.item.actorId) {
- return t('spreed', 'Joining conversation …')
- }
-
- if (!Object.keys(this.lastChatMessage).length) {
- return ''
- }
-
- if (this.shortLastChatMessageAuthor === '') {
- return this.simpleLastChatMessage
- }
-
- if (this.lastChatMessage.actorId === this.$store.getters.getUserId()) {
- return t('spreed', 'You: {lastMessage}', {
- lastMessage: this.simpleLastChatMessage,
- }, undefined, {
- escape: false,
- sanitize: false,
- })
- }
-
- if (this.item.type === CONVERSATION.TYPE.ONE_TO_ONE
- || this.item.type === CONVERSATION.TYPE.ONE_TO_ONE_FORMER
- || this.item.type === CONVERSATION.TYPE.CHANGELOG) {
- return this.simpleLastChatMessage
- }
-
- return t('spreed', '{actor}: {lastMessage}', {
- actor: this.shortLastChatMessageAuthor,
- lastMessage: this.simpleLastChatMessage,
- }, undefined, {
- escape: false,
- sanitize: false,
- })
- },
-
- // Get the last message for this conversation from the message store instead
- // of the conversations store. The message store is updated immediately,
- // while the conversations store is refreshed every 30 seconds. This allows
- // to display message previews in this component as soon as new messages are
- // received by the server.
- lastChatMessage() {
- return this.item.lastMessage
- },
-
dialogMessage() {
return t('spreed', 'Do you really want to delete "{displayName}"?', this.item, undefined, {
escape: false,
@@ -320,51 +273,6 @@ export default {
})
},
- /**
- * This is a simplified version of the last chat message.
- * Parameters are parsed without markup (just replaced with the name),
- * e.g. no avatars on mentions.
- *
- * @return {string} A simple message to show below the conversation name
- */
- simpleLastChatMessage() {
- if (!Object.keys(this.lastChatMessage).length) {
- return ''
- }
-
- const params = this.lastChatMessage.messageParameters
- let subtitle = this.lastChatMessage.message.trim()
-
- // We don't really use rich objects in the subtitle, instead we fall back to the name of the item
- Object.keys(params).forEach((parameterKey) => {
- subtitle = subtitle.replace('{' + parameterKey + '}', params[parameterKey].name)
- })
-
- return subtitle
- },
-
- /**
- * @return {string} Part of the name until the first space
- */
- shortLastChatMessageAuthor() {
- if (!Object.keys(this.lastChatMessage).length
- || this.lastChatMessage.systemMessage.length) {
- return ''
- }
-
- let author = this.lastChatMessage.actorDisplayName.trim()
- const spacePosition = author.indexOf(' ')
- if (spacePosition !== -1) {
- author = author.substring(0, spacePosition)
- }
-
- if (author.length === 0 && this.lastChatMessage.actorType === ATTENDEE.ACTOR_TYPE.GUESTS) {
- return t('spreed', 'Guest')
- }
-
- return author
- },
-
to() {
return this.item?.token
? {
diff --git a/src/components/LeftSidebar/ConversationsList/ConversationSearchResult.vue b/src/components/LeftSidebar/ConversationsList/ConversationSearchResult.vue
new file mode 100644
index 000000000..bec14b91f
--- /dev/null
+++ b/src/components/LeftSidebar/ConversationsList/ConversationSearchResult.vue
@@ -0,0 +1,103 @@
+<!--
+ - @copyright Copyright (c) 2024 Maksim Sukharev <antreesy.web@gmail.com>
+ -
+ - @author Joas Schilling <coding@schilljs.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/>.
+-->
+
+<template>
+ <NcListItem :key="item.token"
+ :name="item.displayName"
+ :title="item.displayName"
+ :active="item.token === selectedRoom"
+ :bold="exposeMessages && !!item.unreadMessages"
+ :counter-number="exposeMessages ? item.unreadMessages : 0"
+ :counter-type="counterType"
+ @click="onClick">
+ <template #icon>
+ <ConversationIcon :item="item" :hide-favorite="!item?.attendeeId" :hide-call="!item?.attendeeId" />
+ </template>
+ <template v-if="conversationInformation" #subname>
+ {{ conversationInformation }}
+ </template>
+ </NcListItem>
+</template>
+
+<script>
+import { inject, toRefs } from 'vue'
+
+import NcListItem from '@nextcloud/vue/dist/Components/NcListItem.js'
+
+import ConversationIcon from './../../ConversationIcon.vue'
+
+import { useConversationInfo } from '../../../composables/useConversationInfo.js'
+
+export default {
+ name: 'ConversationSearchResult',
+
+ components: {
+ ConversationIcon,
+ NcListItem,
+ },
+
+ props: {
+ exposeMessages: {
+ type: Boolean,
+ default: false,
+ },
+ item: {
+ type: Object,
+ default() {
+ return {
+ token: '',
+ participants: [],
+ participantType: 0,
+ unreadMessages: 0,
+ unreadMention: false,
+ objectType: '',
+ type: 0,
+ displayName: '',
+ isFavorite: false,
+ notificationLevel: 0,
+ lastMessage: {},
+ }
+ },
+ },
+ },
+
+ emits: ['click'],
+
+ setup(props) {
+ const { item, exposeMessages } = toRefs(props)
+ const selectedRoom = inject('selectedRoom', null)
+ const { counterType, conversationInformation } = useConversationInfo({ item, exposeMessages })
+
+ return {
+ selectedRoom,
+ counterType,
+ conversationInformation,
+ }
+ },
+
+ methods: {
+ onClick() {
+ this.$emit('click', this.item)
+ },
+ },
+}
+</script>
diff --git a/src/components/LeftSidebar/ConversationsListVirtual.vue b/src/components/LeftSidebar/ConversationsList/ConversationsListVirtual.vue
index 66acca231..1a9e73cbd 100644
--- a/src/components/LeftSidebar/ConversationsListVirtual.vue
+++ b/src/components/LeftSidebar/ConversationsList/ConversationsListVirtual.vue
@@ -37,8 +37,8 @@
<script>
import { RecycleScroller } from 'vue-virtual-scroller'
-import Conversation from './ConversationsList/Conversation.vue'
-import LoadingPlaceholder from '../LoadingPlaceholder.vue'
+import Conversation from './Conversation.vue'
+import LoadingPlaceholder from '../../LoadingPlaceholder.vue'
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'
diff --git a/src/components/LeftSidebar/ConversationsList/ConversationsSearchListVirtual.vue b/src/components/LeftSidebar/ConversationsList/ConversationsSearchListVirtual.vue
new file mode 100644
index 000000000..98b204a42
--- /dev/null
+++ b/src/components/LeftSidebar/ConversationsList/ConversationsSearchListVirtual.vue
@@ -0,0 +1,86 @@
+<!--
+ - @copyright Copyright (c) 2024 Maksim Sukharev <antreesy.web@gmail.com>
+ -
+ - @author Grigorii Shartsev <me@shgk.me>
+ - @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/>.
+ -->
+
+<template>
+ <RecycleScroller ref="scroller"
+ item-tag="ul"
+ :items="conversations"
+ :item-size="CONVERSATION_ITEM_SIZE"
+ key-field="token">
+ <template #default="{ item }">
+ <ConversationSearchResult :item="item" :expose-messages="exposeMessages" @click="onClick" />
+ </template>
+ <template #after>
+ <LoadingPlaceholder v-if="loading" type="conversations" />
+ </template>
+ </RecycleScroller>
+</template>
+
+<script>
+import { RecycleScroller } from 'vue-virtual-scroller'
+
+import ConversationSearchResult from './ConversationSearchResult.vue'
+import LoadingPlaceholder from '../../LoadingPlaceholder.vue'
+
+import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'
+
+const CONVERSATION_ITEM_SIZE = 66
+
+export default {
+ name: 'ConversationsSearchListVirtual',
+
+ components: {
+ LoadingPlaceholder,
+ ConversationSearchResult,
+ RecycleScroller,
+ },
+
+ props: {
+ conversations: {
+ type: Array,
+ required: true,
+ },
+ exposeMessages: {
+ type: Boolean,
+ default: false,
+ },
+ loading: {
+ type: Boolean,
+ default: false,
+ },
+ },
+
+ emits: ['select'],
+
+ setup() {
+ return {
+ CONVERSATION_ITEM_SIZE,
+ }
+ },
+
+ methods: {
+ onClick(item) {
+ this.$emit('select', item)
+ },
+ },
+}
+</script>
diff --git a/src/components/LeftSidebar/LeftSidebar.vue b/src/components/LeftSidebar/LeftSidebar.vue
index ebdf3d4e1..729b70bb5 100644
--- a/src/components/LeftSidebar/LeftSidebar.vue
+++ b/src/components/LeftSidebar/LeftSidebar.vue
@@ -297,7 +297,7 @@ import { useIsMobile } from '@nextcloud/vue/dist/Composables/useIsMobile.js'
import CallPhoneDialog from './CallPhoneDialog/CallPhoneDialog.vue'
import Conversation from './ConversationsList/Conversation.vue'
-import ConversationsListVirtual from './ConversationsListVirtual.vue'
+import ConversationsListVirtual from './ConversationsList/ConversationsListVirtual.vue'
import OpenConversationsList from './OpenConversationsList/OpenConversationsList.vue'
import SearchBox from './SearchBox/SearchBox.vue'
import ConversationIcon from '../ConversationIcon.vue'
diff --git a/src/components/RoomSelector.spec.js b/src/components/RoomSelector.spec.js
index a97e47484..9b62877fb 100644
--- a/src/components/RoomSelector.spec.js
+++ b/src/components/RoomSelector.spec.js
@@ -6,15 +6,33 @@ import { generateOcsUrl } from '@nextcloud/router'
import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
+import ConversationSearchResult from './LeftSidebar/ConversationsList/ConversationSearchResult.vue'
import RoomSelector from './RoomSelector.vue'
import { CONVERSATION } from '../constants.js'
+import { generateOCSResponse } from '../test-helpers.js'
jest.mock('@nextcloud/axios', () => ({
get: jest.fn(),
}))
-describe('RoomSelector.vue', () => {
+const ConversationsSearchListVirtualStub = {
+ props: {
+ conversations: Array,
+ isSearchResult: Boolean,
+ },
+ components: {
+ ConversationSearchResult,
+ },
+ template: `<ul>
+ <ConversationSearchResult v-for="conversation in conversations"
+ :key="conversation.token"
+ :item="conversation"
+ @click="$emit('select', $event)"/>
+ </ul>`,
+}
+
+describe('RoomSelector', () => {
let conversations
beforeEach(() => {
@@ -29,6 +47,7 @@ describe('RoomSelector.vue', () => {
token: 'token-1',
displayName: 'conversation one',
type: CONVERSATION.TYPE.PUBLIC,
+ listable: CONVERSATION.LISTABLE.USERS,
lastActivity: 1,
isFavorite: true,
readOnly: CONVERSATION.STATE.READ_WRITE,
@@ -36,6 +55,7 @@ describe('RoomSelector.vue', () => {
token: 'token-2',
displayName: 'abc',
type: CONVERSATION.TYPE.GROUP,
+ listable: CONVERSATION.LISTABLE.USERS,
lastActivity: 2,
isFavorite: false,
readOnly: CONVERSATION.STATE.READ_ONLY,
@@ -69,82 +89,161 @@ describe('RoomSelector.vue', () => {
},
},
}
-
- axios.get.mockResolvedValue({
- data: {
- ocs: {
- data: conversations,
- },
- },
- })
})
+
afterEach(() => {
jest.clearAllMocks()
})
- test('renders sorted conversation list fetched from server', async () => {
- const wrapper = shallowMount(RoomSelector)
-
- expect(axios.get).toHaveBeenCalledWith(
- generateOcsUrl('/apps/spreed/api/v4/room'),
- { params: { includeStatus: true } }
- )
+ const mountRoomSelector = async (props) => {
+ const payload = conversations.filter(conv => {
+ return !props?.listOpenConversations || conv.listable === CONVERSATION.LISTABLE.USERS
+ })
- // need to wait for re-render, otherwise the list is not rendered yet
- await flushPromises()
+ axios.get.mockResolvedValue(generateOCSResponse({ payload }))
- const list = wrapper.findAll('li')
- expect(list.length).toBe(3)
- expect(list.at(0).text()).toBe('conversation one')
- expect(list.at(1).text()).toBe('zzz')
- expect(list.at(2).text()).toBe('abc')
- })
- test('excludes non-postable conversations', async () => {
const wrapper = shallowMount(RoomSelector, {
- propsData: {
- showPostableOnly: true,
+ stubs: {
+ ConversationsSearchListVirtual: ConversationsSearchListVirtualStub,
+ ConversationSearchResult,
},
+ propsData: props,
})
- expect(axios.get).toHaveBeenCalledWith(
- generateOcsUrl('/apps/spreed/api/v4/room'),
- { params: { includeStatus: true } }
- )
-
// need to wait for re-render, otherwise the list is not rendered yet
await flushPromises()
- const list = wrapper.findAll('li')
- expect(list.length).toBe(2)
- expect(list.at(0).text()).toBe('conversation one')
- expect(list.at(1).text()).toBe('zzz')
- })
- test('emits select event on select', async () => {
- const wrapper = shallowMount(RoomSelector)
+ return wrapper
+ }
+
+ describe('rendering', () => {
+ it('renders sorted conversations list fetched from server', async () => {
+ // Arrange
+ const wrapper = await mountRoomSelector()
+ expect(axios.get).toHaveBeenCalledWith(
+ generateOcsUrl('/apps/spreed/api/v4/room'),
+ { params: { includeStatus: true } }
+ )
+
+ // Assert
+ const list = wrapper.findAllComponents({ name: 'NcListItem' })
+ expect(list).toHaveLength(3)
+ expect(list.at(0).props('name')).toBe(conversations[1].displayName)
+ expect(list.at(1).props('name')).toBe(conversations[0].displayName)
+ expect(list.at(2).props('name')).toBe(conversations[2].displayName)
+ })
- expect(axios.get).toHaveBeenCalledWith(
- generateOcsUrl('/apps/spreed/api/v4/room'),
- { params: { includeStatus: true } }
- )
- await flushPromises()
+ it('renders open conversations list fetched from server', async () => {
+ // Arrange
+ const wrapper = await mountRoomSelector({ listOpenConversations: true })
+ expect(axios.get).toHaveBeenCalledWith(
+ generateOcsUrl('/apps/spreed/api/v4/lis