Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 68 additions & 48 deletions src/components/LeftSidebar/LeftSidebar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,6 @@
<NcAppNavigation ref="leftSidebar" :aria-label="t('spreed', 'Conversation list')">
<template #search>
<div class="new-conversation">
<TransitionWrapper name="radial-reveal">
<NcButton v-show="searchText === ''"
:variant="isInDashboard ? 'primary' : 'tertiary'"
:class="{ 'hidden-visually': isSearching }"
class="talk-home-button"
:title="dashboardButtonLabel"
:aria-label="dashboardButtonLabel"
@click="refreshTalkDashboard">
<template #icon>
<IconHomeOutline :size="20" />
</template>
</NcButton>
</TransitionWrapper>
<div class="conversations-search"
:class="{ 'conversations-search--expanded': isSearching }">
<SearchBox ref="searchBox"
Expand Down Expand Up @@ -149,17 +136,37 @@
:text="FILTER_LABELS[filter]"
@close="handleFilter(filter)" />
</TransitionWrapper>
<NcAppNavigationItem v-if="pendingInvitationsCount"
class="invitation-button"
:name="t('spreed', 'Pending invitations')"
@click="showInvitationHandler">
<template #icon>
<IconAccountMultiplePlusOutline :size="20" />
</template>
<template #counter>
<NcCounterBubble type="highlighted" :count="pendingInvitationsCount" />
</template>
</NcAppNavigationItem>
<template v-if="!isSearching">
<NcAppNavigationItem
class="navigation-item"
:to="{ name: 'root' }"
:name="t('spreed', 'Talk home')"
@click="refreshTalkDashboard">
<template #icon>
<IconHomeOutline :size="20" />
</template>
</NcAppNavigationItem>
<NcAppNavigationItem v-if="!isSearching"
class="navigation-item"
:name="showThreadsList ? t('spreed', 'Back to conversations') : t('spreed', 'Followed threads')"
@click="showThreadsList = !showThreadsList">
<template #icon>
<IconArrowLeft v-if="showThreadsList" class="bidirectional-icon" :size="20" />
<IconForumOutline v-else :size="20" />
</template>
</NcAppNavigationItem>
<NcAppNavigationItem v-if="pendingInvitationsCount"
class="navigation-item"
:name="t('spreed', 'Pending invitations')"
@click="showInvitationHandler">
<template #icon>
<IconAccountMultiplePlusOutline :size="20" />
</template>
<template #counter>
<NcCounterBubble type="highlighted" :count="pendingInvitationsCount" />
</template>
</NcAppNavigationItem>
</template>
</template>

<template #list>
Expand All @@ -172,6 +179,7 @@
<IconAt v-if="filters.length === 1 && filters[0] === 'mentions'" :size="64" />
<IconMessageBadgeOutline v-else-if="filters.length === 1 && filters[0] === 'unread'" :size="64" />
<IconArchiveOutline v-else-if="showArchived" :size="64" />
<IconForumOutline v-else-if="showThreadsList" :size="64" />
<IconMessageOutline v-else :size="64" />
</template>
<template #action>
Expand All @@ -183,14 +191,22 @@
</NcButton>
</template>
</NcEmptyContent>
<ConversationsListVirtual v-show="filteredConversationsList.length > 0"
<ul v-if="showThreadsList" class="threads-tab__list">
<ThreadItem
v-for="thread of followedThreads"
:key="`thread_${thread.thread.id}`"
:thread="thread" />
</ul>
<ConversationsListVirtual
v-else
v-show="filteredConversationsList.length > 0"
ref="scroller"
:conversations="filteredConversationsList"
:loading="!conversationsInitialised"
:compact="isCompact"
class="scroller"
@scroll="debounceHandleScroll" />
<NcButton v-if="!preventFindingUnread && lastUnreadMentionBelowViewportIndex !== null"
<NcButton v-if="!showThreadsList && !preventFindingUnread && lastUnreadMentionBelowViewportIndex !== null"
class="unread-mention-button"
variant="primary"
@click="scrollBottomUnread">
Expand Down Expand Up @@ -277,13 +293,15 @@ 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'
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'
Expand All @@ -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'
Expand Down Expand Up @@ -333,6 +352,7 @@ export default {
name: 'LeftSidebar',

components: {
ThreadItem,
CallPhoneDialog,
InvitationHandler,
NcAppNavigation,
Expand All @@ -359,6 +379,7 @@ export default {
IconArchiveOutline,
IconArrowLeft,
IconCalendarBlankOutline,
IconForumOutline,
IconHomeOutline,
IconPhoneOutline,
IconPlus,
Expand All @@ -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()
Expand All @@ -398,9 +420,11 @@ export default {
canNoteToSelf,
supportsArchive,
showArchived,
showThreadsList,
settingsStore,
FILTER_LABELS,
actorStore: useActorStore(),
chatExtrasStore: useChatExtrasStore(),
tokenStore: useTokenStore(),
}
},
Expand Down Expand Up @@ -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.')
Expand Down Expand Up @@ -486,6 +512,10 @@ export default {
return validConversationsCount === 0 && !this.isNavigating ? [] : filteredConversations
},

followedThreads() {
return this.chatExtrasStore.getSubscribedThreadsList
},

isSearching() {
return this.searchText !== ''
},
Expand All @@ -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: {
Expand All @@ -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() {
Expand Down Expand Up @@ -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) {
Expand All @@ -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' })
}
},
},
Expand Down Expand Up @@ -992,7 +1019,7 @@ export default {
}
}

