diff --git a/src/components/LeftSidebar/LeftSidebar.vue b/src/components/LeftSidebar/LeftSidebar.vue index ea7bba3462e..5edd7e15cd5 100644 --- a/src/components/LeftSidebar/LeftSidebar.vue +++ b/src/components/LeftSidebar/LeftSidebar.vue @@ -7,19 +7,6 @@ - + + + - @@ -277,6 +293,7 @@ import IconCogOutline from 'vue-material-design-icons/CogOutline.vue' import IconFilterOutline from 'vue-material-design-icons/FilterOutline.vue' import IconFilterRemoveOutline from 'vue-material-design-icons/FilterRemoveOutline.vue' import IconFormatListBulleted from 'vue-material-design-icons/FormatListBulleted.vue' +import IconForumOutline from 'vue-material-design-icons/ForumOutline.vue' import IconHomeOutline from 'vue-material-design-icons/HomeOutline.vue' import IconMessageBadgeOutline from 'vue-material-design-icons/MessageBadgeOutline.vue' import IconMessageOutline from 'vue-material-design-icons/MessageOutline.vue' @@ -284,6 +301,7 @@ import IconNoteEditOutline from 'vue-material-design-icons/NoteEditOutline.vue' import IconPhoneOutline from 'vue-material-design-icons/PhoneOutline.vue' import IconPlus from 'vue-material-design-icons/Plus.vue' import NewConversationDialog from '../NewConversationDialog/NewConversationDialog.vue' +import ThreadItem from '../RightSidebar/Threads/ThreadItem.vue' import SearchBox from '../UIShared/SearchBox.vue' import TransitionWrapper from '../UIShared/TransitionWrapper.vue' import CallPhoneDialog from './CallPhoneDialog/CallPhoneDialog.vue' @@ -305,6 +323,7 @@ import { autocompleteQuery } from '../../services/coreService.ts' import { EventBus } from '../../services/EventBus.ts' import { talkBroadcastChannel } from '../../services/talkBroadcastChannel.js' import { useActorStore } from '../../stores/actor.ts' +import { useChatExtrasStore } from '../../stores/chatExtras.ts' import { useFederationStore } from '../../stores/federation.ts' import { useSettingsStore } from '../../stores/settings.js' import { useTalkHashStore } from '../../stores/talkHash.js' @@ -333,6 +352,7 @@ export default { name: 'LeftSidebar', components: { + ThreadItem, CallPhoneDialog, InvitationHandler, NcAppNavigation, @@ -359,6 +379,7 @@ export default { IconArchiveOutline, IconArrowLeft, IconCalendarBlankOutline, + IconForumOutline, IconHomeOutline, IconPhoneOutline, IconPlus, @@ -375,6 +396,7 @@ export default { const scroller = ref(null) const showArchived = ref(false) + const showThreadsList = ref(false) const filters = ref(BrowserStorage.getItem('filterEnabled')?.split(',') ?? []) const federationStore = useFederationStore() @@ -398,9 +420,11 @@ export default { canNoteToSelf, supportsArchive, showArchived, + showThreadsList, settingsStore, FILTER_LABELS, actorStore: useActorStore(), + chatExtrasStore: useChatExtrasStore(), tokenStore: useTokenStore(), } }, @@ -449,6 +473,8 @@ export default { emptyContentDescription() { if (this.showArchived) { return t('spreed', 'You have no archived conversations.') + } else if (this.showThreadsList) { + return t('spreed', 'You have no followed threads.') } if (this.filters.length === 1 && this.filters[0] === 'mentions') { return t('spreed', 'You have no unread mentions.') @@ -486,6 +512,10 @@ export default { return validConversationsCount === 0 && !this.isNavigating ? [] : filteredConversations }, + followedThreads() { + return this.chatExtrasStore.getSubscribedThreadsList + }, + isSearching() { return this.searchText !== '' }, @@ -511,16 +541,6 @@ export default { conversationsInitialised() { return this.$store.getters.conversationsInitialised }, - - isInDashboard() { - return this.$route.name === 'root' - }, - - dashboardButtonLabel() { - return this.isInDashboard - ? t('spreed', 'Reload Talk home') - : t('spreed', 'Talk home') - }, }, watch: { @@ -529,6 +549,14 @@ export default { this.isNavigating = true } }, + + showThreadsList(value) { + if (value) { + // Refresh a list + // FIXME requests should be paginated with offset + this.chatExtrasStore.fetchSubscribedThreadsList() + } + }, }, beforeMount() { @@ -937,6 +965,7 @@ export default { this.abortSearch() this.$store.dispatch('joinConversation', { token: to.params.token }) this.showArchived = this.$store.getters.conversation(to.params.token)?.isArchived ?? false + this.showThreadsList = false this.scrollToConversation(to.params.token) } if (this.isMobile) { @@ -955,10 +984,8 @@ export default { actualizeDataTimeout = null }, 5_000) - if (this.isInDashboard) { + if (this.$route.name === 'root') { EventBus.emit('refresh-talk-dashboard') - } else { - this.$router.push({ name: 'root' }) } }, }, @@ -992,7 +1019,7 @@ export default { } } -.invitation-button { +.navigation-item { padding-inline: calc(var(--default-grid-baseline) * 2); margin-block: var(--default-grid-baseline); @@ -1024,13 +1051,11 @@ export default { transition: all 0.15s ease; z-index: 1; // TODO replace with NcAppNavigationSearch - width: calc(100% - var(--default-grid-baseline) * 3 - var(--default-clickable-area) * 3); + width: calc(100% - (var(--default-grid-baseline) + var(--default-clickable-area)) * 2); display: flex; - margin-inline-start: calc(var(--default-clickable-area) + var(--default-grid-baseline)); &--expanded { width: 100%; - margin-inline-start: 0; } :deep(.input-field) { @@ -1038,11 +1063,6 @@ export default { } } -.talk-home-button { - margin-inline-end: var(--default-grid-baseline); - position: absolute !important; -} - .conversations__filters { display: flex; flex-wrap: wrap; diff --git a/src/services/messagesService.ts b/src/services/messagesService.ts index 93ca04717ac..790070d6982 100644 --- a/src/services/messagesService.ts +++ b/src/services/messagesService.ts @@ -5,7 +5,6 @@ import type { AxiosRequestConfig } from '@nextcloud/axios' import type { - ChatMessage, clearHistoryResponse, deleteMessageResponse, editMessageParams, @@ -14,6 +13,8 @@ import type { getMessageContextResponse, getRecentThreadsParams, getRecentThreadsResponse, + getSubscribedThreadsParams, + getSubscribedThreadsResponse, getThreadResponse, markUnreadResponse, postNewMessageParams, @@ -281,6 +282,24 @@ async function getSingleThreadForConversation(token: string, threadId: number, o return axios.get(generateOcsUrl('apps/spreed/api/v1/chat/{token}/threads/{threadId}', { token, threadId }), options) } +/** + * Fetch a list of threads user subscribed to + * + * @param data the wrapping object + * @param [data.limit=50] Number of threads to return + * @param [data.offset] Thread offset to fetch from + * @param [options] Axios request options + */ +async function getSubscribedThreads({ limit, offset }: getSubscribedThreadsParams = {}, options?: AxiosRequestConfig): getSubscribedThreadsResponse { + return axios.get(generateOcsUrl('apps/spreed/api/v1/chat/subscribed-threads'), { + ...options, + params: { + limit, + offset, + }, + }) +} + /** * Create a new thread for a conversation * @@ -303,6 +322,7 @@ export { getMessageContext, getRecentThreadsForConversation, getSingleThreadForConversation, + getSubscribedThreads, pollNewMessages, postNewMessage, postRichObjectToConversation, diff --git a/src/stores/chatExtras.ts b/src/stores/chatExtras.ts index d41a883d855..a404da7b14b 100644 --- a/src/stores/chatExtras.ts +++ b/src/stores/chatExtras.ts @@ -16,6 +16,7 @@ import { EventBus } from '../services/EventBus.ts' import { getRecentThreadsForConversation, getSingleThreadForConversation, + getSubscribedThreads, setThreadNotificationLevel, summarizeChat, } from '../services/messagesService.ts' @@ -23,6 +24,7 @@ import { parseMentions, parseSpecialSymbols } from '../utils/textParse.ts' type State = { threads: Record> + subscribedThreads: Set threadTitle: Record parentToReply: Record chatInput: Record @@ -39,6 +41,7 @@ type State = { export const useChatExtrasStore = defineStore('chatExtras', { state: (): State => ({ threads: {}, + subscribedThreads: new Set(), threadTitle: {}, parentToReply: {}, chatInput: {}, @@ -64,6 +67,13 @@ export const useChatExtrasStore = defineStore('chatExtras', { } }, + getSubscribedThreadsList: (state): ThreadInfo[] => { + return Object.keys(state.threads) + .flatMap((token) => Object.values(state.threads[token] ?? {})) + .filter((threadInfo) => state.subscribedThreads.has(threadInfo.thread.id)) + .sort((a, b) => b.thread.lastActivity - a.thread.lastActivity) + }, + getThreadTitle: (state) => (token: string) => { return state.threadTitle[token] }, @@ -142,6 +152,22 @@ export const useChatExtrasStore = defineStore('chatExtras', { } }, + /** + * Fetch list of subscribed threads from server + * @param offset thread offset to start fetch with + */ + async fetchSubscribedThreadsList(offset?: number) { + try { + const response = await getSubscribedThreads({ offset }) + response.data.ocs.data.forEach((threadInfo) => { + this.subscribedThreads.add(threadInfo.thread.id) + this.addThread(threadInfo.thread.roomToken, threadInfo) + }) + } catch (error) { + console.error('Error fetching threads:', error) + } + }, + /** * Create a thread from a reply chain in given conversation * If thread already exists, subscribe to it diff --git a/src/types/index.ts b/src/types/index.ts index 20be37d4794..51e63f21b5b 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -270,6 +270,8 @@ export type ThreadAttendee = components['schemas']['ThreadAttendee'] export type ThreadInfo = components['schemas']['ThreadInfo'] export type getRecentThreadsParams = operations['thread-get-recent-active-threads']['parameters']['query'] export type getRecentThreadsResponse = ApiResponse +export type getSubscribedThreadsParams = operations['thread-get-subscribed-threads']['parameters']['query'] +export type getSubscribedThreadsResponse = ApiResponse export type getThreadResponse = ApiResponse export type setThreadNotificationLevelParams = operations['thread-set-notification-level']['requestBody']['content']['application/json'] export type setThreadNotificationLevelResponse = ApiResponse