Browse Source

Merge pull request #14125 from nextcloud/feat/11130/search-messages

feat: Add search message tab to the right sidebar
pull/14120/head
Dorra 10 months ago
committed by GitHub
parent
commit
151636982a
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 12
      src/components/MessagesList/MessagesList.vue
  2. 213
      src/components/RightSidebar/RightSidebar.vue
  3. 439
      src/components/RightSidebar/SearchMessages/SearchMessagesTab.vue
  4. 10
      src/services/coreService.ts
  5. 46
      src/types/index.ts

12
src/components/MessagesList/MessagesList.vue

@ -1158,10 +1158,14 @@ export default {
// TODO: doesn't work if chat is hidden. Need to store
// delayed 'shouldScroll' and call after chat is visible
scrollElement.scrollIntoView({
behavior: smooth ? 'smooth' : 'auto',
block: 'center',
inline: 'nearest',
// FIXME: because scrollToBottom is also triggered and it is wrapped in $nextTick
// We need to trigger this at the same time (nextTick) to avoid focusing and then scrolling to bottom
this.$nextTick(() => {
scrollElement.scrollIntoView({
behavior: smooth ? 'smooth' : 'auto',
block: 'center',
inline: 'nearest',
})
})
if (this.$refs.scroller && !smooth) {

213
src/components/RightSidebar/RightSidebar.vue

@ -6,8 +6,8 @@
<template>
<NcAppSidebar v-if="isSidebarAvailable"
:open="opened"
:name="conversation.displayName"
:title="conversation.displayName"
:name="sidebarTitle"
:title="sidebarTitle"
:active.sync="activeTab"
:class="'active-tab-' + activeTab"
:toggle-classes="{ 'chat-button-sidebar-toggle': isInCall }"
@ -17,97 +17,128 @@
@closed="handleClosed">
<!-- Use a custom icon when sidebar is used for chat messages during the call -->
<template v-if="isInCall" #toggle-icon>
<MessageText :size="20" />
<IconMessageText :size="20" />
<span v-if="unreadMessagesCounter > 0" class="chat-button-unread-marker" />
</template>
<!-- search in messages button-->
<template v-if="!showSearchMessagesTab && getUserId" #secondary-actions>
<NcActionButton type="tertiary"
:title="t('spreed', 'Search messages')"
@click="showSearchMessagesTab = true">
<template #icon>
<IconMagnify :size="20" />
</template>
</NcActionButton>
</template>
<template v-else-if="getUserId" #tertiary-actions>
<NcButton type="tertiary"
:title="t('spreed', 'Back')"
@click="showSearchMessagesTab = false">
<template #icon>
<IconArrowLeft :size="20" />
</template>
</NcButton>
</template>
<template #description>
<InternalSignalingHint />
<LobbyStatus v-if="canFullModerate && hasLobbyEnabled" :token="token" />
</template>
<NcAppSidebarTab v-if="isInCall"
id="chat"
key="chat"
:order="1"
:name="t('spreed', 'Chat')">
<template #icon>
<Message :size="20" />
</template>
<ChatView :is-visible="opened" is-sidebar />
</NcAppSidebarTab>
<NcAppSidebarTab v-if="showParticipantsTab"
id="participants"
key="participants"
ref="participantsTab"
:order="2"
:name="participantsText">
<template #icon>
<AccountMultiple :size="20" />
</template>
<ParticipantsTab :is-active="activeTab === 'participants'"
:can-search="canSearchParticipants"
:can-add="canAddParticipants" />
<NcAppSidebarTab v-if="showSearchMessagesTab"
id="search-messages"
key="search-messages"
:order="0"
:name="t('spreed', 'Search messages')">
<SearchMessagesTab @close="showSearchMessagesTab = false" />
</NcAppSidebarTab>
<NcAppSidebarTab v-if="showBreakoutRoomsTab"
id="breakout-rooms"
key="breakout-rooms"
ref="breakout-rooms"
:order="3"
:name="breakoutRoomsText">
<template #icon>
<DotsCircle :size="20" />
</template>
<BreakoutRoomsTab :main-token="mainConversationToken"
:main-conversation="mainConversation"
:is-active="activeTab === 'breakout-rooms'" />
</NcAppSidebarTab>
<NcAppSidebarTab v-if="showDetailsTab"
id="details-tab"
key="details-tab"
:order="4"
:name="t('spreed', 'Details')">
<template #icon>
<InformationOutline :size="20" />
</template>
<SetGuestUsername v-if="!getUserId" />
<SipSettings v-if="showSIPSettings" :conversation="conversation" />
<div v-if="!getUserId" id="app-settings">
<div id="app-settings-header">
<NcButton type="tertiary" @click="showSettings">
<template #icon>
<CogIcon :size="20" />
</template>
{{ t('spreed', 'Settings') }}
</NcButton>
<template v-else>
<NcAppSidebarTab v-if="isInCall"
id="chat"
key="chat"
:order="1"
:name="t('spreed', 'Chat')">
<template #icon>
<IconMessage :size="20" />
</template>
<ChatView :is-visible="opened" is-sidebar />
</NcAppSidebarTab>
<NcAppSidebarTab v-if="showParticipantsTab"
id="participants"
key="participants"
ref="participantsTab"
:order="2"
:name="participantsText">
<template #icon>
<IconAccountMultiple :size="20" />
</template>
<ParticipantsTab :is-active="activeTab === 'participants'"
:can-search="canSearchParticipants"
:can-add="canAddParticipants" />
</NcAppSidebarTab>
<NcAppSidebarTab v-if="showBreakoutRoomsTab"
id="breakout-rooms"
key="breakout-rooms"
ref="breakout-rooms"
:order="3"
:name="breakoutRoomsText">
<template #icon>
<IconDotsCircle :size="20" />
</template>
<BreakoutRoomsTab :main-token="mainConversationToken"
:main-conversation="mainConversation"
:is-active="activeTab === 'breakout-rooms'" />
</NcAppSidebarTab>
<NcAppSidebarTab v-if="showDetailsTab"
id="details-tab"
key="details-tab"
:order="4"
:name="t('spreed', 'Details')">
<template #icon>
<IconInformationOutline :size="20" />
</template>
<SetGuestUsername v-if="!getUserId" />
<SipSettings v-if="showSIPSettings" :conversation="conversation" />
<div v-if="!getUserId" id="app-settings">
<div id="app-settings-header">
<NcButton type="tertiary" @click="showSettings">
<template #icon>
<IconCog :size="20" />
</template>
{{ t('spreed', 'Settings') }}
</NcButton>
</div>
</div>
</div>
</NcAppSidebarTab>
<NcAppSidebarTab v-if="showSharedItemsTab"
id="shared-items"
key="shared-items"
ref="sharedItemsTab"
:order="5"
:name="t('spreed', 'Shared items')">
<template #icon>
<FolderMultipleImage :size="20" />
</template>
<SharedItemsTab :active="activeTab === 'shared-items'" />
</NcAppSidebarTab>
</NcAppSidebarTab>
<NcAppSidebarTab v-if="showSharedItemsTab"
id="shared-items"
key="shared-items"
ref="sharedItemsTab"
:order="5"
:name="t('spreed', 'Shared items')">
<template #icon>
<IconFolderMultipleImage :size="20" />
</template>
<SharedItemsTab :active="activeTab === 'shared-items'" />
</NcAppSidebarTab>
</template>
</NcAppSidebar>
</template>
<script>
import AccountMultiple from 'vue-material-design-icons/AccountMultiple.vue'
import CogIcon from 'vue-material-design-icons/Cog.vue'
import DotsCircle from 'vue-material-design-icons/DotsCircle.vue'
import FolderMultipleImage from 'vue-material-design-icons/FolderMultipleImage.vue'
import InformationOutline from 'vue-material-design-icons/InformationOutline.vue'
import Message from 'vue-material-design-icons/Message.vue'
import MessageText from 'vue-material-design-icons/MessageText.vue'
import IconAccountMultiple from 'vue-material-design-icons/AccountMultiple.vue'
import IconArrowLeft from 'vue-material-design-icons/ArrowLeft.vue'
import IconCog from 'vue-material-design-icons/Cog.vue'
import IconDotsCircle from 'vue-material-design-icons/DotsCircle.vue'
import IconFolderMultipleImage from 'vue-material-design-icons/FolderMultipleImage.vue'
import IconInformationOutline from 'vue-material-design-icons/InformationOutline.vue'
import IconMagnify from 'vue-material-design-icons/Magnify.vue'
import IconMessage from 'vue-material-design-icons/Message.vue'
import IconMessageText from 'vue-material-design-icons/MessageText.vue'
import { showMessage } from '@nextcloud/dialogs'
import { emit, subscribe, unsubscribe } from '@nextcloud/event-bus'
import { t } from '@nextcloud/l10n'
import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton.js'
import NcAppSidebar from '@nextcloud/vue/dist/Components/NcAppSidebar.js'
import NcAppSidebarTab from '@nextcloud/vue/dist/Components/NcAppSidebarTab.js'
import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
@ -116,13 +147,13 @@ import BreakoutRoomsTab from './BreakoutRooms/BreakoutRoomsTab.vue'
import InternalSignalingHint from './InternalSignalingHint.vue'
import LobbyStatus from './LobbyStatus.vue'
import ParticipantsTab from './Participants/ParticipantsTab.vue'
import SearchMessagesTab from './SearchMessages/SearchMessagesTab.vue'
import SharedItemsTab from './SharedItems/SharedItemsTab.vue'
import SipSettings from './SipSettings.vue'
import ChatView from '../ChatView.vue'
import SetGuestUsername from '../SetGuestUsername.vue'
import { CONVERSATION, WEBINAR, PARTICIPANT } from '../../constants.js'
import BrowserStorage from '../../services/BrowserStorage.js'
import { hasTalkFeature } from '../../services/CapabilitiesManager.ts'
import { useSidebarStore } from '../../stores/sidebar.js'
@ -133,21 +164,25 @@ export default {
ChatView,
InternalSignalingHint,
LobbyStatus,
NcActionButton,
NcAppSidebar,
NcAppSidebarTab,
NcButton,
ParticipantsTab,
SearchMessagesTab,
SetGuestUsername,
SharedItemsTab,
SipSettings,
// Icons
AccountMultiple,
CogIcon,
DotsCircle,
FolderMultipleImage,
InformationOutline,
Message,
MessageText,
IconAccountMultiple,
IconArrowLeft,
IconCog,
IconDotsCircle,
IconFolderMultipleImage,
IconInformationOutline,
IconMagnify,
IconMessage,
IconMessageText,
},
props: {
@ -168,6 +203,7 @@ export default {
activeTab: 'participants',
contactsLoading: false,
unreadNotificationHandle: null,
showSearchMessagesTab: false,
}
},
@ -296,6 +332,15 @@ export default {
title: t('spreed', 'Open chat')
}
},
sidebarTitle() {
return this.showSearchMessagesTab
? t('spreed', 'Search in {name}', { name: this.conversation.displayName }, undefined, {
escape: false,
sanitize: false,
})
: this.conversation.displayName
}
},
watch: {

439
src/components/RightSidebar/SearchMessages/SearchMessagesTab.vue

@ -0,0 +1,439 @@
<!--
- SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
- SPDX-License-Identifier: AGPL-3.0-or-later
-->
<script setup lang="ts">
import debounce from 'debounce'
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import type { Route } from 'vue-router'
import IconCalendarRange from 'vue-material-design-icons/CalendarRange.vue'
import IconFilter from 'vue-material-design-icons/Filter.vue'
import IconMessageOutline from 'vue-material-design-icons/MessageOutline.vue'
import { showError } from '@nextcloud/dialogs'
import { t } from '@nextcloud/l10n'
import NcAvatar from '@nextcloud/vue/dist/Components/NcAvatar.js'
import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
import NcChip from '@nextcloud/vue/dist/Components/NcChip.js'
import NcDateTime from '@nextcloud/vue/dist/Components/NcDateTime.js'
import NcDateTimePickerNative from '@nextcloud/vue/dist/Components/NcDateTimePickerNative.js'
import NcEmptyContent from '@nextcloud/vue/dist/Components/NcEmptyContent.js'
import NcListItem from '@nextcloud/vue/dist/Components/NcListItem.js'
import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon.js'
import NcSelect from '@nextcloud/vue/dist/Components/NcSelect.js'
import AvatarWrapper from '../../AvatarWrapper/AvatarWrapper.vue'
import SearchBox from '../../UIShared/SearchBox.vue'
import TransitionWrapper from '../../UIShared/TransitionWrapper.vue'
import { useIsInCall } from '../../../composables/useIsInCall.js'
import { useStore } from '../../../composables/useStore.js'
import { ATTENDEE } from '../../../constants.js'
import { searchMessages } from '../../../services/coreService.ts'
import { EventBus } from '../../../services/EventBus.ts'
import {
CoreUnifiedSearchResultEntry,
UserFilterObject,
SearchMessagePayload,
UnifiedSearchResponse,
} from '../../../types/index.ts'
import CancelableRequest from '../../../utils/cancelableRequest.js'
const emit = defineEmits<{
(event: 'close'): void
}>()
const searchBox = ref(null)
const isFocused = ref(false)
const searchResults = ref<(CoreUnifiedSearchResultEntry &
{
to: {
name: string;
hash: string;
params: {
token: string;
skipLeaveWarning: boolean;
};
}
})[]>([])
const searchText = ref('')
const fromUser = ref<UserFilterObject | null>(null)
const sinceDate = ref<Date | null>(null)
const untilDate = ref<Date | null>(null)
const searchLimit = ref(10)
const searchCursor = ref<number | string | null>(0)
const searchDetailsOpened = ref(false)
const isFetchingResults = ref(false)
const isSearchExhausted = ref(false)
const store = useStore()
const isInCall = useIsInCall()
const token = computed(() => store.getters.getToken())
const participantsInitialised = computed(() => store.getters.participantsInitialised(token.value))
const participants = computed<UserFilterObject>(() => {
return store.getters.participantsList(token.value)
.filter(({ actorType }) => actorType === ATTENDEE.ACTOR_TYPE.USERS) // FIXME: federated users are not supported by the search provider
.map(({ actorId, displayName, actorType }: { actorId: string; displayName: string; actorType: string}) => ({
id: actorId,
displayName,
isNoUser: actorType !== 'users',
user: actorId,
disableMenu: true,
showUserStatus: false,
}))
})
const canLoadMore = computed(() => !isSearchExhausted.value && !isFetchingResults.value && searchCursor.value !== 0)
const hasFilter = computed(() => fromUser.value || sinceDate.value || untilDate.value)
onMounted(() => {
EventBus.on('route-change', onRouteChange)
})
onBeforeUnmount(() => {
EventBus.off('route-change', onRouteChange)
abortSearch()
})
const onRouteChange = ({ from, to }: { from: Route, to: Route }): void => {
if (to.name !== 'conversation' || from.params.token !== to.params.token || (to.hash && isInCall.value)) {
abortSearch()
emit('close')
}
}
watch(searchText, (value) => {
if (value.trim().length === 0) {
searchResults.value = []
searchCursor.value = 0
isSearchExhausted.value = false
}
})
/**
* Cancel search and cleanup the search fields and results.
*/
function abortSearch() {
cancelSearchFn()
searchText.value = ''
fromUser.value = null
sinceDate.value = null
untilDate.value = null
searchDetailsOpened.value = false
searchResults.value = []
searchCursor.value = 0
}
/**
* Continue fetching more search results
*/
function loadMore() {
return fetchSearchResults(false)
}
/**
*
*/
function fetchNewSearchResult() {
return fetchSearchResults(true)
}
let cancelSearchFn = () => {}
type SearchMessageCancelableRequest = {
request: (payload: SearchMessagePayload) => UnifiedSearchResponse,
cancel: () => void,
}
/**
* @param [isNew=true] Is it a new search (search parameters changed)?
* Fetch the search results from the server
*/
async function fetchSearchResults(isNew = true) {
const term = searchText.value.trim()
// Don't search if the search text is empty
if (term.length === 0) {
return
}
isFetchingResults.value = true
try {
// cancel the previous search request
cancelSearchFn()
const { request, cancel } = CancelableRequest(searchMessages) as SearchMessageCancelableRequest
cancelSearchFn = cancel
if (isNew) {
searchCursor.value = 0
}
if (searchCursor.value === 0) {
searchResults.value = []
}
if (term.length === 0 && !fromUser.value && !sinceDate.value && !untilDate.value) {
return
}
const response = await request({
term,
person: fromUser.value?.id,
since: !isNaN(sinceDate.value) ? sinceDate.value?.toISOString() : null,
until: !isNaN(untilDate.value) ? untilDate.value?.toISOString() : null,
limit: searchLimit.value,
cursor: searchCursor.value || null,
from: `/call/${token.value}`,
})
const data = response?.data?.ocs?.data
if (data?.entries.length > 0) {
let entries = data?.entries
isSearchExhausted.value = entries.length < searchLimit.value
searchCursor.value = data.cursor
// FIXME: remove the filter after the person filter is fixed on the server
if (fromUser.value) {
entries = entries.filter((entry) => entry.attributes.actorId === fromUser.value?.id)
if (entries.length === 0 && !isSearchExhausted.value) {
return await loadMore()
}
}
searchResults.value = searchResults.value.concat(entries.map((entry : CoreUnifiedSearchResultEntry) => {
return {
...entry,
to: {
name: 'conversation',
hash: `#message_${entry.attributes.messageId}`,
params: {
token: entry.attributes.conversation,
skipLeaveWarning: true
}
}
}
})
)
}
} catch (exception) {
if (CancelableRequest.isCancel(exception)) {
return
}
console.error('Error searching for messages', exception)
showError(t('spreed', 'An error occurred while performing the search'))
} finally {
isFetchingResults.value = false
}
}
const debounceFetchSearchResults = debounce(fetchNewSearchResult, 250)
</script>
<template>
<div class="search-messages-tab">
<div class="search-form">
<div class="search-form__main">
<div class="search-form__search-box-wrapper">
<SearchBox ref="searchBox"
:value.sync="searchText"
:placeholder-text="t('spreed', 'Search messages …')"
:is-focused.sync="isFocused"
@input="debounceFetchSearchResults" />
<NcButton :pressed.sync="searchDetailsOpened"
:aria-label="t('spreed', 'Search options')"
:title="t('spreed', 'Search options')"
type="tertiary-no-background">
<template #icon>
<IconFilter :size="15" />
</template>
</NcButton>
</div>
<TransitionWrapper name="radial-reveal">
<div v-show="searchDetailsOpened" class="search-form__search-detail">
<NcSelect v-model="fromUser"
class="search-form__search-detail__from-user"
:aria-label-combobox="t('spreed', 'From User')"
:placeholder="t('spreed', 'From User')"
user-select
:loading="!participantsInitialised"
:options="participants"
@update:modelValue="debounceFetchSearchResults" />
<div class="search-form__search-detail__date-picker-wrapper">
<NcDateTimePickerNative id="search-form__search-detail__date-picker--since"
v-model="sinceDate"
class="search-form__search-detail__date-picker"
format="YYYY-MM-DD"
type="date"
:step="1"
:max="new Date()"
:aria-label="t('spreed', 'Since')"
:label="t('spreed', 'Since')"
@update:modelValue="debounceFetchSearchResults" />
<NcDateTimePickerNative id="search-form__search-detail__date-picker--until"
v-model="untilDate"
class="search-form__search-detail__date-picker"
format="YYYY-MM-DD"
type="date"
:max="new Date()"
:aria-label="t('spreed', 'Until')"
:label="t('spreed', 'Until')"
:minute-step="1"
@update:modelValue="debounceFetchSearchResults" />
</div>
</div>
</TransitionWrapper>
<TransitionWrapper name="fade">
<div v-show="hasFilter && !searchDetailsOpened"
class="search-form__search-bubbles">
<NcChip v-if="fromUser"
type="tertiary"
:text="fromUser.displayName"
@close="fromUser = null">
<template #icon>
<NcAvatar :size="24"
:user="fromUser.id"
:display-name="fromUser.displayName"
:show-user-status="false" />
</template>
</NcChip>
<NcChip v-if="sinceDate"
type="tertiary"
:text="t('spreed', 'Since') + ' ' + sinceDate?.toLocaleDateString()"
@close="sinceDate = null">
<template #icon>
<IconCalendarRange :size="15" />
</template>
</NcChip>
<NcChip v-if="untilDate"
type="tertiary"
:text="t('spreed', 'Until') + ' ' + untilDate?.toLocaleDateString()"
@close="untilDate = null">
<template #icon>
<IconCalendarRange :size="15" />
</template>
</NcChip>
</div>
</TransitionWrapper>
</div>
</div>
<div class="search-results">
<template v-if="searchResults.length !== 0">
<NcListItem v-for="item of searchResults"
:key="`message_${item.attributes.messageId}`"
:data-nav-id="`message_${item.attributes.messageId}`"
:name="item.title"
:to="item.to"
:v-tooltip="item.subline">
<template #icon>
<AvatarWrapper :id="item.attributes.actorId"
:name="item.title"
:source="item.attributes.actorType"
:disable-menu="true"
:token="item.attributes.conversation" />
</template>
<template #subname>
{{ item.subline }}
</template>
<template #details>
<NcDateTime :timestamp="parseInt(item.attributes.timestamp) * 1000"
class="search-results__date"
relative-time="narrow"
ignore-seconds />
</template>
</NcListItem>
</template>
<NcEmptyContent v-else-if="!isFetchingResults && searchText.trim().length !== 0"
class="search-results__empty"
:name="t('spreed', 'No results found')">
<template #icon>
<IconMessageOutline :size="64" />
</template>
</NcEmptyContent>
<template v-if="canLoadMore">
<NcButton wide type="tertiary" @click="fetchSearchResults(false)">
{{ t('spreed', 'Load more results') }}
</NcButton>
</template>
<template v-if="isFetchingResults">
<NcLoadingIcon class="search-results__loading" />
</template>
</div>
</div>
</template>
<style lang="scss" scoped>
.search-messages-tab {
display: flex;
flex-direction: column;
height: 100%;
}
.search-form {
display: flex;
flex-direction: column;
padding-bottom: var(--default-grid-baseline);
&__search-box-wrapper {
display: flex;
gap: var(--default-grid-baseline);
}
&__main {
display: flex;
flex-direction: column;
flex: 1;
}
&__search-detail {
display: flex;
flex-direction: column;
width: 100%;
margin-top: calc(var(--default-grid-baseline) * 4);
&__from-user {
margin-top: 8px;
:deep(.vs__dropdown-toggle) {
overflow-y: clip;
}
}
&__date-picker {
width: 100%;
min-width: 100px;
&-wrapper {
display: flex;
gap: var(--default-grid-baseline);
}
}
}
&__search-bubbles {
display: flex;
flex-wrap: wrap;
gap: var(--default-grid-baseline);
margin-top: var(--default-grid-baseline);
}
}
.search-results {
transition: all 0.15s ease;
height: 100%;
overflow-y: auto;
&__date {
font-size: x-small;
}
&__loading {
height: var(--default-clickable-area);
}
&__empty {
height: 100%;
}
}
</style>

10
src/services/coreService.ts

@ -10,6 +10,8 @@ import { getTalkConfig, hasTalkFeature } from './CapabilitiesManager.ts'
import { SHARE } from '../constants.js'
import type {
TaskProcessingResponse,
UnifiedSearchResponse,
SearchMessagePayload,
} from '../types/index.ts'
const canInviteToFederation = hasTalkFeature('local', 'federation-v1')
@ -63,8 +65,16 @@ const deleteTaskById = async function(id: number, options?: object): Promise<nul
return axios.delete(generateOcsUrl('taskprocessing/task/{id}', { id }), options)
}
const searchMessages = async function(params: SearchMessagePayload, options: object): UnifiedSearchResponse {
return axios.get(generateOcsUrl('search/providers/talk-message-current/search'), {
...options,
params,
})
}
export {
autocompleteQuery,
getTaskById,
deleteTaskById,
searchMessages,
}

46
src/types/index.ts

@ -312,3 +312,49 @@ export type setSipSettingsParams = Required<operations['settings-setsip-settings
export type setSipSettingsResponse = ApiResponse<operations['settings-setsip-settings']['responses'][200]['content']['application/json']>
export type setUserSettingsParams = Required<operations['settings-set-user-setting']>['requestBody']['content']['application/json']
export type setUserSettingsResponse = ApiResponse<operations['settings-set-user-setting']['responses'][200]['content']['application/json']>
// Unified Search
export type MessageSearchResultAttributes = {
conversation: string
messageId: string
actorType: string
actorId: string
timestamp: string
}
export type CoreUnifiedSearchResultEntry = {
thumbnailUrl: string;
title: string;
subline: string;
resourceUrl: string;
icon: string;
rounded: boolean;
attributes: MessageSearchResultAttributes;
}
export type UserFilterObject = {
id: string
displayName: string
isNoUser: boolean
user: string
disableMenu: boolean
showUserStatus: boolean
}
export type CoreUnifiedSearchResult = {
name: string;
isPaginated: boolean;
entries: CoreUnifiedSearchResultEntry[];
cursor: number | string | null;
}
export type UnifiedSearchResponse = ApiResponseUnwrapped<CoreUnifiedSearchResult>
export type SearchMessagePayload = {
term: string,
person?: string,
since?: string | null,
until?: string | null,
cursor?: number | string | null,
limit?: number,
from?: string
}
Loading…
Cancel
Save