Skip to content
Draft
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
59 changes: 57 additions & 2 deletions src/components/LeftSidebar/LeftSidebar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,7 @@
:conversations-list="conversationsList"
:search-results="searchResults"
:search-results-listed-conversations="searchResultsListedConversations"
:search-results-messages="searchResultsMessages"
@abort-search="abortSearch"
@create-new-conversation="createConversation"
@create-and-join-conversation="createAndJoinConversation" />
Expand Down Expand Up @@ -354,7 +355,10 @@ import {
fetchNoteToSelfConversation,
searchListedConversations,
} from '../../services/conversationsService.ts'
import { autocompleteQuery } from '../../services/coreService.ts'
import {
autocompleteQuery,
searchMessagesEverywhere,
} from '../../services/coreService.ts'
import { EventBus } from '../../services/EventBus.ts'
import { talkBroadcastChannel } from '../../services/talkBroadcastChannel.js'
import { useActorStore } from '../../stores/actor.ts'
Expand Down Expand Up @@ -477,11 +481,14 @@ export default {
searchText: '',
searchResults: [],
searchResultsListedConversations: [],
searchResultsMessages: [],
contactsLoading: false,
listedConversationsLoading: false,
messagesLoading: false,
canStartConversations: getTalkConfig('local', 'conversations', 'can-create'),
cancelSearchPossibleConversations: () => {},
cancelSearchListedConversations: () => {},
cancelSearchMessages: () => {},
debounceFetchSearchResults: () => {},
debounceFetchConversations: () => {},
debounceHandleScroll: () => {},
Expand Down Expand Up @@ -690,6 +697,9 @@ export default {
this.cancelSearchListedConversations()
this.cancelSearchListedConversations = null

this.cancelSearchMessages()
this.cancelSearchMessages = null

if (this.refreshTimer) {
clearInterval(this.refreshTimer)
this.refreshTimer = null
Expand Down Expand Up @@ -814,6 +824,44 @@ export default {
}
},

async fetchMessagesEverywhere() {
try {
this.messagesLoading = true
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has no use in this case


this.cancelSearchMessages('canceled')
const { request, cancel } = CancelableRequest(searchMessagesEverywhere)
this.cancelSearchMessages = cancel

const response = await request({
term: this.searchText,
limit: 5,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is misleading as it lacks Show more messages button when there can be more results to show

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see a "see more" in the screenshot, so how would we see older results?

})
const data = response?.data?.ocs?.data
if (data && data.entries.length > 0) {
this.searchResultsMessages = data.entries.map((entry) => {
const threadId = (entry.attributes.threadId !== entry.attributes.messageId) ? entry.attributes.threadId : undefined

return {
...entry,
to: {
name: 'conversation',
hash: `#message_${entry.attributes.messageId}`,
params: { token: entry.attributes.conversation },
query: { threadId },
},
}
})
}
this.messagesLoading = false
} catch (exception) {
if (CancelableRequest.isCancel(exception)) {
return
}
console.error('Error searching for messages', exception)
showError(t('spreed', 'An error occurred while performing the search'))
}
},

async fetchSearchResults() {
if (!this.isSearching) {
return
Expand All @@ -824,7 +872,11 @@ export default {
this.showThreadsList = false

this.resetNavigation()
await Promise.all([this.fetchPossibleConversations(), this.fetchListedConversations()])
await Promise.all([
this.fetchPossibleConversations(),
this.fetchListedConversations(),
this.fetchMessagesEverywhere(),
])
Comment on lines +875 to +879
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We wanted to have the global search as an additional action, not the default, didn't we?

Copy link
Copy Markdown
Member

@nimishavijay nimishavijay Dec 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd vote for it being the default.

in case that it not possible, add some filter chips on the top row like ✔️ Conversations ✔️People Messages, so that it is visible that global message search is possible

Edit: the filter chips would be useful either way as the space available is not that much.

Copy link
Copy Markdown
Member

@nickvergessen nickvergessen Dec 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd vote for it being the default.

This is quite problematic. A lot of people use the quick filtering to navigate to other conversations, if that always triggers yet another search request, it adds quite some load, while sometimes people might know already where they need to go.

Just for the record, on our server we have:

  • ~1.8k requests for open conversations today with a search term coming from the left sidebar filtering (we should look into reducing those as well)
  • ~150 unified searches
  • ~30 searches in the current conversation

So if we "all of a sudden" add 1.8k additional search requests it should be sensible and optional from my pov

Copy link
Copy Markdown
Contributor Author

@Antreesy Antreesy Dec 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can also try to put all the factual search results behind a click (e.g. initially show only joined group and private conversations matching search term):

image

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Thinking …"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is quite problematic. A lot of people use the quick filtering to navigate to other conversations, if that always triggers yet another search request, it adds quite some load, while sometimes people might know already where they need to go.

Got it, exactly why I proposed the filter chips idea :) By default it can be unselected and if people select it we can trigger the global message search.
We should at some point consolidate the right sidebar message "in this conversation" message search and the global search too, and we can make the use of more filters. eg:

image

this.initializeNavigation()
},

Expand Down Expand Up @@ -890,6 +942,9 @@ export default {
if (this.cancelSearchListedConversations) {
this.cancelSearchListedConversations()
}
if (this.cancelSearchMessages) {
this.cancelSearchMessages()
}
},

showSettings() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@
-->

<script setup lang="ts">
import type { ParticipantSearchResult, Conversation as TypeConversation } from '../../../types/index.ts'
import type {
ParticipantSearchResult,
Conversation as TypeConversation,
UnifiedSearchResultEntryWithRouterLink,
} from '../../../types/index.ts'

import { loadState } from '@nextcloud/initial-state'
import { t } from '@nextcloud/l10n'
Expand All @@ -15,6 +19,7 @@ import NcListItem from '@nextcloud/vue/components/NcListItem'
import IconChatPlusOutline from 'vue-material-design-icons/ChatPlusOutline.vue'
import AvatarWrapper from '../../AvatarWrapper/AvatarWrapper.vue'
import ConversationIcon from '../../ConversationIcon.vue'
import SearchMessageItem from '../../RightSidebar/SearchMessages/SearchMessageItem.vue'
import NavigationHint from '../../UIShared/NavigationHint.vue'
import ConversationItem from '../ConversationsList/ConversationItem.vue'
import { ATTENDEE, AVATAR, CONVERSATION } from '../../../constants.ts'
Expand All @@ -28,6 +33,7 @@ const props = defineProps<{
contactsLoading: boolean
searchResultsListedConversations: TypeConversation[]
searchResults: ParticipantSearchResult[]
searchResultsMessages: UnifiedSearchResultEntryWithRouterLink[]
}>()

const emit = defineEmits<{
Expand Down Expand Up @@ -75,6 +81,7 @@ type VirtualListItem
= | { type: 'caption', id: string, name: string }
| { type: 'hint', id: string, hint: string }
| { type: 'conversation', id: number, object: TypeConversation }
| { type: 'message', id: number, object: UnifiedSearchResultEntryWithRouterLink }
| { type: 'open_conversation', id: number, object: TypeConversation }
| { type: 'action', id: string, name: string, subname: string }
| { type: 'user' | 'group' | 'circle' | 'federated', id: string, object: ParticipantSearchResult, icon: Record<string, unknown> }
Expand Down Expand Up @@ -113,6 +120,16 @@ const searchResultsVirtual = computed<VirtualListItem[]>(() => {
})
}

// Add messages section
virtualList.push({ type: 'caption', id: 'messages_caption', name: t('spreed', 'Messages') })
if (props.searchResultsMessages.length === 0) {
virtualList.push({ type: 'hint', id: 'hint_messages', hint: t('spreed', 'No matches found') })
} else {
props.searchResultsMessages.forEach((item: UnifiedSearchResultEntryWithRouterLink) => {
virtualList.push({ type: 'message', id: +item.attributes.messageId, object: item })
})
}

// Categorize search results into different sections
const subList = props.searchResults.reduce<SubListType>((acc, result) => {
if (result.source === ATTENDEE.ACTOR_TYPE.USERS) {
Expand Down Expand Up @@ -269,6 +286,19 @@ const iconSize = computed(() => isCompact.value ? AVATAR.SIZE.COMPACT : AVATAR.S
is-search-result
:compact="isCompact"
@click="emit('abort-search')" />
<SearchMessageItem
v-if="item.data.type === 'message'"
:ref="`message-${item.data.object.attributes.conversation}`"
:message-id="+item.data.object.attributes.messageId"
:title="isCompact ? item.data.object.subline : item.data.object.title"
:subline="item.data.object.subline"
:actor-id="item.data.object.attributes.actorId"
:actor-type="item.data.object.attributes.actorType"
:token="item.data.object.attributes.conversation"
:timestamp="+item.data.object.attributes.timestamp"
:to="item.data.object.to"
:compact="isCompact"
@click="emit('abort-search')" />
<NcAppNavigationCaption
v-else-if="item.data.type === 'caption'"
:name="item.data.name"
Expand Down
21 changes: 16 additions & 5 deletions src/components/RightSidebar/SearchMessages/SearchMessageItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import NcListItem from '@nextcloud/vue/components/NcListItem'
import CloseCircleOutline from 'vue-material-design-icons/CloseCircleOutline.vue'
import AvatarWrapper from '../../AvatarWrapper/AvatarWrapper.vue'
import ConversationIcon from '../../ConversationIcon.vue'
import { CONVERSATION } from '../../../constants.ts'
import { AVATAR, CONVERSATION } from '../../../constants.ts'
import { EventBus } from '../../../services/EventBus.ts'
import { useDashboardStore } from '../../../stores/dashboard.ts'
import { formatDateTime } from '../../../utils/formattedTime.ts'
Expand All @@ -34,6 +34,7 @@ const props = withDefaults(defineProps<{
timestamp: number
messageParameters?: ChatMessage['messageParameters']
isReminder?: boolean
compact?: boolean
}>(), {
messageParameters: () => ({}),
isReminder: false,
Expand Down Expand Up @@ -88,6 +89,8 @@ function handleResultClick() {
:to="to"
:active="active"
:title="richSubline"
:compact
class="search-message"
force-menu
@click="handleResultClick">
<template #icon>
Expand All @@ -96,14 +99,15 @@ function handleResultClick() {
:id="actorId"
:name="title"
:source="actorType"
:size="compact ? AVATAR.SIZE.COMPACT : AVATAR.SIZE.DEFAULT"
disable-menu
:token="token" />
<ConversationIcon
v-else
:item="conversation"
hide-user-status />
</template>
<template #subname>
<template v-if="!compact" #subname>
{{ richSubline }}
</template>
<template v-if="isReminder" #actions>
Expand All @@ -119,15 +123,22 @@ function handleResultClick() {
<template #details>
<NcDateTime
:timestamp="timestamp * 1000"
class="search-results__date"
class="search-message__date"
relative-time="short"
ignore-seconds />
</template>
</NcListItem>
</template>

<style lang="scss" scoped>
.search-results__date {
font-size: x-small;
.search-message {
&__date {
font-size: x-small;
}

/* Overwrite NcListItem styles for compact view */
:deep(.list-item--compact .list-item-content__name) {
font-weight: 400;
}
}
</style>
15 changes: 4 additions & 11 deletions src/components/RightSidebar/SearchMessages/SearchMessagesTab.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type {
SearchMessagePayload,
UnifiedSearchResponse,
UnifiedSearchResultEntry,
UnifiedSearchResultEntryWithRouterLink,
} from '../../../types/index.ts'

import { showError } from '@nextcloud/dialogs'
Expand All @@ -35,7 +36,7 @@ import { useArrowNavigation } from '../../../composables/useArrowNavigation.js'
import { useGetToken } from '../../../composables/useGetToken.ts'
import { useIsInCall } from '../../../composables/useIsInCall.js'
import { ATTENDEE } from '../../../constants.ts'
import { searchMessages } from '../../../services/coreService.ts'
import { searchMessagesInConversation } from '../../../services/coreService.ts'
import { EventBus } from '../../../services/EventBus.ts'
import CancelableRequest from '../../../utils/cancelableRequest.js'

Expand All @@ -51,15 +52,7 @@ const searchBox = ref<InstanceType<typeof SearchBox> | null>(null)
const { initializeNavigation, resetNavigation } = useArrowNavigation(searchMessagesTab, searchBox)

const isFocused = ref(false)
const searchResults = ref<(UnifiedSearchResultEntry & {
to: {
name: string
hash: string
params: {
token: string
}
}
})[]>([])
const searchResults = ref<UnifiedSearchResultEntryWithRouterLink[]>([])
const searchText = ref('')
const fromUser = ref<IUserData | undefined>(undefined)
const sinceDate = ref<Date | null>(null)
Expand Down Expand Up @@ -180,7 +173,7 @@ async function fetchSearchResults(isNew = true): Promise<void> {
cancelSearchFn()
resetNavigation()

const { request, cancel } = CancelableRequest(searchMessages) as SearchMessageCancelableRequest
const { request, cancel } = CancelableRequest(searchMessagesInConversation) as SearchMessageCancelableRequest
cancelSearchFn = cancel

if (isNew) {
Expand Down
17 changes: 15 additions & 2 deletions src/services/coreService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,19 @@ async function deleteTaskById(id: number, options?: AxiosRequestConfig): Promise
* @param params
* @param options
*/
async function searchMessages(params: SearchMessagePayload, options?: AxiosRequestConfig): UnifiedSearchResponse {
async function searchMessagesEverywhere(params: SearchMessagePayload, options?: AxiosRequestConfig): UnifiedSearchResponse {
return axios.get(generateOcsUrl('search/providers/talk-message/search'), {
...options,
params,
})
}

/**
*
* @param params
* @param options
*/
async function searchMessagesInConversation(params: SearchMessagePayload, options?: AxiosRequestConfig): UnifiedSearchResponse {
return axios.get(generateOcsUrl('search/providers/talk-message-current/search'), {
...options,
params,
Expand All @@ -116,5 +128,6 @@ export {
deleteTaskById,
getTaskById,
getUserProfile,
searchMessages,
searchMessagesEverywhere,
searchMessagesInConversation,
}
9 changes: 9 additions & 0 deletions src/types/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,15 @@ export type SearchMessagePayload = operationsCore['unified_search-search']['para
export type UnifiedSearchResultEntry = componentsCore['schemas']['UnifiedSearchResultEntry'] & {
attributes: MessageSearchResultAttributes
}
export type UnifiedSearchResultEntryWithRouterLink = UnifiedSearchResultEntry & {
to: {
name: string
hash: string
params: {
token: string
}
}
}
export type UnifiedSearchResponse = ApiResponse<operationsCore['unified_search-search']['responses'][200]['content']['application/json'] & {
ocs: {
meta: componentsCore['schemas']['OCSMeta']
Expand Down
1 change: 1 addition & 0 deletions src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -488,6 +488,7 @@ export type {
SearchMessagePayload,
UnifiedSearchResponse,
UnifiedSearchResultEntry,
UnifiedSearchResultEntryWithRouterLink,
} from './core.ts'

// Files API
Expand Down
Loading