diff --git a/src/__mocks__/capabilities.ts b/src/__mocks__/capabilities.ts
index 43c9da15a1..8c5d6dd725 100644
--- a/src/__mocks__/capabilities.ts
+++ b/src/__mocks__/capabilities.ts
@@ -100,6 +100,7 @@ export const mockedCapabilities: Capabilities = {
'mutual-calendar-events',
'upcoming-reminders',
'sensitive-conversations',
+ 'threads',
// Conditional features
'message-expiration',
'reactions',
diff --git a/src/components/MessagesList/MessagesGroup/Message/MessageButtonsBar/MessageButtonsBar.vue b/src/components/MessagesList/MessagesGroup/Message/MessageButtonsBar/MessageButtonsBar.vue
index 0267878e4a..53fd805f29 100644
--- a/src/components/MessagesList/MessagesGroup/Message/MessageButtonsBar/MessageButtonsBar.vue
+++ b/src/components/MessagesList/MessagesGroup/Message/MessageButtonsBar/MessageButtonsBar.vue
@@ -139,6 +139,28 @@
{{ t('spreed', 'Download file') }}
+
+
+
+
+
+
+ {{ t('spreed', 'Go to thread') }}
+
+
+
+
+
+
+ {{ t('spreed', 'Create a thread') }}
+
+
-
+
+
+
+
+
+
+
@@ -63,14 +73,17 @@ import moment from '@nextcloud/moment'
import debounce from 'debounce'
import uniqueId from 'lodash/uniqueId.js'
import { computed } from 'vue'
+import NcButton from '@nextcloud/vue/components/NcButton'
import NcEmptyContent from '@nextcloud/vue/components/NcEmptyContent'
import NcLoadingIcon from '@nextcloud/vue/components/NcLoadingIcon'
+import IconArrowLeft from 'vue-material-design-icons/ArrowLeft.vue'
import Message from 'vue-material-design-icons/Message.vue'
import LoadingPlaceholder from '../UIShared/LoadingPlaceholder.vue'
import TransitionWrapper from '../UIShared/TransitionWrapper.vue'
import MessagesGroup from './MessagesGroup/MessagesGroup.vue'
import MessagesSystemGroup from './MessagesGroup/MessagesSystemGroup.vue'
import { useDocumentVisibility } from '../../composables/useDocumentVisibility.ts'
+import { useGetThreadId } from '../../composables/useGetThreadId.ts'
import { useIsInCall } from '../../composables/useIsInCall.js'
import { ATTENDEE, CHAT, CONVERSATION, MESSAGE } from '../../constants.ts'
import { EventBus } from '../../services/EventBus.ts'
@@ -83,8 +96,10 @@ const SCROLL_TOLERANCE = 10
export default {
name: 'MessagesList',
components: {
+ IconArrowLeft,
LoadingPlaceholder,
Message,
+ NcButton,
NcEmptyContent,
NcLoadingIcon,
TransitionWrapper,
@@ -121,10 +136,13 @@ export default {
setup(props) {
const isDocumentVisible = useDocumentVisibility()
const isChatVisible = computed(() => isDocumentVisible.value && props.isVisible)
+ const threadId = useGetThreadId()
+
return {
isInCall: useIsInCall(),
chatExtrasStore: useChatExtrasStore(),
isChatVisible,
+ threadId,
}
},
@@ -188,7 +206,14 @@ export default {
* @return {Array}
*/
messagesList() {
+ if (!this.threadId) {
+ return this.$store.getters.messagesList(this.token)
+ }
+
return this.$store.getters.messagesList(this.token)
+ .filter((message) => {
+ return message.threadId === this.threadId
+ })
},
isMessagesListPopulated() {
@@ -664,7 +689,13 @@ export default {
if (!this.$store.getters.getFirstKnownMessageId(token)) {
try {
// Start from message hash or unread marker
- const startingMessageId = focusMessageId !== null ? focusMessageId : this.conversation.lastReadMessage
+ let startingMessageId = focusMessageId !== null ? focusMessageId : this.conversation.lastReadMessage
+ // Check if thread is initially opened
+ if (this.threadId) {
+ // FIXME temporary get thread messages from the start
+ startingMessageId = this.threadId
+ }
+
// First time load, initialize important properties
if (!startingMessageId) {
throw new Error(`[DEBUG] spreed: context message ID is ${startingMessageId}`)
@@ -1210,10 +1241,9 @@ export default {
async onRouteChange({ from, to }) {
if (from.name === 'conversation' && to.name === 'conversation'
- && from.params.token === to.params.token
- && from.hash !== to.hash) {
- // the hash changed, need to focus/highlight another message
- if (to.hash && to.hash.startsWith('#message_')) {
+ && from.params.token === to.params.token) {
+ if (to.hash && to.hash.startsWith('#message_') && from.hash !== to.hash) {
+ // the hash changed, need to focus/highlight another message
const focusedId = this.getMessageIdFromHash(to.hash)
if (this.messagesList.find((m) => m.id === focusedId)) {
// need some delay (next tick is too short) to be able to run
@@ -1236,6 +1266,21 @@ export default {
await this.getMessageContext(this.token, focusedId)
this.focusMessage(focusedId, true)
}
+ } else if (to.query.threadId && from.query.threadId !== to.query.threadId) {
+ // FIXME temporary get thread messages from the start
+ const topMostThreadMessage = this.$store.getters.messagesList(to.params.token).find((message) => message.id === +to.query.threadId)
+ if (!topMostThreadMessage) {
+ // Update environment around context to fill the gaps
+ this.$store.dispatch('setFirstKnownMessageId', {
+ token: this.token,
+ id: +to.query.threadId,
+ })
+ this.$store.dispatch('setLastKnownMessageId', {
+ token: this.token,
+ id: +to.query.threadId,
+ })
+ await this.getMessageContext(this.token, +to.query.threadId)
+ }
}
}
},
diff --git a/src/components/Quote.vue b/src/components/Quote.vue
index 685e095c31..9ffe35adfe 100644
--- a/src/components/Quote.vue
+++ b/src/components/Quote.vue
@@ -15,6 +15,7 @@ import NcLoadingIcon from '@nextcloud/vue/components/NcLoadingIcon'
import IconClose from 'vue-material-design-icons/Close.vue'
import IconPencilOutline from 'vue-material-design-icons/PencilOutline.vue'
import AvatarWrapper from './AvatarWrapper/AvatarWrapper.vue'
+import { useGetThreadId } from '../composables/useGetThreadId.ts'
import { useMessageInfo } from '../composables/useMessageInfo.ts'
import { AVATAR } from '../constants.ts'
import { EventBus } from '../services/EventBus.ts'
@@ -36,6 +37,7 @@ const { message, canCancel = false, editMessage = false } = defineProps<{
const route = useRoute()
const actorStore = useActorStore()
const chatExtrasStore = useChatExtrasStore()
+const threadId = useGetThreadId()
const {
isFileShare,
@@ -48,9 +50,12 @@ const {
const actorInfo = computed(() => [actorDisplayNameWithFallback.value, remoteServer.value].filter((value) => value).join(' '))
+const query = computed(() => ({ threadId: (isExistingMessage(message) && threadId.value === message.threadId) ? message.threadId : undefined }))
const hash = computed(() => '#message_' + message.id)
-const component = computed(() => canCancel ? { tag: 'div', link: undefined } : { tag: 'router-link', link: { hash: hash.value } })
+const component = computed(() => canCancel
+ ? { tag: 'div', link: undefined }
+ : { tag: 'router-link', link: { query: query.value, hash: hash.value } })
const isOwnMessageQuoted = computed(() => isExistingMessage(message) ? actorStore.checkIfSelfIsActor(message) : false)
diff --git a/src/components/RightSidebar/RightSidebar.vue b/src/components/RightSidebar/RightSidebar.vue
index 39442f2377..64aacf2e39 100644
--- a/src/components/RightSidebar/RightSidebar.vue
+++ b/src/components/RightSidebar/RightSidebar.vue
@@ -25,21 +25,28 @@
+ @update:state="handleUpdateState" />
-
+ @close="handleUpdateState('default')" />
+
+
+
-
+
@@ -140,6 +147,7 @@ import RightSidebarContent from './RightSidebarContent.vue'
import SearchMessagesTab from './SearchMessages/SearchMessagesTab.vue'
import SharedItemsTab from './SharedItems/SharedItemsTab.vue'
import SipSettings from './SipSettings.vue'
+import ThreadsTab from './Threads/ThreadsTab.vue'
import { useGetParticipants } from '../../composables/useGetParticipants.ts'
import { useGetToken } from '../../composables/useGetToken.ts'
import { CONVERSATION, PARTICIPANT, WEBINAR } from '../../constants.ts'
@@ -167,6 +175,7 @@ export default {
SearchMessagesTab,
SetGuestUsername,
SharedItemsTab,
+ ThreadsTab,
SipSettings,
// Icons
IconAccountMultiple,
@@ -250,7 +259,7 @@ export default {
return {
contactsLoading: false,
unreadNotificationHandle: null,
- showSearchMessagesTab: false,
+ contentState: 'default',
}
},
@@ -427,6 +436,7 @@ export default {
isInCall(newValue) {
if (newValue) {
// Set 'chat' tab as active, and switch to it if sidebar is open
+ this.contentState = 'default'
this.activeTab = 'chat'
return
}
@@ -506,11 +516,13 @@ export default {
}
},
- handleShowSearch(value) {
- this.showSearchMessagesTab = value
+ handleUpdateState(value) {
+ this.contentState = value
// FIXME upstream: NcAppSidebar should emit update:active
- if (value) {
+ if (value === 'search') {
this.activeTab = 'search-messages'
+ } else if (value === 'threads') {
+ this.activeTab = 'threads'
} else {
this.activeTab = this.isInCall ? 'chat' : 'participants'
}
diff --git a/src/components/RightSidebar/RightSidebarContent.vue b/src/components/RightSidebar/RightSidebarContent.vue
index f81d51dd92..7fb311079c 100644
--- a/src/components/RightSidebar/RightSidebarContent.vue
+++ b/src/components/RightSidebar/RightSidebarContent.vue
@@ -43,14 +43,16 @@ type MutualEvent = {
color: string
}
+type SidebarContentState = 'default' | 'search' | 'threads'
+
const props = defineProps<{
isUser: boolean
- state: 'default' | 'search'
+ state: SidebarContentState
mode: 'compact' | 'preview' | 'full'
}>()
const emit = defineEmits<{
- (event: 'update:search', value: boolean): void
+ (event: 'update:state', value: SidebarContentState): void
(event: 'update:mode', value: 'compact' | 'preview' | 'full'): void
}>()
@@ -83,6 +85,11 @@ const sidebarTitle = computed(() => {
escape: 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
})
@@ -202,7 +209,7 @@ function handleHeaderClick() {
+ @click="emit('update:state', 'search')">
@@ -262,12 +269,12 @@ function handleHeaderClick() {
-
+