From 97973dd8e5560eb6eec6a36593a6ede15afc531d Mon Sep 17 00:00:00 2001 From: Ilijan Kotarac Date: Tue, 28 Jul 2026 16:35:26 +0200 Subject: [PATCH] Fetch team collaborators and show them in the add-participants list Add a getCollaborators endpoint on TeamAPI (GET /teams/{tid}/collaborators), plumb it through TeamService and a new TeamRepository.getTeamCollaborators() that resolves the returned user IDs into full profiles via UserRepository. Merge the resolved collaborators into addParticipants' contacts list so they appear alongside team members and services when adding participants to a conversation, without needing a separate tab or picker. --- .../addParticipants/addParticipants.tsx | 34 +++++++++++++++++-- .../repositories/team/TeamRepository.ts | 16 +++++++++ .../script/repositories/team/TeamService.ts | 6 +++- libraries/api-client/src/team/index.ts | 11 +++++- .../team/collaborator/teamCollaborator.ts | 29 ++++++++++++++++ libraries/api-client/src/team/team/index.ts | 1 + libraries/api-client/src/team/team/teamApi.ts | 12 +++++++ 7 files changed, 105 insertions(+), 4 deletions(-) create mode 100644 libraries/api-client/src/team/team/collaborator/teamCollaborator.ts diff --git a/apps/webapp/src/script/page/rightSidebar/addParticipants/addParticipants.tsx b/apps/webapp/src/script/page/rightSidebar/addParticipants/addParticipants.tsx index b0d26120c5f..686d3fa8080 100644 --- a/apps/webapp/src/script/page/rightSidebar/addParticipants/addParticipants.tsx +++ b/apps/webapp/src/script/page/rightSidebar/addParticipants/addParticipants.tsx @@ -17,7 +17,7 @@ * */ -import {FC, useCallback, useMemo, useState} from 'react'; +import {FC, useCallback, useEffect, useMemo, useState} from 'react'; import is from '@sindresorhus/is'; import {CONVERSATION_PROTOCOL} from '@wireapp/api-client/lib/team'; @@ -124,7 +124,27 @@ const AddParticipants: FC = ({ const [selectedContacts, setSelectedContacts] = useState([]); const [isInitialServiceSearch, setIsInitialServiceSearch] = useState(true); - const contacts = useMemo(() => { + const [collaborators, setCollaborators] = useState([]); + + useEffect(() => { + if (!isTeam) { + return undefined; + } + + let isCurrent = true; + + void teamRepository.getTeamCollaborators().then(fetchedCollaborators => { + if (isCurrent) { + setCollaborators(fetchedCollaborators); + } + }); + + return () => { + isCurrent = false; + }; + }, [isTeam, teamRepository]); + + const baseContacts = useMemo(() => { if (isTeam) { const isTeamOrServices = isTeamOnly || isServicesRoom; return isTeamOrServices ? teamMembers.toSorted(sortUsersByPriority) : teamUsers; @@ -132,6 +152,16 @@ const AddParticipants: FC = ({ return connectedUsers; }, [connectedUsers, isServicesRoom, isTeam, isTeamOnly, teamMembers, teamUsers]); + const contacts = useMemo(() => { + if (!collaborators.length) { + return baseContacts; + } + // Collaborators (team-level, without full team membership) are shown alongside team members/apps + const knownIds = new Set(baseContacts.map(contact => contact.id)); + const newCollaborators = collaborators.filter(collaborator => !knownIds.has(collaborator.id)); + return [...baseContacts, ...newCollaborators]; + }, [baseContacts, collaborators]); + const apps = useMemo(() => { const normalizedQuery = searchInput.trim().toLowerCase(); return contacts diff --git a/apps/webapp/src/script/repositories/team/TeamRepository.ts b/apps/webapp/src/script/repositories/team/TeamRepository.ts index 027a76a7106..1915e473ad1 100644 --- a/apps/webapp/src/script/repositories/team/TeamRepository.ts +++ b/apps/webapp/src/script/repositories/team/TeamRepository.ts @@ -388,6 +388,22 @@ export class TeamRepository extends TypedEventEmitter { return IntegrationMapper.mapServicesFromArray(servicesData, domain); } + async getTeamCollaborators(): Promise { + const teamId = this.teamState.team()?.id; + if (!teamId) { + return []; + } + + const domain = this.teamState.teamDomain(); + const selfId = this.userState.self().id; + const collaborators = await this.teamService.getCollaborators(teamId); + const collaboratorIds: QualifiedId[] = collaborators + .map(({user}) => ({domain, id: user})) + .filter(({id}) => id !== selfId); + + return this.userRepository.getUsersById(collaboratorIds); + } + readonly onTeamEvent = async (eventJson: any, source: EventSource): Promise => { if (this.teamState.isTeamDeleted()) { // We don't want to handle any events after the team has been deleted diff --git a/apps/webapp/src/script/repositories/team/TeamService.ts b/apps/webapp/src/script/repositories/team/TeamService.ts index 41447266bbc..1b6e403c3a9 100644 --- a/apps/webapp/src/script/repositories/team/TeamService.ts +++ b/apps/webapp/src/script/repositories/team/TeamService.ts @@ -24,7 +24,7 @@ import {TeamMigrationPayload} from '@wireapp/api-client/lib/team/invitation/team import type {LegalHoldMemberData} from '@wireapp/api-client/lib/team/legalhold/'; import type {MemberData, Members} from '@wireapp/api-client/lib/team/member/'; import type {Services} from '@wireapp/api-client/lib/team/service/'; -import type {TeamData} from '@wireapp/api-client/lib/team/team/'; +import type {TeamCollaborator, TeamData} from '@wireapp/api-client/lib/team/team/'; import {container} from 'tsyringe'; import {APIClient} from '../../service/apiClientSingleton'; @@ -64,6 +64,10 @@ export class TeamService { return this.apiClient.api.teams.service.getTeamServices(teamId); } + getCollaborators(teamId: string): Promise { + return this.apiClient.api.teams.team.getCollaborators(teamId); + } + async conversationHasGuestLink(conversationId: string): Promise { const {status} = await this.apiClient.api.conversation.getConversationGuestLinkFeature(conversationId); return status === 'enabled'; diff --git a/libraries/api-client/src/team/index.ts b/libraries/api-client/src/team/index.ts index a55c80e122a..8ab3df58eb4 100644 --- a/libraries/api-client/src/team/index.ts +++ b/libraries/api-client/src/team/index.ts @@ -20,7 +20,16 @@ export {NewTeamInvitation, TeamInvitation, TeamInvitationAPI, TeamInvitationChunk} from './invitation/'; export {LegalHoldAPI} from './legalhold/'; export {MemberAPI, MemberData, Members, Permissions, PermissionsData, Role} from './member/'; -export {NewTeamData, TeamAPI, TeamChunkData, TeamData, TeamInfo, UpdateTeamData} from './team/'; +export { + CollaboratorPermission, + NewTeamData, + TeamAPI, + TeamChunkData, + TeamCollaborator, + TeamData, + TeamInfo, + UpdateTeamData, +} from './team/'; export {PaymentAPI, PaymentData} from './payment/'; export {ServiceAPI, Service, ServiceWhitelistData} from './service/'; export {TeamError, InviteEmailInUseError, InvalidInvitationCodeError, ServiceNotFoundError} from './teamError'; diff --git a/libraries/api-client/src/team/team/collaborator/teamCollaborator.ts b/libraries/api-client/src/team/team/collaborator/teamCollaborator.ts new file mode 100644 index 00000000000..37a6832cd35 --- /dev/null +++ b/libraries/api-client/src/team/team/collaborator/teamCollaborator.ts @@ -0,0 +1,29 @@ +/* + * Wire + * Copyright (C) 2026 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + * + */ + +export enum CollaboratorPermission { + CREATE_TEAM_CONVERSATION = 'create_team_conversation', + IMPLICIT_CONNECTION = 'implicit_connection', +} + +export interface TeamCollaborator { + permissions: CollaboratorPermission[]; + team: string; + user: string; +} diff --git a/libraries/api-client/src/team/team/index.ts b/libraries/api-client/src/team/team/index.ts index 8194733223f..c6410a20f35 100644 --- a/libraries/api-client/src/team/team/index.ts +++ b/libraries/api-client/src/team/team/index.ts @@ -17,6 +17,7 @@ * */ +export * from './collaborator/teamCollaborator'; export * from './newTeamData'; export * from './teamApi'; export * from './teamChunkData'; diff --git a/libraries/api-client/src/team/team/teamApi.ts b/libraries/api-client/src/team/team/teamApi.ts index fcf8a52300d..8c172a238bf 100644 --- a/libraries/api-client/src/team/team/teamApi.ts +++ b/libraries/api-client/src/team/team/teamApi.ts @@ -21,6 +21,7 @@ import Axios, {AxiosRequestConfig} from 'axios'; import {NewAppRequest} from './app/newAppRequest'; import {NewAppResponse} from './app/newAppResponse'; +import {TeamCollaborator} from './collaborator/teamCollaborator'; import {LeadData} from './leadData'; import {TeamSizeData} from './teamSizeData'; import {UpdateTeamData} from './updateTeamData'; @@ -34,6 +35,7 @@ export class TeamAPI { public static readonly URL = { APPS: 'apps', + COLLABORATORS: 'collaborators', SIZE: 'size', TEAMS: '/teams', CONSENT: '/consent', @@ -143,6 +145,16 @@ export class TeamAPI { return response.data; } + public async getCollaborators(teamId: string): Promise { + const config: AxiosRequestConfig = { + method: 'get', + url: `${TeamAPI.URL.TEAMS}/${teamId}/${TeamAPI.URL.COLLABORATORS}`, + }; + + const response = await this.client.sendJSON(config); + return response.data; + } + public async postApp(teamId: string, app: NewAppRequest): Promise { const config: AxiosRequestConfig = { data: app,