mirror of
https://github.com/nextcloud/spreed.git
synced 2025-07-22 12:01:02 +00:00
feat(threads): show threads overview in RightSidebar
Signed-off-by: Maksim Sukharev <antreesy.web@gmail.com>
This commit is contained in:
@ -100,6 +100,7 @@ export const mockedCapabilities: Capabilities = {
|
|||||||
'mutual-calendar-events',
|
'mutual-calendar-events',
|
||||||
'upcoming-reminders',
|
'upcoming-reminders',
|
||||||
'sensitive-conversations',
|
'sensitive-conversations',
|
||||||
|
'threads',
|
||||||
// Conditional features
|
// Conditional features
|
||||||
'message-expiration',
|
'message-expiration',
|
||||||
'reactions',
|
'reactions',
|
||||||
|
@ -25,21 +25,28 @@
|
|||||||
<RightSidebarContent ref="sidebarContent"
|
<RightSidebarContent ref="sidebarContent"
|
||||||
:is-user="!!getUserId"
|
:is-user="!!getUserId"
|
||||||
:mode="CONTENT_MODES[contentModeIndex]"
|
:mode="CONTENT_MODES[contentModeIndex]"
|
||||||
:state="showSearchMessagesTab ? 'search' : 'default'"
|
:state="contentState"
|
||||||
@update:mode="handleUpdateMode"
|
@update:mode="handleUpdateMode"
|
||||||
@update:search="handleShowSearch" />
|
@update:state="handleUpdateState" />
|
||||||
</template>
|
</template>
|
||||||
<template #description>
|
<template #description>
|
||||||
<InternalSignalingHint />
|
<InternalSignalingHint />
|
||||||
<LobbyStatus v-if="canFullModerate && hasLobbyEnabled" :token="token" />
|
<LobbyStatus v-if="canFullModerate && hasLobbyEnabled" :token="token" />
|
||||||
</template>
|
</template>
|
||||||
<NcAppSidebarTab v-if="showSearchMessagesTab"
|
<NcAppSidebarTab v-if="contentState === 'search'"
|
||||||
id="search-messages"
|
id="search-messages"
|
||||||
key="search-messages"
|
key="search-messages"
|
||||||
:order="0"
|
:order="0"
|
||||||
:name="t('spreed', 'Search messages')">
|
:name="t('spreed', 'Search messages')">
|
||||||
<SearchMessagesTab :is-active="activeTab === 'search-messages'"
|
<SearchMessagesTab :is-active="activeTab === 'search-messages'"
|
||||||
@close="handleShowSearch(false)" />
|
@close="handleUpdateState('default')" />
|
||||||
|
</NcAppSidebarTab>
|
||||||
|
<NcAppSidebarTab v-else-if="contentState === 'threads'"
|
||||||
|
id="threads"
|
||||||
|
key="threads"
|
||||||
|
:order="0"
|
||||||
|
:name="t('spreed', 'Threads')">
|
||||||
|
<ThreadsTab @close="handleUpdateState('default')" />
|
||||||
</NcAppSidebarTab>
|
</NcAppSidebarTab>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<NcAppSidebarTab v-if="isInCall"
|
<NcAppSidebarTab v-if="isInCall"
|
||||||
@ -108,7 +115,7 @@
|
|||||||
<template #icon>
|
<template #icon>
|
||||||
<IconFolderMultipleImage :size="20" />
|
<IconFolderMultipleImage :size="20" />
|
||||||
</template>
|
</template>
|
||||||
<SharedItemsTab :active="activeTab === 'shared-items'" />
|
<SharedItemsTab :active="activeTab === 'shared-items'" @update:state="handleUpdateState" />
|
||||||
</NcAppSidebarTab>
|
</NcAppSidebarTab>
|
||||||
</template>
|
</template>
|
||||||
</NcAppSidebar>
|
</NcAppSidebar>
|
||||||
@ -140,6 +147,7 @@ import RightSidebarContent from './RightSidebarContent.vue'
|
|||||||
import SearchMessagesTab from './SearchMessages/SearchMessagesTab.vue'
|
import SearchMessagesTab from './SearchMessages/SearchMessagesTab.vue'
|
||||||
import SharedItemsTab from './SharedItems/SharedItemsTab.vue'
|
import SharedItemsTab from './SharedItems/SharedItemsTab.vue'
|
||||||
import SipSettings from './SipSettings.vue'
|
import SipSettings from './SipSettings.vue'
|
||||||
|
import ThreadsTab from './Threads/ThreadsTab.vue'
|
||||||
import { useGetParticipants } from '../../composables/useGetParticipants.ts'
|
import { useGetParticipants } from '../../composables/useGetParticipants.ts'
|
||||||
import { useGetToken } from '../../composables/useGetToken.ts'
|
import { useGetToken } from '../../composables/useGetToken.ts'
|
||||||
import { CONVERSATION, PARTICIPANT, WEBINAR } from '../../constants.ts'
|
import { CONVERSATION, PARTICIPANT, WEBINAR } from '../../constants.ts'
|
||||||
@ -167,6 +175,7 @@ export default {
|
|||||||
SearchMessagesTab,
|
SearchMessagesTab,
|
||||||
SetGuestUsername,
|
SetGuestUsername,
|
||||||
SharedItemsTab,
|
SharedItemsTab,
|
||||||
|
ThreadsTab,
|
||||||
SipSettings,
|
SipSettings,
|
||||||
// Icons
|
// Icons
|
||||||
IconAccountMultiple,
|
IconAccountMultiple,
|
||||||
@ -250,7 +259,7 @@ export default {
|
|||||||
return {
|
return {
|
||||||
contactsLoading: false,
|
contactsLoading: false,
|
||||||
unreadNotificationHandle: null,
|
unreadNotificationHandle: null,
|
||||||
showSearchMessagesTab: false,
|
contentState: 'default',
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -427,6 +436,7 @@ export default {
|
|||||||
isInCall(newValue) {
|
isInCall(newValue) {
|
||||||
if (newValue) {
|
if (newValue) {
|
||||||
// Set 'chat' tab as active, and switch to it if sidebar is open
|
// Set 'chat' tab as active, and switch to it if sidebar is open
|
||||||
|
this.contentState = 'default'
|
||||||
this.activeTab = 'chat'
|
this.activeTab = 'chat'
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -506,11 +516,13 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
handleShowSearch(value) {
|
handleUpdateState(value) {
|
||||||
this.showSearchMessagesTab = value
|
this.contentState = value
|
||||||
// FIXME upstream: NcAppSidebar should emit update:active
|
// FIXME upstream: NcAppSidebar should emit update:active
|
||||||
if (value) {
|
if (value === 'search') {
|
||||||
this.activeTab = 'search-messages'
|
this.activeTab = 'search-messages'
|
||||||
|
} else if (value === 'threads') {
|
||||||
|
this.activeTab = 'threads'
|
||||||
} else {
|
} else {
|
||||||
this.activeTab = this.isInCall ? 'chat' : 'participants'
|
this.activeTab = this.isInCall ? 'chat' : 'participants'
|
||||||
}
|
}
|
||||||
|
@ -43,14 +43,16 @@ type MutualEvent = {
|
|||||||
color: string
|
color: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SidebarContentState = 'default' | 'search' | 'threads'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
isUser: boolean
|
isUser: boolean
|
||||||
state: 'default' | 'search'
|
state: SidebarContentState
|
||||||
mode: 'compact' | 'preview' | 'full'
|
mode: 'compact' | 'preview' | 'full'
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(event: 'update:search', value: boolean): void
|
(event: 'update:state', value: SidebarContentState): void
|
||||||
(event: 'update:mode', value: 'compact' | 'preview' | 'full'): void
|
(event: 'update:mode', value: 'compact' | 'preview' | 'full'): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
@ -83,6 +85,11 @@ const sidebarTitle = computed(() => {
|
|||||||
escape: false,
|
escape: false,
|
||||||
sanitize: false,
|
sanitize: false,
|
||||||
})
|
})
|
||||||
|
} else if (props.state === 'threads') {
|
||||||
|
return t('spreed', 'Threads in {name}', { name: conversation.value.displayName }, {
|
||||||
|
escape: false,
|
||||||
|
sanitize: false,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
return conversation.value.displayName
|
return conversation.value.displayName
|
||||||
})
|
})
|
||||||
@ -202,7 +209,7 @@ function handleHeaderClick() {
|
|||||||
<NcButton variant="tertiary"
|
<NcButton variant="tertiary"
|
||||||
:title="t('spreed', 'Search messages')"
|
:title="t('spreed', 'Search messages')"
|
||||||
:aria-label="t('spreed', 'Search messages')"
|
:aria-label="t('spreed', 'Search messages')"
|
||||||
@click="emit('update:search', true)">
|
@click="emit('update:state', 'search')">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<IconMagnify :size="20" />
|
<IconMagnify :size="20" />
|
||||||
</template>
|
</template>
|
||||||
@ -262,12 +269,12 @@ function handleHeaderClick() {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- Search messages in this conversation -->
|
<!-- Search messages in this conversation -->
|
||||||
<template v-else-if="isUser && state === 'search'">
|
<template v-else-if="isUser">
|
||||||
<div class="content__header content__header--row">
|
<div class="content__header content__header--row">
|
||||||
<NcButton variant="tertiary"
|
<NcButton variant="tertiary"
|
||||||
:title="t('spreed', 'Back')"
|
:title="t('spreed', 'Back')"
|
||||||
:aria-label="t('spreed', 'Back')"
|
:aria-label="t('spreed', 'Back')"
|
||||||
@click="emit('update:search', false)">
|
@click="emit('update:state', 'default')">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<IconArrowLeft class="bidirectional-icon" :size="20" />
|
<IconArrowLeft class="bidirectional-icon" :size="20" />
|
||||||
</template>
|
</template>
|
||||||
|
@ -17,11 +17,14 @@ import NcAppNavigationCaption from '@nextcloud/vue/components/NcAppNavigationCap
|
|||||||
import NcButton from '@nextcloud/vue/components/NcButton'
|
import NcButton from '@nextcloud/vue/components/NcButton'
|
||||||
import NcCollectionList from '@nextcloud/vue/components/NcCollectionList'
|
import NcCollectionList from '@nextcloud/vue/components/NcCollectionList'
|
||||||
import NcEmptyContent from '@nextcloud/vue/components/NcEmptyContent'
|
import NcEmptyContent from '@nextcloud/vue/components/NcEmptyContent'
|
||||||
|
import NcListItem from '@nextcloud/vue/components/NcListItem'
|
||||||
import NcRelatedResourcesPanel from '@nextcloud/vue/components/NcRelatedResourcesPanel'
|
import NcRelatedResourcesPanel from '@nextcloud/vue/components/NcRelatedResourcesPanel'
|
||||||
import DotsHorizontal from 'vue-material-design-icons/DotsHorizontal.vue'
|
import DotsHorizontal from 'vue-material-design-icons/DotsHorizontal.vue'
|
||||||
|
import IconDotsHorizontal from 'vue-material-design-icons/DotsHorizontal.vue'
|
||||||
import FolderMultipleImage from 'vue-material-design-icons/FolderMultipleImage.vue'
|
import FolderMultipleImage from 'vue-material-design-icons/FolderMultipleImage.vue'
|
||||||
import IconPoll from 'vue-material-design-icons/Poll.vue'
|
import IconPoll from 'vue-material-design-icons/Poll.vue'
|
||||||
import LoadingComponent from '../../LoadingComponent.vue'
|
import LoadingComponent from '../../LoadingComponent.vue'
|
||||||
|
import ThreadItem from '../Threads/ThreadItem.vue'
|
||||||
import SharedItems from './SharedItems.vue'
|
import SharedItems from './SharedItems.vue'
|
||||||
import SharedItemsBrowser from './SharedItemsBrowser.vue'
|
import SharedItemsBrowser from './SharedItemsBrowser.vue'
|
||||||
import { useGetToken } from '../../../composables/useGetToken.ts'
|
import { useGetToken } from '../../../composables/useGetToken.ts'
|
||||||
@ -29,6 +32,7 @@ import { CONVERSATION } from '../../../constants.ts'
|
|||||||
import { hasTalkFeature } from '../../../services/CapabilitiesManager.ts'
|
import { hasTalkFeature } from '../../../services/CapabilitiesManager.ts'
|
||||||
import { EventBus } from '../../../services/EventBus.ts'
|
import { EventBus } from '../../../services/EventBus.ts'
|
||||||
import { useActorStore } from '../../../stores/actor.ts'
|
import { useActorStore } from '../../../stores/actor.ts'
|
||||||
|
import { useChatExtrasStore } from '../../../stores/chatExtras.ts'
|
||||||
import { useSharedItemsStore } from '../../../stores/sharedItems.ts'
|
import { useSharedItemsStore } from '../../../stores/sharedItems.ts'
|
||||||
import { useSidebarStore } from '../../../stores/sidebar.ts'
|
import { useSidebarStore } from '../../../stores/sidebar.ts'
|
||||||
import {
|
import {
|
||||||
@ -41,6 +45,11 @@ import {
|
|||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
active: boolean
|
active: boolean
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: 'update:state', value: 'threads'): void
|
||||||
|
}>()
|
||||||
|
|
||||||
const token = useGetToken()
|
const token = useGetToken()
|
||||||
const showSharedItemsBrowser = ref(false)
|
const showSharedItemsBrowser = ref(false)
|
||||||
const browserActiveTab = ref('')
|
const browserActiveTab = ref('')
|
||||||
@ -48,6 +57,7 @@ const projectsEnabled = loadState('core', 'projects_enabled', false)
|
|||||||
const hasRelatedResources = ref(false)
|
const hasRelatedResources = ref(false)
|
||||||
|
|
||||||
const store = useStore()
|
const store = useStore()
|
||||||
|
const chatExtrasStore = useChatExtrasStore()
|
||||||
const sharedItemsStore = useSharedItemsStore()
|
const sharedItemsStore = useSharedItemsStore()
|
||||||
const sidebarStore = useSidebarStore()
|
const sidebarStore = useSidebarStore()
|
||||||
const actorStore = useActorStore()
|
const actorStore = useActorStore()
|
||||||
@ -60,9 +70,13 @@ const canCreatePollDrafts = computed(() => {
|
|||||||
const sharedItems = computed(() => sharedItemsStore.sharedItems(token.value))
|
const sharedItems = computed(() => sharedItemsStore.sharedItems(token.value))
|
||||||
const hasSharedItems = computed(() => Object.keys(sharedItems.value).length > 0)
|
const hasSharedItems = computed(() => Object.keys(sharedItems.value).length > 0)
|
||||||
|
|
||||||
|
const supportThreads = computed(() => hasTalkFeature(token.value, 'threads'))
|
||||||
|
const threadsInformation = computed(() => supportThreads.value ? chatExtrasStore.getThreadsList(token.value).slice(0, 3) : [])
|
||||||
|
|
||||||
watch([token, () => props.active, () => sidebarStore.show], ([token, isActive, isOpen]) => {
|
watch([token, () => props.active, () => sidebarStore.show], ([token, isActive, isOpen]) => {
|
||||||
if (token && isActive && isOpen) {
|
if (token && isActive && isOpen) {
|
||||||
sharedItemsStore.getSharedItemsOverview(token)
|
sharedItemsStore.getSharedItemsOverview(token)
|
||||||
|
supportThreads.value && chatExtrasStore.fetchRecentThreadsList(token)
|
||||||
}
|
}
|
||||||
}, { immediate: true })
|
}, { immediate: true })
|
||||||
|
|
||||||
@ -110,6 +124,23 @@ function openPollDraftHandler() {
|
|||||||
</template>
|
</template>
|
||||||
{{ t('spreed', 'Browse poll drafts') }}
|
{{ t('spreed', 'Browse poll drafts') }}
|
||||||
</NcButton>
|
</NcButton>
|
||||||
|
<!-- Threads overview -->
|
||||||
|
<template v-if="supportThreads && threadsInformation.length">
|
||||||
|
<NcAppNavigationCaption :name="t('spreed', 'Recent threads')" />
|
||||||
|
<ul class="threads-list">
|
||||||
|
<ThreadItem v-for="thread of threadsInformation"
|
||||||
|
:key="`thread_${thread.thread.id}`"
|
||||||
|
:thread="thread" />
|
||||||
|
<NcListItem
|
||||||
|
:name="t('spreed', 'Show more threads')"
|
||||||
|
one-line
|
||||||
|
@click="emit('update:state', 'threads')">
|
||||||
|
<template #icon>
|
||||||
|
<IconDotsHorizontal class="threads-icon" :size="20" />
|
||||||
|
</template>
|
||||||
|
</NcListItem>
|
||||||
|
</ul>
|
||||||
|
</template>
|
||||||
<!-- Shared items grouped by type -->
|
<!-- Shared items grouped by type -->
|
||||||
<template v-for="type in sharedItemsOrder" :key="type">
|
<template v-for="type in sharedItemsOrder" :key="type">
|
||||||
<div v-if="sharedItems[type]">
|
<div v-if="sharedItems[type]">
|
||||||
@ -195,4 +226,14 @@ function openPollDraftHandler() {
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.threads {
|
||||||
|
&-list {
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-icon {
|
||||||
|
width: 40px; // AVATAR.SIZE.DEFAULT
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
175
src/components/RightSidebar/Threads/ThreadItem.vue
Normal file
175
src/components/RightSidebar/Threads/ThreadItem.vue
Normal file
@ -0,0 +1,175 @@
|
|||||||
|
<!--
|
||||||
|
- SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||||
|
- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
-->
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { RouteLocationAsRelative } from 'vue-router'
|
||||||
|
import type {
|
||||||
|
ChatMessage,
|
||||||
|
ThreadInfo,
|
||||||
|
} from '../../../types/index.ts'
|
||||||
|
|
||||||
|
import { t } from '@nextcloud/l10n'
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
import { useStore } from 'vuex'
|
||||||
|
import NcActionButton from '@nextcloud/vue/components/NcActionButton'
|
||||||
|
import NcDateTime from '@nextcloud/vue/components/NcDateTime'
|
||||||
|
import NcListItem from '@nextcloud/vue/components/NcListItem'
|
||||||
|
import IconArrowLeftTop from 'vue-material-design-icons/ArrowLeftTop.vue'
|
||||||
|
import IconBellOutline from 'vue-material-design-icons/BellOutline.vue'
|
||||||
|
import AvatarWrapper from '../../AvatarWrapper/AvatarWrapper.vue'
|
||||||
|
import { ATTENDEE } from '../../../constants.ts'
|
||||||
|
|
||||||
|
const { thread } = defineProps<{ thread: ThreadInfo }>()
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const route = useRoute()
|
||||||
|
const store = useStore()
|
||||||
|
|
||||||
|
const threadAuthor = computed(() => thread.first.actorDisplayName.trim().split(' ')[0])
|
||||||
|
const lastActivity = computed(() => thread.thread.lastActivity * 1000)
|
||||||
|
const name = computed(() => getSimpleLine(thread.first))
|
||||||
|
const subname = computed(() => {
|
||||||
|
if (!thread.last) {
|
||||||
|
return t('spreed', 'No messages')
|
||||||
|
}
|
||||||
|
const actor = thread.last.actorDisplayName.trim().split(' ')[0]
|
||||||
|
|
||||||
|
if (!actor && thread.last.actorType === ATTENDEE.ACTOR_TYPE.GUESTS) {
|
||||||
|
return t('spreed', 'Guest')
|
||||||
|
}
|
||||||
|
|
||||||
|
return t('spreed', '{actor}: {lastMessage}', { actor, lastMessage: getSimpleLine(thread.last) }, {
|
||||||
|
escape: false,
|
||||||
|
sanitize: false,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const to = computed<RouteLocationAsRelative>(() => {
|
||||||
|
return {
|
||||||
|
name: 'conversation',
|
||||||
|
params: { token: thread.thread.roomToken },
|
||||||
|
query: { threadId: thread.thread.id },
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const active = computed(() => {
|
||||||
|
return route.fullPath.startsWith(router.resolve(to.value).fullPath)
|
||||||
|
})
|
||||||
|
|
||||||
|
const timeFormat = computed<Intl.DateTimeFormatOptions>(() => {
|
||||||
|
if (new Date().toDateString() === new Date(lastActivity.value).toDateString()) {
|
||||||
|
return { timeStyle: 'short' }
|
||||||
|
}
|
||||||
|
return { dateStyle: 'short' }
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FIXME copied from conversation item/composable/quote component, should be shared from utils
|
||||||
|
* @param message chat message object
|
||||||
|
*/
|
||||||
|
function getSimpleLine(message: ChatMessage | undefined) {
|
||||||
|
if (!message) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
let text = message.message
|
||||||
|
|
||||||
|
Object.entries(message.messageParameters as ChatMessage['messageParameters']).forEach(([key, value]) => {
|
||||||
|
text = text.replaceAll('{' + key + '}', value.name)
|
||||||
|
})
|
||||||
|
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<NcListItem :data-nav-id="`thread_${thread.thread.id}`"
|
||||||
|
class="thread"
|
||||||
|
:name="name"
|
||||||
|
:to="to"
|
||||||
|
:active="active"
|
||||||
|
force-menu>
|
||||||
|
<template #icon>
|
||||||
|
<AvatarWrapper
|
||||||
|
:id="thread.first.actorId"
|
||||||
|
:name="thread.first.actorDisplayName"
|
||||||
|
:source="thread.first.actorType"
|
||||||
|
disable-menu
|
||||||
|
:token="thread.thread.roomToken" />
|
||||||
|
</template>
|
||||||
|
<template #name>
|
||||||
|
<span class="thread__author">{{ threadAuthor }}</span>
|
||||||
|
<span>{{ name }}</span>
|
||||||
|
</template>
|
||||||
|
<template #subname>
|
||||||
|
{{ subname }}
|
||||||
|
</template>
|
||||||
|
<template #actions>
|
||||||
|
<NcActionButton close-after-click
|
||||||
|
@click.stop="() => { console.log('Subscribe') }">
|
||||||
|
<template #icon>
|
||||||
|
<IconBellOutline :size="20" />
|
||||||
|
</template>
|
||||||
|
{{ t('spreed', 'Subscribe to thread') }}
|
||||||
|
</NcActionButton>
|
||||||
|
</template>
|
||||||
|
<template #details>
|
||||||
|
<span class="thread__details">
|
||||||
|
<span class="thread__details-replies">
|
||||||
|
<IconArrowLeftTop :size="16" />
|
||||||
|
{{ thread.thread.numReplies }}
|
||||||
|
</span>
|
||||||
|
<NcDateTime
|
||||||
|
:timestamp="lastActivity"
|
||||||
|
:format="timeFormat"
|
||||||
|
:relative-time="false"
|
||||||
|
ignore-seconds />
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</NcListItem>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.thread {
|
||||||
|
:deep(.list-item-content__name) {
|
||||||
|
font-size: var(--font-size-small);
|
||||||
|
font-weight: 400;
|
||||||
|
color: var(--color-text-maxcontrast);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__author {
|
||||||
|
margin-inline-end: calc(0.5 * var(--default-grid-baseline));
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.list-item-content__subname) {
|
||||||
|
color: var(--color-main-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__details {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-end;
|
||||||
|
font-size: var(--font-size-small);
|
||||||
|
|
||||||
|
&-replies {
|
||||||
|
display: flex;
|
||||||
|
gap: calc(0.5 * var(--default-grid-baseline));
|
||||||
|
padding-inline: calc(2 * var(--default-grid-baseline));
|
||||||
|
border-radius: var(--border-radius-pill);
|
||||||
|
background-color: var(--color-primary-element-light);
|
||||||
|
color: var(--color-main-text);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.list-item__wrapper--active .thread__details-replies {
|
||||||
|
color: var(--color-primary-text);
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
57
src/components/RightSidebar/Threads/ThreadsTab.vue
Normal file
57
src/components/RightSidebar/Threads/ThreadsTab.vue
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
<!--
|
||||||
|
- SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||||
|
- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
-->
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { RouteLocation } from 'vue-router'
|
||||||
|
|
||||||
|
import { computed, onBeforeUnmount, onMounted } from 'vue'
|
||||||
|
import ThreadItem from './ThreadItem.vue'
|
||||||
|
import { useGetToken } from '../../../composables/useGetToken.ts'
|
||||||
|
import { useIsInCall } from '../../../composables/useIsInCall.js'
|
||||||
|
import { EventBus } from '../../../services/EventBus.ts'
|
||||||
|
import { useChatExtrasStore } from '../../../stores/chatExtras.ts'
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: 'close'): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const chatExtrasStore = useChatExtrasStore()
|
||||||
|
const isInCall = useIsInCall()
|
||||||
|
const token = useGetToken()
|
||||||
|
const threadsInformation = computed(() => chatExtrasStore.getThreadsList(token.value))
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
EventBus.on('route-change', onRouteChange)
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
EventBus.off('route-change', onRouteChange)
|
||||||
|
})
|
||||||
|
|
||||||
|
const onRouteChange = ({ from, to }: { from: RouteLocation, to: RouteLocation }): void => {
|
||||||
|
if (to.name !== 'conversation' || from.params.token !== to.params.token || (from.query.threadId !== to.query.threadId && isInCall.value)) {
|
||||||
|
emit('close')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ul class="threads-tab__list">
|
||||||
|
<ThreadItem v-for="thread of threadsInformation"
|
||||||
|
:key="`thread_${thread.thread.id}`"
|
||||||
|
:thread="thread" />
|
||||||
|
</ul>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.threads-tab {
|
||||||
|
&__list {
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
height: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -54,6 +54,14 @@ export const useChatExtrasStore = defineStore('chatExtras', {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getThreadsList: (state) => (token: string): ThreadInfo[] => {
|
||||||
|
if (state.threads[token]) {
|
||||||
|
return Object.values(state.threads[token]).sort((a, b) => b.thread.lastActivity - a.thread.lastActivity)
|
||||||
|
} else {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
getParentIdToReply: (state) => (token: string) => {
|
getParentIdToReply: (state) => (token: string) => {
|
||||||
if (state.parentToReply[token]) {
|
if (state.parentToReply[token]) {
|
||||||
return state.parentToReply[token]
|
return state.parentToReply[token]
|
||||||
|
Reference in New Issue
Block a user