From 1ff1841db1ac30a6222db024a9934b7bd5ede971 Mon Sep 17 00:00:00 2001 From: Maksim Sukharev Date: Tue, 5 Nov 2024 11:00:42 +0100 Subject: [PATCH 1/3] fix: move autocompleteQuery to coreService Signed-off-by: Maksim Sukharev --- .../LeftSidebar/LeftSidebar.spec.js | 9 ++- src/components/LeftSidebar/LeftSidebar.vue | 4 +- .../NewConversationContactsPage.vue | 4 +- .../Participants/ParticipantsTab.vue | 4 +- src/services/CapabilitiesManager.ts | 6 +- .../coreService.spec.js} | 22 ++++---- src/services/conversationsService.js | 40 +------------ src/services/coreService.ts | 56 +++++++++++++++++++ 8 files changed, 85 insertions(+), 60 deletions(-) rename src/services/{conversationsService.spec.js => __tests__/coreService.spec.js} (73%) create mode 100644 src/services/coreService.ts diff --git a/src/components/LeftSidebar/LeftSidebar.spec.js b/src/components/LeftSidebar/LeftSidebar.spec.js index 3c30c759f91..c1e5c385d0c 100644 --- a/src/components/LeftSidebar/LeftSidebar.spec.js +++ b/src/components/LeftSidebar/LeftSidebar.spec.js @@ -15,16 +15,19 @@ import { loadState } from '@nextcloud/initial-state' import LeftSidebar from './LeftSidebar.vue' import router from '../../__mocks__/router.js' -import { searchPossibleConversations, searchListedConversations } from '../../services/conversationsService.js' +import { searchListedConversations } from '../../services/conversationsService.js' +import { autocompleteQuery } from '../../services/coreService.ts' import { EventBus } from '../../services/EventBus.ts' import storeConfig from '../../store/storeConfig.js' import { findNcListItems, findNcActionButton, findNcButton } from '../../test-helpers.js' import { requestTabLeadership } from '../../utils/requestTabLeadership.js' jest.mock('../../services/conversationsService', () => ({ - searchPossibleConversations: jest.fn(), searchListedConversations: jest.fn(), })) +jest.mock('../../services/coreService', () => ({ + autocompleteQuery: jest.fn(), +})) // short-circuit debounce jest.mock('debounce', () => jest.fn().mockImplementation(fn => fn)) @@ -296,7 +299,7 @@ describe('LeftSidebar.vue', () => { * @param {object} loadStateSettingsOverride Allows to override some properties */ async function testSearch(searchTerm, possibleResults, listedResults, loadStateSettingsOverride) { - searchPossibleConversations.mockResolvedValue({ + autocompleteQuery.mockResolvedValue({ data: { ocs: { data: possibleResults, diff --git a/src/components/LeftSidebar/LeftSidebar.vue b/src/components/LeftSidebar/LeftSidebar.vue index 81304c498b4..a333d6bb995 100644 --- a/src/components/LeftSidebar/LeftSidebar.vue +++ b/src/components/LeftSidebar/LeftSidebar.vue @@ -364,9 +364,9 @@ import { getTalkConfig, hasTalkFeature } from '../../services/CapabilitiesManage import { createPrivateConversation, fetchNoteToSelfConversation, - searchPossibleConversations, searchListedConversations, } from '../../services/conversationsService.js' +import { autocompleteQuery } from '../../services/coreService.ts' import { EventBus } from '../../services/EventBus.ts' import { talkBroadcastChannel } from '../../services/talkBroadcastChannel.js' import { useFederationStore } from '../../stores/federation.ts' @@ -737,7 +737,7 @@ export default { try { // FIXME: move to conversationsStore this.cancelSearchPossibleConversations('canceled') - const { request, cancel } = CancelableRequest(searchPossibleConversations) + const { request, cancel } = CancelableRequest(autocompleteQuery) this.cancelSearchPossibleConversations = cancel const response = await request({ diff --git a/src/components/NewConversationDialog/NewConversationContactsPage.vue b/src/components/NewConversationDialog/NewConversationContactsPage.vue index 339c8ab1cfb..c48b661b158 100644 --- a/src/components/NewConversationDialog/NewConversationContactsPage.vue +++ b/src/components/NewConversationDialog/NewConversationContactsPage.vue @@ -76,7 +76,7 @@ import DialpadPanel from '../UIShared/DialpadPanel.vue' import TransitionWrapper from '../UIShared/TransitionWrapper.vue' import { useArrowNavigation } from '../../composables/useArrowNavigation.js' -import { searchPossibleConversations } from '../../services/conversationsService.js' +import { autocompleteQuery } from '../../services/coreService.ts' import CancelableRequest from '../../utils/cancelableRequest.js' export default { @@ -205,7 +205,7 @@ export default { this.contactsLoading = true try { this.cancelSearchPossibleConversations('canceled') - const { request, cancel } = CancelableRequest(searchPossibleConversations) + const { request, cancel } = CancelableRequest(autocompleteQuery) this.cancelSearchPossibleConversations = cancel const response = await request({ searchText: this.searchText }) diff --git a/src/components/RightSidebar/Participants/ParticipantsTab.vue b/src/components/RightSidebar/Participants/ParticipantsTab.vue index ce866f90b5d..dd6ca179178 100644 --- a/src/components/RightSidebar/Participants/ParticipantsTab.vue +++ b/src/components/RightSidebar/Participants/ParticipantsTab.vue @@ -86,7 +86,7 @@ import { useIsInCall } from '../../../composables/useIsInCall.js' import { useSortParticipants } from '../../../composables/useSortParticipants.js' import { ATTENDEE } from '../../../constants.js' import { getTalkConfig, hasTalkFeature } from '../../../services/CapabilitiesManager.ts' -import { searchPossibleConversations } from '../../../services/conversationsService.js' +import { autocompleteQuery } from '../../../services/coreService.ts' import { EventBus } from '../../../services/EventBus.ts' import { addParticipant } from '../../../services/participantsService.js' import { useSidebarStore } from '../../../stores/sidebar.js' @@ -279,7 +279,7 @@ export default { this.resetNavigation() try { this.cancelSearchPossibleConversations('canceled') - const { request, cancel } = CancelableRequest(searchPossibleConversations) + const { request, cancel } = CancelableRequest(autocompleteQuery) this.cancelSearchPossibleConversations = cancel const response = await request({ diff --git a/src/services/CapabilitiesManager.ts b/src/services/CapabilitiesManager.ts index 660c6fe9e06..dc17a79e587 100644 --- a/src/services/CapabilitiesManager.ts +++ b/src/services/CapabilitiesManager.ts @@ -72,14 +72,18 @@ export function hasTalkFeature(token: string = 'local', feature: string): boolea * @param key1 top-level key (e.g. 'attachments') * @param key2 second-level key (e.g. 'allowed') */ -export function getTalkConfig(token: string = 'local', key1: keyof Config, key2: keyof Config[keyof Config]) { +export function getTalkConfig(token: string = 'local', key1: keyof Config, key2: string) { const remoteCapabilities = getRemoteCapability(token) + const locals = localCapabilities?.spreed?.config?.[key1] if (localCapabilities?.spreed?.['config-local']?.[key1]?.includes(key2)) { + // @ts-expect-error Vue: Element implicitly has an any type because expression of type string can't be used to index type return localCapabilities?.spreed?.config?.[key1]?.[key2] } else if (token === 'local' || !remoteCapabilities) { + // @ts-expect-error Vue: Element implicitly has an any type because expression of type string can't be used to index type return localCapabilities?.spreed?.config?.[key1]?.[key2] } else { // TODO discuss handling remote config (respect remote only / both / minimal) + // @ts-expect-error Vue: Element implicitly has an any type because expression of type string can't be used to index type return remoteCapabilities?.spreed?.config?.[key1]?.[key2] } } diff --git a/src/services/conversationsService.spec.js b/src/services/__tests__/coreService.spec.js similarity index 73% rename from src/services/conversationsService.spec.js rename to src/services/__tests__/coreService.spec.js index 726c47d55ea..14924750614 100644 --- a/src/services/conversationsService.spec.js +++ b/src/services/__tests__/coreService.spec.js @@ -5,8 +5,8 @@ import axios from '@nextcloud/axios' import { generateOcsUrl } from '@nextcloud/router' -import { searchPossibleConversations } from './conversationsService.js' -import { SHARE } from '../constants.js' +import { SHARE } from '../../constants.js' +import { autocompleteQuery } from '../coreService.ts' jest.mock('@nextcloud/axios', () => ({ get: jest.fn(), @@ -24,7 +24,7 @@ jest.mock('@nextcloud/capabilities', () => ({ })) })) -describe('conversationsService', () => { +describe('coreService', () => { afterEach(() => { // cleaning up the mess left behind the previous test jest.clearAllMocks() @@ -35,8 +35,8 @@ describe('conversationsService', () => { * @param {boolean} onlyUsers Whether or not to only search for users * @param {Array} expectedShareTypes The expected search types to look for */ - function testSearchPossibleConversations(token, onlyUsers, expectedShareTypes) { - searchPossibleConversations( + function testAutocompleteQuery(token, onlyUsers, expectedShareTypes) { + autocompleteQuery( { searchText: 'search-text', token, @@ -60,8 +60,8 @@ describe('conversationsService', () => { ) } - test('searchPossibleConversations with only users', () => { - testSearchPossibleConversations( + test('autocompleteQuery with only users', () => { + testAutocompleteQuery( 'conversation-token', true, [ @@ -70,8 +70,8 @@ describe('conversationsService', () => { ) }) - test('searchPossibleConversations with other share types', () => { - testSearchPossibleConversations( + test('autocompleteQuery with other share types', () => { + testAutocompleteQuery( 'conversation-token', false, [ @@ -84,8 +84,8 @@ describe('conversationsService', () => { ) }) - test('searchPossibleConversations with other share types and a new token', () => { - testSearchPossibleConversations( + test('autocompleteQuery with other share types and a new token', () => { + testAutocompleteQuery( 'new', false, [ diff --git a/src/services/conversationsService.js b/src/services/conversationsService.js index deab17e3927..4b344faa95f 100644 --- a/src/services/conversationsService.js +++ b/src/services/conversationsService.js @@ -6,12 +6,7 @@ import axios from '@nextcloud/axios' import { generateOcsUrl } from '@nextcloud/router' -import { getTalkConfig, hasTalkFeature } from './CapabilitiesManager.ts' -import { ATTENDEE, CONVERSATION, SHARE } from '../constants.js' - -const canInviteToFederation = hasTalkFeature('local', 'federation-v1') - && getTalkConfig('local', 'federation', 'enabled') - && getTalkConfig('local', 'federation', 'outgoing-enabled') +import { ATTENDEE, CONVERSATION } from '../constants.js' /** * Fetches the conversations from the server. @@ -70,38 +65,6 @@ const fetchNoteToSelfConversation = async function() { return axios.get(generateOcsUrl('apps/spreed/api/v4/room/note-to-self')) } -/** - * Fetch possible conversations - * - * @param {object} data the wrapping object; - * @param {string} data.searchText The string that will be used in the search query. - * @param {string} [data.token] The token of the conversation (if any), or "new" for a new one - * @param {boolean} [data.onlyUsers] Only return users - * @param {object} options options - */ -const searchPossibleConversations = async function({ searchText, token, onlyUsers }, options) { - token = token || 'new' - onlyUsers = !!onlyUsers - - const shareTypes = [ - SHARE.TYPE.USER, - !onlyUsers ? SHARE.TYPE.GROUP : null, - !onlyUsers ? SHARE.TYPE.CIRCLE : null, - (!onlyUsers && token !== 'new') ? SHARE.TYPE.EMAIL : null, - (!onlyUsers && canInviteToFederation) ? SHARE.TYPE.REMOTE : null, - ].filter(type => type !== null) - - return axios.get(generateOcsUrl('core/autocomplete/get'), { - ...options, - params: { - search: searchText, - itemType: 'call', - itemId: token, - shareTypes, - }, - }) -} - /** * Create a new one to one conversation with the specified user. * @@ -390,7 +353,6 @@ export { fetchNoteToSelfConversation, getUpcomingEvents, searchListedConversations, - searchPossibleConversations, createOneToOneConversation, createGroupConversation, createPrivateConversation, diff --git a/src/services/coreService.ts b/src/services/coreService.ts new file mode 100644 index 00000000000..5dfc93e0a28 --- /dev/null +++ b/src/services/coreService.ts @@ -0,0 +1,56 @@ +/** + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import axios from '@nextcloud/axios' +import { generateOcsUrl } from '@nextcloud/router' + +import { getTalkConfig, hasTalkFeature } from './CapabilitiesManager.ts' +import { SHARE } from '../constants.js' + +const canInviteToFederation = hasTalkFeature('local', 'federation-v1') + && getTalkConfig('local', 'federation', 'enabled') + && getTalkConfig('local', 'federation', 'outgoing-enabled') + +type SearchPayload = { + searchText: string + token?: string + onlyUsers?: boolean +} + +/** + * Fetch possible conversations + * + * @param data the wrapping object; + * @param data.searchText The string that will be used in the search query. + * @param [data.token] The token of the conversation (if any), or "new" for a new one + * @param [data.onlyUsers] Only return users + * @param options options + */ +const autocompleteQuery = async function({ searchText, token, onlyUsers }: SearchPayload, options: object) { + token = token || 'new' + onlyUsers = !!onlyUsers + + const shareTypes = [ + SHARE.TYPE.USER, + !onlyUsers ? SHARE.TYPE.GROUP : null, + !onlyUsers ? SHARE.TYPE.CIRCLE : null, + (!onlyUsers && token !== 'new') ? SHARE.TYPE.EMAIL : null, + (!onlyUsers && canInviteToFederation) ? SHARE.TYPE.REMOTE : null, + ].filter(type => type !== null) + + return axios.get(generateOcsUrl('core/autocomplete/get'), { + ...options, + params: { + search: searchText, + itemType: 'call', + itemId: token, + shareTypes, + }, + }) +} + +export { + autocompleteQuery, +} From fe453ff3b64bf0415dc7e80c31a34f99cf27fecc Mon Sep 17 00:00:00 2001 From: Maksim Sukharev Date: Tue, 5 Nov 2024 12:15:21 +0100 Subject: [PATCH 2/3] fix: adjust autocompleteQuery payload Signed-off-by: Maksim Sukharev --- src/components/LeftSidebar/LeftSidebar.vue | 2 +- src/services/coreService.ts | 31 +++++++++++----------- 2 files changed, 16 insertions(+), 17 deletions(-) diff --git a/src/components/LeftSidebar/LeftSidebar.vue b/src/components/LeftSidebar/LeftSidebar.vue index a333d6bb995..d4847d32894 100644 --- a/src/components/LeftSidebar/LeftSidebar.vue +++ b/src/components/LeftSidebar/LeftSidebar.vue @@ -742,7 +742,7 @@ export default { const response = await request({ searchText: this.searchText, - token: undefined, + token: 'new', onlyUsers: !this.canStartConversations, }) diff --git a/src/services/coreService.ts b/src/services/coreService.ts index 5dfc93e0a28..267f86d2c11 100644 --- a/src/services/coreService.ts +++ b/src/services/coreService.ts @@ -15,30 +15,29 @@ const canInviteToFederation = hasTalkFeature('local', 'federation-v1') type SearchPayload = { searchText: string - token?: string + token?: string | 'new' onlyUsers?: boolean } /** * Fetch possible conversations * - * @param data the wrapping object; - * @param data.searchText The string that will be used in the search query. - * @param [data.token] The token of the conversation (if any), or "new" for a new one - * @param [data.onlyUsers] Only return users + * @param payload the wrapping object; + * @param payload.searchText The string that will be used in the search query. + * @param [payload.token] The token of the conversation (if any) | 'new' for new conversations + * @param [payload.onlyUsers] Whether to return only registered users * @param options options */ -const autocompleteQuery = async function({ searchText, token, onlyUsers }: SearchPayload, options: object) { - token = token || 'new' - onlyUsers = !!onlyUsers - - const shareTypes = [ - SHARE.TYPE.USER, - !onlyUsers ? SHARE.TYPE.GROUP : null, - !onlyUsers ? SHARE.TYPE.CIRCLE : null, - (!onlyUsers && token !== 'new') ? SHARE.TYPE.EMAIL : null, - (!onlyUsers && canInviteToFederation) ? SHARE.TYPE.REMOTE : null, - ].filter(type => type !== null) +const autocompleteQuery = async function({ searchText, token = 'new', onlyUsers = false }: SearchPayload, options: object) { + const shareTypes = onlyUsers + ? [SHARE.TYPE.USER] + : [ + SHARE.TYPE.USER, + SHARE.TYPE.GROUP, + SHARE.TYPE.CIRCLE, + token !== 'new' ? SHARE.TYPE.EMAIL : null, + canInviteToFederation ? SHARE.TYPE.REMOTE : null, + ].filter(type => type !== null) return axios.get(generateOcsUrl('core/autocomplete/get'), { ...options, From 58122e1e9238992cdc3d8b432c9091120d2f84e8 Mon Sep 17 00:00:00 2001 From: Maksim Sukharev Date: Tue, 5 Nov 2024 16:49:02 +0100 Subject: [PATCH 3/3] fix: allow to add e-mail guests when creating a conversation - introduce 'forceTypes' in autocomplete request Signed-off-by: Maksim Sukharev --- .../NewConversationDialog/NewConversationContactsPage.vue | 7 ++++++- src/services/coreService.ts | 8 +++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/components/NewConversationDialog/NewConversationContactsPage.vue b/src/components/NewConversationDialog/NewConversationContactsPage.vue index c48b661b158..6ba0b1f7a6b 100644 --- a/src/components/NewConversationDialog/NewConversationContactsPage.vue +++ b/src/components/NewConversationDialog/NewConversationContactsPage.vue @@ -76,6 +76,7 @@ import DialpadPanel from '../UIShared/DialpadPanel.vue' import TransitionWrapper from '../UIShared/TransitionWrapper.vue' import { useArrowNavigation } from '../../composables/useArrowNavigation.js' +import { SHARE } from '../../constants.js' import { autocompleteQuery } from '../../services/coreService.ts' import CancelableRequest from '../../utils/cancelableRequest.js' @@ -208,7 +209,11 @@ export default { const { request, cancel } = CancelableRequest(autocompleteQuery) this.cancelSearchPossibleConversations = cancel - const response = await request({ searchText: this.searchText }) + const response = await request({ + searchText: this.searchText, + token: 'new', + forceTypes: [SHARE.TYPE.EMAIL], // e-mail guests are allowed directly after conversation creation + }) this.searchResults = response?.data?.ocs?.data || [] if (this.searchResults.length === 0) { diff --git a/src/services/coreService.ts b/src/services/coreService.ts index 267f86d2c11..aa88642dedb 100644 --- a/src/services/coreService.ts +++ b/src/services/coreService.ts @@ -17,6 +17,7 @@ type SearchPayload = { searchText: string token?: string | 'new' onlyUsers?: boolean + forceTypes?: typeof SHARE.TYPE[keyof typeof SHARE.TYPE][] } /** @@ -26,18 +27,19 @@ type SearchPayload = { * @param payload.searchText The string that will be used in the search query. * @param [payload.token] The token of the conversation (if any) | 'new' for new conversations * @param [payload.onlyUsers] Whether to return only registered users + * @param [payload.forceTypes] Whether to force some types to be included in query * @param options options */ -const autocompleteQuery = async function({ searchText, token = 'new', onlyUsers = false }: SearchPayload, options: object) { +const autocompleteQuery = async function({ searchText, token = 'new', onlyUsers = false, forceTypes = [] }: SearchPayload, options: object) { const shareTypes = onlyUsers - ? [SHARE.TYPE.USER] + ? [SHARE.TYPE.USER].concat(forceTypes) : [ SHARE.TYPE.USER, SHARE.TYPE.GROUP, SHARE.TYPE.CIRCLE, token !== 'new' ? SHARE.TYPE.EMAIL : null, canInviteToFederation ? SHARE.TYPE.REMOTE : null, - ].filter(type => type !== null) + ].filter(type => type !== null).concat(forceTypes) return axios.get(generateOcsUrl('core/autocomplete/get'), { ...options,