.invitation-button {
.navigation-item {
padding-inline: calc(var(--default-grid-baseline) * 2);
margin-block: var(--default-grid-baseline);

Expand Down Expand Up @@ -1024,25 +1051,18 @@ 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) {
margin-block-start: 0;
}
}

.talk-home-button {
margin-inline-end: var(--default-grid-baseline);
position: absolute !important;
}

.conversations__filters {
display: flex;
flex-wrap: wrap;
Expand Down
22 changes: 21 additions & 1 deletion src/services/messagesService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@

import type { AxiosRequestConfig } from '@nextcloud/axios'
import type {
ChatMessage,
clearHistoryResponse,
deleteMessageResponse,
editMessageParams,
Expand All @@ -14,6 +13,8 @@ import type {
getMessageContextResponse,
getRecentThreadsParams,
getRecentThreadsResponse,
getSubscribedThreadsParams,
getSubscribedThreadsResponse,
getThreadResponse,
markUnreadResponse,
postNewMessageParams,
Expand Down Expand Up @@ -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
*
Expand All @@ -303,6 +322,7 @@ export {
getMessageContext,
getRecentThreadsForConversation,
getSingleThreadForConversation,
getSubscribedThreads,
pollNewMessages,
postNewMessage,
postRichObjectToConversation,
Expand Down
26 changes: 26 additions & 0 deletions src/stores/chatExtras.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,15 @@ import { EventBus } from '../services/EventBus.ts'
import {
getRecentThreadsForConversation,
getSingleThreadForConversation,
getSubscribedThreads,
setThreadNotificationLevel,
summarizeChat,
} from '../services/messagesService.ts'
import { parseMentions, parseSpecialSymbols } from '../utils/textParse.ts'

type State = {
threads: Record<string, Record<number, ThreadInfo>>
subscribedThreads: Set<number>
threadTitle: Record<string, string>
parentToReply: Record<string, number>
chatInput: Record<string, string>
Expand All @@ -39,6 +41,7 @@ type State = {
export const useChatExtrasStore = defineStore('chatExtras', {
state: (): State => ({
threads: {},
subscribedThreads: new Set(),
threadTitle: {},
parentToReply: {},
chatInput: {},
Expand All @@ -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]
},
Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<operations['thread-get-recent-active-threads']['responses'][200]['content']['application/json']>
export type getSubscribedThreadsParams = operations['thread-get-subscribed-threads']['parameters']['query']
export type getSubscribedThreadsResponse = ApiResponse<operations['thread-get-subscribed-threads']['responses'][200]['content']['application/json']>
export type getThreadResponse = ApiResponse<operations['thread-get-thread']['responses'][200]['content']['application/json']>
export type setThreadNotificationLevelParams = operations['thread-set-notification-level']['requestBody']['content']['application/json']
export type setThreadNotificationLevelResponse = ApiResponse<operations['thread-set-notification-level']['responses'][200]['content']['application/json']>
Expand Down