Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[stable30] feat: invite email guests when creating a conversation #13708

Merged
merged 3 commits into from
Nov 6, 2024
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
9 changes: 6 additions & 3 deletions src/components/LeftSidebar/LeftSidebar.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -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,
Expand Down
6 changes: 3 additions & 3 deletions src/components/LeftSidebar/LeftSidebar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -737,12 +737,12 @@ 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({
searchText: this.searchText,
token: undefined,
token: 'new',
onlyUsers: !this.canStartConversations,
})

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,8 @@ 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 { SHARE } from '../../constants.js'
import { autocompleteQuery } from '../../services/coreService.ts'
import CancelableRequest from '../../utils/cancelableRequest.js'

export default {
Expand Down Expand Up @@ -205,10 +206,14 @@ 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 })
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) {
Expand Down
4 changes: 2 additions & 2 deletions src/components/RightSidebar/Participants/ParticipantsTab.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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({
Expand Down
6 changes: 5 additions & 1 deletion src/services/CapabilitiesManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand All @@ -24,7 +24,7 @@ jest.mock('@nextcloud/capabilities', () => ({
}))
}))

describe('conversationsService', () => {
describe('coreService', () => {
afterEach(() => {
// cleaning up the mess left behind the previous test
jest.clearAllMocks()
Expand All @@ -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,
Expand All @@ -60,8 +60,8 @@ describe('conversationsService', () => {
)
}

test('searchPossibleConversations with only users', () => {
testSearchPossibleConversations(
test('autocompleteQuery with only users', () => {
testAutocompleteQuery(
'conversation-token',
true,
[
Expand All @@ -70,8 +70,8 @@ describe('conversationsService', () => {
)
})

test('searchPossibleConversations with other share types', () => {
testSearchPossibleConversations(
test('autocompleteQuery with other share types', () => {
testAutocompleteQuery(
'conversation-token',
false,
[
Expand All @@ -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,
[
Expand Down
40 changes: 1 addition & 39 deletions src/services/conversationsService.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
*
Expand Down Expand Up @@ -390,7 +353,6 @@ export {
fetchNoteToSelfConversation,
getUpcomingEvents,
searchListedConversations,
searchPossibleConversations,
createOneToOneConversation,
createGroupConversation,
createPrivateConversation,
Expand Down
57 changes: 57 additions & 0 deletions src/services/coreService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/**
* 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 | 'new'
onlyUsers?: boolean
forceTypes?: typeof SHARE.TYPE[keyof typeof SHARE.TYPE][]
}

/**
* Fetch possible conversations
*
* @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 [payload.forceTypes] Whether to force some types to be included in query
* @param options options
*/
const autocompleteQuery = async function({ searchText, token = 'new', onlyUsers = false, forceTypes = [] }: SearchPayload, options: object) {
const shareTypes = onlyUsers
? [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).concat(forceTypes)

return axios.get(generateOcsUrl('core/autocomplete/get'), {
...options,
params: {
search: searchText,
itemType: 'call',
itemId: token,
shareTypes,
},
})
}

export {
autocompleteQuery,
}
Loading