From 02f89f03d03a92fa7760aba27d5ed5aa16dd4a28 Mon Sep 17 00:00:00 2001 From: Paul Anuschek Date: Thu, 19 Feb 2026 11:06:10 +0100 Subject: [PATCH 01/12] added Note component --- .../GroupCreation/GroupCreationModal.tsx | 40 ++++++++++++----- .../src/script/components/Note/Note.styles.ts | 43 ++++++++++++++++++ .../src/script/components/Note/Note.tsx | 45 +++++++++++++++++++ 3 files changed, 116 insertions(+), 12 deletions(-) create mode 100644 apps/webapp/src/script/components/Note/Note.styles.ts create mode 100644 apps/webapp/src/script/components/Note/Note.tsx diff --git a/apps/webapp/src/script/components/Modals/GroupCreation/GroupCreationModal.tsx b/apps/webapp/src/script/components/Modals/GroupCreation/GroupCreationModal.tsx index c0a0aa208b7..faa4980ab1e 100644 --- a/apps/webapp/src/script/components/Modals/GroupCreation/GroupCreationModal.tsx +++ b/apps/webapp/src/script/components/Modals/GroupCreation/GroupCreationModal.tsx @@ -27,12 +27,13 @@ import {amplify} from 'amplify'; import cx from 'classnames'; import {container} from 'tsyringe'; -import {Button, ButtonVariant, Option, Select} from '@wireapp/react-ui-kit'; +import {Button, ButtonVariant, Option, Select, Text} from '@wireapp/react-ui-kit'; import {WebAppEvents} from '@wireapp/webapp-events'; import {FadingScrollbar} from 'Components/FadingScrollbar'; import * as Icon from 'Components/Icon'; import {ModalComponent} from 'Components/Modals/ModalComponent'; +import {Note} from 'Components/Note/Note'; import {SearchInput} from 'Components/SearchInput'; import {TextInput} from 'Components/TextInput'; import {InfoToggle} from 'Components/toggle/InfoToggle'; @@ -79,11 +80,13 @@ const GroupCreationModal = ({ isMLSEnabled: isMLSEnabledForTeam, isProtocolToggleEnabledForUser, isCellsEnabled: isCellsEnabledForTeam, + isAppsEnabled: isAppsEnabledForTeam, } = useKoSubscribableChildren(teamState, [ 'isTeam', 'isMLSEnabled', 'isProtocolToggleEnabledForUser', 'isCellsEnabled', + 'isAppsEnabled', ]); const {self: selfUser} = useKoSubscribableChildren(userState, ['self']); @@ -154,7 +157,13 @@ const GroupCreationModal = ({ const isGuestAndServicesRoom = accessState === ACCESS_STATE.TEAM.GUESTS_SERVICES; const isGuestRoom = accessState === ACCESS_STATE.TEAM.GUEST_ROOM; const isGuestEnabled = isGuestRoom || isGuestAndServicesRoom; - const isServicesEnabled = isServicesRoom || isGuestAndServicesRoom; + + const isAppsFeatureAvailable = + isTeam && + ((selectedProtocol.value == CONVERSATION_PROTOCOL.PROTEUS && teamState?.hasWhitelistedServices()) || + (selectedProtocol.value == CONVERSATION_PROTOCOL.MLS && isAppsEnabledForTeam)); + + const isServicesEnabled = isAppsFeatureAvailable && (isServicesRoom || isGuestAndServicesRoom); const {setCurrentTab: setCurrentSidebarTab} = useSidebarStore(); @@ -504,17 +513,24 @@ const GroupCreationModal = ({ name={t('guestOptionsTitle')} info={t('guestRoomToggleInfo')} /> - {selectedProtocol.value !== CONVERSATION_PROTOCOL.MLS && ( - + + + {!isAppsFeatureAvailable && ( + + + To improve your workflow with apps, your team needs configuration. Please contact your team admin. + + )} + {areReadReceiptsEnabled && ( = ({title, children}) => { + return ( +
+
+ + {title} +
+ +
+
{children}
+
+
+ ); +}; + +export {Note}; From 3819a9453ceb18a897beb9e085ce198fc1eca1cd Mon Sep 17 00:00:00 2001 From: Paul Anuschek Date: Tue, 24 Feb 2026 09:25:57 +0100 Subject: [PATCH 02/12] added apps disabled banner --- apps/webapp/src/i18n/en-US.json | 2 + .../CreateConversationSteps/Preference.tsx | 40 ++++++++++++------- .../GroupCreation/GroupCreationModal.tsx | 12 ++---- .../AppsDisabledNote/AppsDisabledNote.tsx | 33 +++++++++++++++ .../src/script/components/Note/Note.styles.ts | 16 +++++--- 5 files changed, 75 insertions(+), 28 deletions(-) create mode 100644 apps/webapp/src/script/components/Note/AppsDisabledNote/AppsDisabledNote.tsx diff --git a/apps/webapp/src/i18n/en-US.json b/apps/webapp/src/i18n/en-US.json index 6e49ba83173..8973ebf4763 100644 --- a/apps/webapp/src/i18n/en-US.json +++ b/apps/webapp/src/i18n/en-US.json @@ -1846,6 +1846,8 @@ "selfNotSupportMLSMsgPart1": "You can't communicate with {selfUserName}, as your device doesn't support the suitable protocol.", "selfNotSupportMLSMsgPart2": "to call, and send messages and files.", "selfProfileImageAlt": "Your profile picture", + "servicesNotEnabledNoteTitle": "Your team doesn't use apps yet", + "servicesNotEnabledBody": "To improve your workflow with apps, your team needs configuration. Please contact your team admin.", "servicesOptionsTitle": "Apps", "servicesRoomToggleInfo": "Open this conversation to apps.", "servicesRoomToggleInfoExtended": "Open this conversation to apps. You can always change it later.", diff --git a/apps/webapp/src/script/components/Modals/CreateConversation/CreateConversationSteps/Preference.tsx b/apps/webapp/src/script/components/Modals/CreateConversation/CreateConversationSteps/Preference.tsx index bac44ef95ae..84ec893d0d5 100644 --- a/apps/webapp/src/script/components/Modals/CreateConversation/CreateConversationSteps/Preference.tsx +++ b/apps/webapp/src/script/components/Modals/CreateConversation/CreateConversationSteps/Preference.tsx @@ -20,6 +20,7 @@ import {CONVERSATION_PROTOCOL} from '@wireapp/api-client/lib/team'; import {container} from 'tsyringe'; +import {AppsDisabledNote} from 'Components/Note/AppsDisabledNote/AppsDisabledNote'; import {InfoToggle} from 'Components/toggle/InfoToggle'; import {TeamState} from 'Repositories/team/TeamState'; import {Config} from 'src/script/Config'; @@ -27,7 +28,6 @@ import {useKoSubscribableChildren} from 'Util/ComponentUtil'; import {t} from 'Util/localizerUtil'; import {useCreateConversationModal} from '../hooks/useCreateConversationModal'; -import {ConversationType} from '../types'; export const Preference = () => { const { @@ -44,9 +44,16 @@ export const Preference = () => { const teamState = container.resolve(TeamState); - const {isCellsEnabled: isCellsEnabledForTeam, isMLSEnabled} = useKoSubscribableChildren(teamState, [ + const { + isCellsEnabled: isCellsEnabledForTeam, + isMLSEnabled, + isAppsEnabled, + hasWhitelistedServices, + } = useKoSubscribableChildren(teamState, [ 'isCellsEnabled', 'isMLSEnabled', + 'isAppsEnabled', + 'hasWhitelistedServices', ]); const isCellsEnabledForEnvironment = Config.getConfig().FEATURE.ENABLE_CELLS; const isCellsOptionEnabled = isCellsEnabledForEnvironment && isCellsEnabledForTeam; @@ -55,7 +62,11 @@ export const Preference = () => { ? teamState.teamFeatures()?.mls?.config.defaultProtocol : CONVERSATION_PROTOCOL.PROTEUS; - // Read receipts are temorarily disabled for MLS groups and channels until it is supported + const isAppsFeatureAvailable = + (defaultProtocol === CONVERSATION_PROTOCOL.MLS && isAppsEnabled) || + (defaultProtocol === CONVERSATION_PROTOCOL.PROTEUS && hasWhitelistedServices); + + // Read receipts are temporarily disabled for MLS groups and channels until it is supported const areReadReceiptsEnabled = defaultProtocol !== CONVERSATION_PROTOCOL.MLS; return ( @@ -70,17 +81,18 @@ export const Preference = () => { dataUieName="read-receipts" /> - {conversationType === ConversationType.Group && ( - - )} + + + {!isAppsFeatureAvailable && } + {areReadReceiptsEnabled && ( - {!isAppsFeatureAvailable && ( - - - To improve your workflow with apps, your team needs configuration. Please contact your team admin. - - - )} + {!isAppsFeatureAvailable && } {areReadReceiptsEnabled && ( { + return ( + + {t('servicesNotEnabledBody')} + + ); +}; + +export {AppsDisabledNote}; diff --git a/apps/webapp/src/script/components/Note/Note.styles.ts b/apps/webapp/src/script/components/Note/Note.styles.ts index 748f141f2be..eea7cf35f26 100644 --- a/apps/webapp/src/script/components/Note/Note.styles.ts +++ b/apps/webapp/src/script/components/Note/Note.styles.ts @@ -20,11 +20,17 @@ import {CSSObject} from '@emotion/react'; export const ContainerStyle: CSSObject = { - gap: '12px', - padding: '16px 20px', - backgroundColor: '#e8f0fe', - border: '1px solid #1967d2', - borderRadius: '8px', + display: 'flex', + flexDirection: 'column', + gap: '0.3rem', + padding: '0.75rem', + background: 'var(--accent-color-50)', + '.theme-dark &': { + background: 'var(--accent-color-800)', + boxShadow: 'none', + }, + border: '1px solid var(--accent-color-500)', + borderRadius: '0.5rem', color: '#000', lineHeight: '1.5', }; From 8530839a61a1bf735451a02d92a907a32bace4cc Mon Sep 17 00:00:00 2001 From: Paul Date: Tue, 24 Feb 2026 13:45:26 +0100 Subject: [PATCH 03/12] fixed apps not enabled note formatting --- .../CreateConversationSteps/Preference.tsx | 8 +++++--- apps/webapp/src/script/components/Note/Note.styles.ts | 2 +- apps/webapp/src/script/components/toggle/InfoToggle.tsx | 5 ++++- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/apps/webapp/src/script/components/Modals/CreateConversation/CreateConversationSteps/Preference.tsx b/apps/webapp/src/script/components/Modals/CreateConversation/CreateConversationSteps/Preference.tsx index 84ec893d0d5..34ae1d55344 100644 --- a/apps/webapp/src/script/components/Modals/CreateConversation/CreateConversationSteps/Preference.tsx +++ b/apps/webapp/src/script/components/Modals/CreateConversation/CreateConversationSteps/Preference.tsx @@ -20,6 +20,7 @@ import {CONVERSATION_PROTOCOL} from '@wireapp/api-client/lib/team'; import {container} from 'tsyringe'; +import {ConversationType} from 'Components/Modals/CreateConversation/types'; import {AppsDisabledNote} from 'Components/Note/AppsDisabledNote/AppsDisabledNote'; import {InfoToggle} from 'Components/toggle/InfoToggle'; import {TeamState} from 'Repositories/team/TeamState'; @@ -64,7 +65,9 @@ export const Preference = () => { const isAppsFeatureAvailable = (defaultProtocol === CONVERSATION_PROTOCOL.MLS && isAppsEnabled) || - (defaultProtocol === CONVERSATION_PROTOCOL.PROTEUS && hasWhitelistedServices); + (defaultProtocol === CONVERSATION_PROTOCOL.PROTEUS && + hasWhitelistedServices && + conversationType !== ConversationType.Channel); // Read receipts are temporarily disabled for MLS groups and channels until it is supported const areReadReceiptsEnabled = defaultProtocol !== CONVERSATION_PROTOCOL.MLS; @@ -89,10 +92,9 @@ export const Preference = () => { isDisabled={!isAppsFeatureAvailable} name={t('servicesOptionsTitle')} isChecked={isServicesEnabled && isAppsFeatureAvailable} + label={!isAppsFeatureAvailable && } /> - {!isAppsFeatureAvailable && } - {areReadReceiptsEnabled && ( void; + label?: React.ReactNode; } const InfoToggle = ({ @@ -39,6 +40,7 @@ const InfoToggle = ({ isDisabled, name, setIsChecked, + label, }: InfoToggleProps) => { const dataUieNameInfoText = `status-info-toggle-${dataUieName}`; const dataUieNameLabelText = `do-toggle-${dataUieName}`; @@ -77,6 +79,7 @@ const InfoToggle = ({ + {label} ); }; From 35cad354a90619727ae2b559b32c6c43cdda6e8d Mon Sep 17 00:00:00 2001 From: Paul Date: Wed, 25 Feb 2026 11:13:46 +0100 Subject: [PATCH 04/12] fixed i18n.d.ts types --- apps/webapp/src/types/i18n.d.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/webapp/src/types/i18n.d.ts b/apps/webapp/src/types/i18n.d.ts index dd4f9087816..11dd656a9a3 100644 --- a/apps/webapp/src/types/i18n.d.ts +++ b/apps/webapp/src/types/i18n.d.ts @@ -1850,6 +1850,8 @@ declare module 'I18n/en-US.json' { 'selfNotSupportMLSMsgPart1': `You can\'t communicate with {selfUserName}, as your device doesn\'t support the suitable protocol.`; 'selfNotSupportMLSMsgPart2': `to call, and send messages and files.`; 'selfProfileImageAlt': `Your profile picture`; + 'servicesNotEnabledNoteTitle': `Your team doesn\'t use apps yet`; + 'servicesNotEnabledBody': `To improve your workflow with apps, your team needs configuration. Please contact your team admin.`; 'servicesOptionsTitle': `Apps`; 'servicesRoomToggleInfo': `Open this conversation to apps.`; 'servicesRoomToggleInfoExtended': `Open this conversation to apps. You can always change it later.`; From 633c489dce207fffcf86f947662fa7e4bd11c3ff Mon Sep 17 00:00:00 2001 From: Paul Date: Thu, 26 Feb 2026 08:55:12 +0100 Subject: [PATCH 05/12] reintroduced missing i18n.d.ts types --- apps/webapp/src/i18n/en-US.json | 7 +++++++ apps/webapp/src/types/i18n.d.ts | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/apps/webapp/src/i18n/en-US.json b/apps/webapp/src/i18n/en-US.json index 8973ebf4763..572aacfc160 100644 --- a/apps/webapp/src/i18n/en-US.json +++ b/apps/webapp/src/i18n/en-US.json @@ -713,6 +713,13 @@ "conversationFileUploadOverlayDescription": "Drag & drop to add files", "conversationFileUploadOverlayTitle": "Upload files", "conversationFileVideoPreviewLabel": "Video file preview for: {src}", + "conversationFilterDrafts": "Drafts", + "conversationFilterMentions": "Mentions", + "conversationFilterNone": "No filter", + "conversationFilterPings": "Pings", + "conversationFilterReplies": "Replies", + "conversationFilterTooltip": "Filter conversations", + "conversationFilterUnread": "Unread", "conversationFoldersEmptyText": "Add your conversations to folders to stay organized.", "conversationFoldersEmptyTextLearnMore": "Learn more", "conversationFooterArchive": "Archive", diff --git a/apps/webapp/src/types/i18n.d.ts b/apps/webapp/src/types/i18n.d.ts index 11dd656a9a3..02b51c07b77 100644 --- a/apps/webapp/src/types/i18n.d.ts +++ b/apps/webapp/src/types/i18n.d.ts @@ -717,6 +717,13 @@ declare module 'I18n/en-US.json' { 'conversationFileUploadOverlayDescription': `Drag & drop to add files`; 'conversationFileUploadOverlayTitle': `Upload files`; 'conversationFileVideoPreviewLabel': `Video file preview for: {src}`; + 'conversationFilterDrafts': `Drafts`; + 'conversationFilterMentions': `Mentions`; + 'conversationFilterNone': `No filter`; + 'conversationFilterPings': `Pings`; + 'conversationFilterReplies': `Replies`; + 'conversationFilterTooltip': `Filter conversations`; + 'conversationFilterUnread': `Unread`; 'conversationFoldersEmptyText': `Add your conversations to folders to stay organized.`; 'conversationFoldersEmptyTextLearnMore': `Learn more`; 'conversationFooterArchive': `Archive`; From de474d4edd7f81f1ed071e8952d6994238a58be5 Mon Sep 17 00:00:00 2001 From: Paul <44203546+panuschek@users.noreply.github.com> Date: Thu, 26 Feb 2026 10:34:22 +0100 Subject: [PATCH 06/12] Update apps/webapp/src/script/components/Note/AppsDisabledNote/AppsDisabledNote.tsx Co-authored-by: Zafar Saeed Khan --- .../components/Note/AppsDisabledNote/AppsDisabledNote.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/webapp/src/script/components/Note/AppsDisabledNote/AppsDisabledNote.tsx b/apps/webapp/src/script/components/Note/AppsDisabledNote/AppsDisabledNote.tsx index 4b102e8a847..973859fbf1a 100644 --- a/apps/webapp/src/script/components/Note/AppsDisabledNote/AppsDisabledNote.tsx +++ b/apps/webapp/src/script/components/Note/AppsDisabledNote/AppsDisabledNote.tsx @@ -22,7 +22,7 @@ import React from 'react'; import {Note} from 'Components/Note/Note'; import {t} from 'Util/LocalizerUtil'; -const AppsDisabledNote: React.FC = () => { +const AppsDisabledNote = () => { return ( {t('servicesNotEnabledBody')} From 389d333778192a47c14d25ef80509797e00f9727 Mon Sep 17 00:00:00 2001 From: Paul <44203546+panuschek@users.noreply.github.com> Date: Thu, 26 Feb 2026 10:34:31 +0100 Subject: [PATCH 07/12] Update apps/webapp/src/script/components/Note/Note.tsx Co-authored-by: Zafar Saeed Khan --- apps/webapp/src/script/components/Note/Note.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/webapp/src/script/components/Note/Note.tsx b/apps/webapp/src/script/components/Note/Note.tsx index fda7d77960e..d3add20369a 100644 --- a/apps/webapp/src/script/components/Note/Note.tsx +++ b/apps/webapp/src/script/components/Note/Note.tsx @@ -27,7 +27,7 @@ interface NoteProps { children?: ReactNode; } -const Note: React.FC = ({title, children}) => { +const Note = ({title, children}: NoteProps) => { return (
From 2eb90d99600586f80817cc1718840c19b0687594 Mon Sep 17 00:00:00 2001 From: Paul Date: Sun, 1 Mar 2026 08:11:42 +0100 Subject: [PATCH 08/12] add search for aps --- .../AppsDisabledNote/AppsDisabledNote.tsx | 2 - .../src/script/components/Note/Note.tsx | 2 +- .../UserSearchableList/UserSearchableList.tsx | 19 +- .../AddParticipants/AddParticipants.tsx | 230 +++++++++--------- .../ConversationDetailsOptions.tsx | 5 +- .../script/repositories/entity/User/User.ts | 3 +- .../script/repositories/user/UserMapper.ts | 3 + libraries/api-client/src/user/user.ts | 7 + .../ConversationService.ts | 3 + 9 files changed, 146 insertions(+), 128 deletions(-) diff --git a/apps/webapp/src/script/components/Note/AppsDisabledNote/AppsDisabledNote.tsx b/apps/webapp/src/script/components/Note/AppsDisabledNote/AppsDisabledNote.tsx index 973859fbf1a..017e404bc5a 100644 --- a/apps/webapp/src/script/components/Note/AppsDisabledNote/AppsDisabledNote.tsx +++ b/apps/webapp/src/script/components/Note/AppsDisabledNote/AppsDisabledNote.tsx @@ -17,8 +17,6 @@ * */ -import React from 'react'; - import {Note} from 'Components/Note/Note'; import {t} from 'Util/LocalizerUtil'; diff --git a/apps/webapp/src/script/components/Note/Note.tsx b/apps/webapp/src/script/components/Note/Note.tsx index d3add20369a..de11257df14 100644 --- a/apps/webapp/src/script/components/Note/Note.tsx +++ b/apps/webapp/src/script/components/Note/Note.tsx @@ -17,7 +17,7 @@ * */ -import React, {ReactNode} from 'react'; +import {ReactNode} from 'react'; import {InfoIcon} from 'Components/Icon'; import {ContainerStyle, ContentStyle, HeaderStyle} from 'Components/Note/Note.styles'; diff --git a/apps/webapp/src/script/components/UserSearchableList/UserSearchableList.tsx b/apps/webapp/src/script/components/UserSearchableList/UserSearchableList.tsx index 7dbecb76923..736370d3b92 100644 --- a/apps/webapp/src/script/components/UserSearchableList/UserSearchableList.tsx +++ b/apps/webapp/src/script/components/UserSearchableList/UserSearchableList.tsx @@ -19,7 +19,7 @@ import React, {useEffect, useState} from 'react'; -import {QualifiedId} from '@wireapp/api-client/lib/user'; +import {QualifiedId, UserType} from '@wireapp/api-client/lib/user'; import {container} from 'tsyringe'; import {useDebouncedCallback} from 'use-debounce'; @@ -53,6 +53,7 @@ export type UserListProps = React.ComponentProps & { /** will do an extra request to the server when user types in (otherwise will only lookup given local users) */ allowRemoteSearch?: boolean; filterRemoteTeamUsers?: boolean; + userType: UserType; }; export const UserSearchableList = ({ @@ -66,6 +67,7 @@ export const UserSearchableList = ({ selfUser, users, teamState = container.resolve(TeamState), + userType, ...props }: UserListProps) => { const {searchRepository, teamRepository, selfFirst, ...userListProps} = props; @@ -74,7 +76,9 @@ export const UserSearchableList = ({ const [filteredUsers, setFilteredUsers] = useState([]); const [remoteTeamMembers, setRemoteTeamMembers] = useState([]); - const filteredSelectedUsers = selectedUsers ? searchRepository.searchUserInSet(filter, selectedUsers) : undefined; + const filteredSelectedUsers = selectedUsers + ? searchRepository.searchUserInSet(filter, selectedUsers).filter(u => u.type === UserType.REGULAR) + : undefined; const selfInTeam = teamState.isInTeam(selfUser); @@ -85,7 +89,7 @@ export const UserSearchableList = ({ const fetchMembersFromBackend = useDebouncedCallback(async (query: string, ignoreMembers: User[]) => { const resultUsers = await searchRepository.searchByName(query, selfUser.teamId); const selfTeamId = selfUser.teamId; - const foundMembers = resultUsers.filter(user => user.teamId === selfTeamId); + const foundMembers = resultUsers.filter(user => user.teamId === selfTeamId && user.type === userType); const ignoreIds = ignoreMembers.map(member => member.id); const uniqueMembers = foundMembers.filter(member => !ignoreIds.includes(member.id)); @@ -108,10 +112,11 @@ export const UserSearchableList = ({ .searchUserInSet(filter, users) .filter( user => - user.isMe || - conversationState.hasConversationWith(user) || - teamRepository.isSelfConnectedTo(user.id) || - user.username() === normalizedQuery, + (user.isMe || + conversationState.hasConversationWith(user) || + teamRepository.isSelfConnectedTo(user.id) || + user.username() === normalizedQuery) && + user.type === userType, ); if (normalizedQuery !== '' && selfInTeam && allowRemoteSearch) { diff --git a/apps/webapp/src/script/page/RightSidebar/AddParticipants/AddParticipants.tsx b/apps/webapp/src/script/page/RightSidebar/AddParticipants/AddParticipants.tsx index 4aba69f4556..3a745c8f25c 100644 --- a/apps/webapp/src/script/page/RightSidebar/AddParticipants/AddParticipants.tsx +++ b/apps/webapp/src/script/page/RightSidebar/AddParticipants/AddParticipants.tsx @@ -19,15 +19,13 @@ import {FC, useMemo, useState} from 'react'; -import {CONVERSATION_PROTOCOL} from '@wireapp/api-client/lib/team'; +import {UserType} from '@wireapp/api-client/lib/user'; import cx from 'classnames'; -import {TabIndex, Button, ButtonVariant} from '@wireapp/react-ui-kit'; +import {Button, TabIndex} from '@wireapp/react-ui-kit'; import {FadingScrollbar} from 'Components/FadingScrollbar'; -import * as Icon from 'Components/Icon'; import {SearchInput} from 'Components/SearchInput'; -import {ServiceList} from 'Components/ServiceList/ServiceList'; import {UserSearchableList} from 'Components/UserSearchableList'; import {ConversationRepository} from 'Repositories/conversation/ConversationRepository'; import {Conversation} from 'Repositories/entity/Conversation'; @@ -118,10 +116,15 @@ const AddParticipants: FC = ({ const contacts = useMemo(() => { if (isTeam) { const isTeamOrServices = isTeamOnly || isServicesRoom; - return isTeamOrServices ? teamMembers.sort(sortUsersByPriority) : teamUsers; + // isTeamOnly is true if guests are not allowed in this conversation + // If guests are allowed teamUsers are loaded as they contain team members AND all users the logged in user is connected with + if (currentState === PARTICIPANTS_STATE.ADD_PEOPLE) { + return isTeamOrServices ? teamMembers.sort(sortUsersByPriority) : teamUsers; + } + return teamMembers.sort(sortUsersByPriority).filter(u => u.type === UserType.APP); } return connectedUsers; - }, [connectedUsers, isServicesRoom, isTeam, isTeamOnly, teamMembers, teamUsers]); + }, [connectedUsers, isServicesRoom, isTeam, isTeamOnly, teamMembers, teamUsers, currentState]); const enabledAddAction = selectedContacts.length > ENABLE_ADD_ACTIONS_LENGTH; @@ -134,14 +137,7 @@ const AddParticipants: FC = ({ const isService = !!firstUserEntity?.isService; const allowIntegrations = isGroupOrChannel || isService; - return ( - isTeam && - allowIntegrations && - inTeam && - !isTeamOnly && - isServicesEnabled && - activeConversation.protocol !== CONVERSATION_PROTOCOL.MLS - ); + return isTeam && allowIntegrations && inTeam && !isTeamOnly && isServicesEnabled; }, [ firstUserEntity?.isService, inTeam, @@ -155,19 +151,24 @@ const AddParticipants: FC = ({ const manageServicesUrl = getManageServicesUrl('client_landing'); const isSearching = searchInput.length > ENABLE_IS_SEARCHING_LENGTH; - const onAddPeople = () => setCurrentState(PARTICIPANTS_STATE.ADD_PEOPLE); - - const searchServices = async (value: string) => { - await integrationRepository.searchForServices(value); - setIsInitialServiceSearch(false); + const onAddPeople = () => { + setSelectedContacts([]); + setCurrentState(PARTICIPANTS_STATE.ADD_PEOPLE); }; const onAddServices = async () => { + setSelectedContacts([]); setCurrentState(PARTICIPANTS_STATE.ADD_SERVICE); await searchServices(searchInput); }; + const searchServices = async (value: string) => { + await integrationRepository.searchForServices(value); + + setIsInitialServiceSearch(false); + }; + const openManageServices = () => { if (manageServicesUrl) { safeWindowOpen(manageServicesUrl); @@ -179,6 +180,8 @@ const AddParticipants: FC = ({ const addUsers = async () => { const userEntities = selectedContacts.slice(); + userEntities.forEach(ue => console.log(`userEntity ${JSON.stringify(ue)}`)); + await conversationRepository.addUsers(activeConversation, userEntities); }; @@ -254,104 +257,101 @@ const AddParticipants: FC = ({ )} - {isAddPeopleState && ( - - )} - - {isAddServiceState && ( - <> - {!!services.length && ( - <> - {canManageServices() && !!manageServicesUrl && ( -
    -
  • - handleKeyDown({ - event, - callback: openManageServices, - keys: [KEY.ENTER, KEY.SPACE], - }) - } - data-uie-name="go-manage-services" - > -
    - -
    - -
    {t('addParticipantsManageServices')}
    -
  • -
- )} - - - - )} - - {!services.length && !isInitialServiceSearch && ( -
- - - {canManageServices() && !!manageServicesUrl && ( - <> -
- {t('addParticipantsNoServicesManager')} -
- - - - )} - - {!canManageServices() && ( -
- {t('addParticipantsNoServicesMember')} -
- )} -
- )} - - )} + + + {/*{isAddServiceState && (*/} + {/* <>*/} + {/* {!!services.length && (*/} + {/* <>*/} + {/* {canManageServices() && !!manageServicesUrl && (*/} + {/*
    */} + {/* */} + {/* handleKeyDown({*/} + {/* event,*/} + {/* callback: openManageServices,*/} + {/* keys: [KEY.ENTER, KEY.SPACE],*/} + {/* })*/} + {/* }*/} + {/* data-uie-name="go-manage-services"*/} + {/* >*/} + {/*
    */} + {/* */} + {/*
    */} + + {/*
    {t('addParticipantsManageServices')}
    */} + {/* */} + {/*
*/} + {/* )}*/} + + {/* */} + {/* */} + {/* )}*/} + + {/* {!services.length && !isInitialServiceSearch && (*/} + {/*
*/} + {/* */} + + {/* {canManageServices() && !!manageServicesUrl && (*/} + {/* <>*/} + {/*
*/} + {/* {t('addParticipantsNoServicesManager')}*/} + {/*
*/} + + {/* */} + {/* handleKeyDown({*/} + {/* event,*/} + {/* callback: openManageServices,*/} + {/* keys: [KEY.ENTER, KEY.SPACE],*/} + {/* })*/} + {/* }*/} + {/* data-uie-name="go-enable-services"*/} + {/* style={{marginTop: '1em'}}*/} + {/* >*/} + {/* {t('addParticipantsManageServicesNoResults')}*/} + {/* */} + {/* */} + {/* )}*/} + + {/* {!canManageServices() && (*/} + {/*
*/} + {/* {t('addParticipantsNoServicesMember')}*/} + {/*
*/} + {/* )}*/} + {/*
*/} + {/* )}*/} + {/* */} + {/*)}*/}
- {isAddPeopleState && ( -
- -
- )} +
+ +
); diff --git a/apps/webapp/src/script/page/RightSidebar/ConversationDetails/components/ConversationDetailsOptions/ConversationDetailsOptions.tsx b/apps/webapp/src/script/page/RightSidebar/ConversationDetails/components/ConversationDetailsOptions/ConversationDetailsOptions.tsx index 28122eb54f3..27ae3639a38 100644 --- a/apps/webapp/src/script/page/RightSidebar/ConversationDetails/components/ConversationDetailsOptions/ConversationDetailsOptions.tsx +++ b/apps/webapp/src/script/page/RightSidebar/ConversationDetails/components/ConversationDetailsOptions/ConversationDetailsOptions.tsx @@ -29,7 +29,7 @@ import {PanelActions} from 'Components/panel/PanelActions'; import {ReceiptModeToggle} from 'Components/toggle/ReceiptModeToggle'; import {ConversationRepository} from 'Repositories/conversation/ConversationRepository'; import {ConversationRoleRepository} from 'Repositories/conversation/ConversationRoleRepository'; -import {isGroupMLSConversation, isMLSConversation} from 'Repositories/conversation/ConversationSelectors'; +import {isGroupMLSConversation} from 'Repositories/conversation/ConversationSelectors'; import {Conversation} from 'Repositories/entity/Conversation'; import {User} from 'Repositories/entity/User'; import {TeamState} from 'Repositories/team/TeamState'; @@ -126,7 +126,8 @@ const ConversationDetailsOptions = ({ const showOptionGuests = isActiveGroupParticipant && isTeamConversation; const showOptionNotificationsGroup = isMutable && isGroupOrChannel; const showOptionTimedMessages = isActiveGroupParticipant && isSelfDeletingMessagesEnabled; - const showOptionServices = isActiveGroupParticipant && isTeamConversation && !isMLSConversation(activeConversation); + //TODO: Actually check for apps and services availability + const showOptionServices = isActiveGroupParticipant && isTeamConversation; // && !isMLSConversation(activeConversation); const showOptionNotifications1To1 = isMutable && !isGroupOrChannel; const showOptionReadReceipts = isTeamConversation && !isGroupMLSConversation(activeConversation); const showChannelOptions = isChannel && isChannelsEnabled; diff --git a/apps/webapp/src/script/repositories/entity/User/User.ts b/apps/webapp/src/script/repositories/entity/User/User.ts index 52e46804c8d..d08c2cf615c 100644 --- a/apps/webapp/src/script/repositories/entity/User/User.ts +++ b/apps/webapp/src/script/repositories/entity/User/User.ts @@ -19,7 +19,7 @@ import {ConnectionStatus} from '@wireapp/api-client/lib/connection/'; import {CONVERSATION_PROTOCOL} from '@wireapp/api-client/lib/team'; -import {QualifiedId} from '@wireapp/api-client/lib/user'; +import {QualifiedId, UserType} from '@wireapp/api-client/lib/user'; import {amplify} from 'amplify'; import ko from 'knockout'; @@ -104,6 +104,7 @@ export class User { public domain: string; public readonly isBlockedLegalHold: ko.PureComputed; public readonly supportedProtocols: ko.Observable; + public type: UserType | undefined; public static get ACCENT_COLOR() { return { diff --git a/apps/webapp/src/script/repositories/user/UserMapper.ts b/apps/webapp/src/script/repositories/user/UserMapper.ts index 399ebba7fb6..f1e78b704f7 100644 --- a/apps/webapp/src/script/repositories/user/UserMapper.ts +++ b/apps/webapp/src/script/repositories/user/UserMapper.ts @@ -108,6 +108,7 @@ export class UserMapper { service, team: teamId, supported_protocols: supportedProtocols, + type, } = userData; if (accentId) { @@ -181,6 +182,8 @@ export class UserMapper { userEntity.isDeleted = true; } + userEntity.type = type; + return userEntity; } } diff --git a/libraries/api-client/src/user/user.ts b/libraries/api-client/src/user/user.ts index 88cb4fc0d53..8858f9f3d73 100644 --- a/libraries/api-client/src/user/user.ts +++ b/libraries/api-client/src/user/user.ts @@ -26,6 +26,12 @@ import {Picture} from '../self/'; import {CONVERSATION_PROTOCOL} from '../team'; import {UserAsset} from '../user/'; +export enum UserType { + REGULAR = 'regular', + APP = 'app', + BOT = 'bot', +} + export interface User { accent_id?: AccentColor.AccentColorID; assets?: UserAsset[]; @@ -42,4 +48,5 @@ export interface User { team?: string; searchable?: boolean; supported_protocols?: CONVERSATION_PROTOCOL[]; + type: UserType; } diff --git a/libraries/core/src/conversation/ConversationService/ConversationService.ts b/libraries/core/src/conversation/ConversationService/ConversationService.ts index 93e8d8c1283..1ed5c80ba52 100644 --- a/libraries/core/src/conversation/ConversationService/ConversationService.ts +++ b/libraries/core/src/conversation/ConversationService/ConversationService.ts @@ -487,6 +487,9 @@ export class ConversationService extends TypedEventEmitter { groupId, conversationId, }: Required & {shouldRetry?: boolean}): Promise { + console.log( + `addUsersToMLSConversation(qualifiedUsers=${JSON.stringify(qualifiedUsers)}, groupId=${groupId}, conversationId=${conversationId})`, + ); return this.MLSRecoveryOrchestrator.execute({ context: {operationName: OperationName.addUsers, qualifiedConversationId: conversationId, groupId}, callBack: () => this.performAddUsersToMLSConversationAPI({qualifiedUsers, groupId, conversationId}), From fdc9948e7d9a50258204d21759672cc35f4c2704 Mon Sep 17 00:00:00 2001 From: Paul Date: Wed, 11 Mar 2026 10:33:40 +0100 Subject: [PATCH 09/12] introduced flag enablement of services in teamState --- .../page/LeftSidebar/panels/StartUI/StartUI.tsx | 8 +------- .../src/script/repositories/team/TeamState.ts | 17 ++++++++++++++--- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/apps/webapp/src/script/page/LeftSidebar/panels/StartUI/StartUI.tsx b/apps/webapp/src/script/page/LeftSidebar/panels/StartUI/StartUI.tsx index 703264db6c9..314f090c2b0 100644 --- a/apps/webapp/src/script/page/LeftSidebar/panels/StartUI/StartUI.tsx +++ b/apps/webapp/src/script/page/LeftSidebar/panels/StartUI/StartUI.tsx @@ -19,7 +19,6 @@ import {useEffect, useRef, useState} from 'react'; -import {CONVERSATION_PROTOCOL} from '@wireapp/api-client/lib/team'; import cx from 'classnames'; import {container} from 'tsyringe'; @@ -90,14 +89,9 @@ const StartUI = ({ const actions = mainViewModel.actions; const isTeam = teamState.isTeam(); - const appsEnabled = teamState?.isAppsEnabled(); - const defaultProtocol = teamState.teamFeatures()?.mls?.config.defaultProtocol; - const isProteus = defaultProtocol !== CONVERSATION_PROTOCOL.MLS; const showServiceTab = - isTeam && - canChatWithServices() && - ((isProteus && teamState?.hasWhitelistedServices()) || (!isProteus && appsEnabled)); + isTeam && canChatWithServices() && (teamState?.isLegacyServicesEnabled() || teamState?.isAppsEnabled()); const [searchQuery, setSearchQuery] = useState(''); const [activeTab, setActiveTab] = useState(Tabs.PEOPLE); diff --git a/apps/webapp/src/script/repositories/team/TeamState.ts b/apps/webapp/src/script/repositories/team/TeamState.ts index a76fbe1c4a8..bbb1d7ca40d 100644 --- a/apps/webapp/src/script/repositories/team/TeamState.ts +++ b/apps/webapp/src/script/repositories/team/TeamState.ts @@ -18,8 +18,8 @@ */ import {Backend} from '@wireapp/api-client/lib/env'; -import {Role} from '@wireapp/api-client/lib/team'; -import {FeatureList, FEATURE_STATUS, SELF_DELETING_TIMEOUT} from '@wireapp/api-client/lib/team/feature/'; +import {CONVERSATION_PROTOCOL, Role} from '@wireapp/api-client/lib/team'; +import {FEATURE_STATUS, FeatureList, SELF_DELETING_TIMEOUT} from '@wireapp/api-client/lib/team/feature/'; import ko from 'knockout'; import {container, singleton} from 'tsyringe'; @@ -64,6 +64,7 @@ export class TeamState { readonly isCellsEnabled: ko.PureComputed; readonly isAuditLogEnabled: ko.PureComputed; readonly isAppsEnabled: ko.PureComputed; + readonly isLegacyServicesEnabled: ko.PureComputed; constructor(private readonly userState = container.resolve(UserState)) { this.isTeam = ko.pureComputed(() => !!this.team()?.id); @@ -147,7 +148,17 @@ export class TeamState { }); this.isAppsEnabled = ko.pureComputed(() => { - return this.teamFeatures()?.apps?.status === FEATURE_STATUS.ENABLED; + return ( + this.teamFeatures()?.apps?.status === FEATURE_STATUS.ENABLED && + this.teamFeatures().mls?.config?.defaultProtocol === CONVERSATION_PROTOCOL.MLS + ); + }); + + this.isLegacyServicesEnabled = ko.pureComputed(() => { + return ( + this.hasWhitelistedServices() && + this.teamFeatures().mls?.config?.defaultProtocol === CONVERSATION_PROTOCOL.PROTEUS + ); }); this.isAuditLogEnabled = ko.pureComputed(() => { From 5e41e5ded4700f543ec72d721f3425940f03f41b Mon Sep 17 00:00:00 2001 From: Paul Date: Tue, 17 Mar 2026 09:54:35 +0100 Subject: [PATCH 10/12] test: add AddParticipants.test.tsx --- .../page/RightSidebar/AddParticipants/AddParticipants.test.tsx | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 apps/webapp/src/script/page/RightSidebar/AddParticipants/AddParticipants.test.tsx diff --git a/apps/webapp/src/script/page/RightSidebar/AddParticipants/AddParticipants.test.tsx b/apps/webapp/src/script/page/RightSidebar/AddParticipants/AddParticipants.test.tsx new file mode 100644 index 00000000000..e69de29bb2d From aced5f708ede704904b6059b42178a2e8fab14a7 Mon Sep 17 00:00:00 2001 From: Paul Date: Wed, 25 Mar 2026 12:23:01 +0100 Subject: [PATCH 11/12] test: add AddParticipants.test.tsx --- .../components/UserSearchableList/UserSearchableList.tsx | 2 +- .../RightSidebar/AddParticipants/AddParticipants.test.tsx | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/apps/webapp/src/script/components/UserSearchableList/UserSearchableList.tsx b/apps/webapp/src/script/components/UserSearchableList/UserSearchableList.tsx index 736370d3b92..d21b0b7dbd9 100644 --- a/apps/webapp/src/script/components/UserSearchableList/UserSearchableList.tsx +++ b/apps/webapp/src/script/components/UserSearchableList/UserSearchableList.tsx @@ -67,7 +67,7 @@ export const UserSearchableList = ({ selfUser, users, teamState = container.resolve(TeamState), - userType, + userType = UserType.REGULAR, ...props }: UserListProps) => { const {searchRepository, teamRepository, selfFirst, ...userListProps} = props; diff --git a/apps/webapp/src/script/page/RightSidebar/AddParticipants/AddParticipants.test.tsx b/apps/webapp/src/script/page/RightSidebar/AddParticipants/AddParticipants.test.tsx index e69de29bb2d..191629a8be3 100644 --- a/apps/webapp/src/script/page/RightSidebar/AddParticipants/AddParticipants.test.tsx +++ b/apps/webapp/src/script/page/RightSidebar/AddParticipants/AddParticipants.test.tsx @@ -0,0 +1,5 @@ +describe('AddParticipants', () => { + it('doesn`t render apps tab when apps are deactivated', () => { + + }); +}); From 4809698add7776fd80171d67a5ddf10d0de60101 Mon Sep 17 00:00:00 2001 From: Paul Date: Wed, 25 Mar 2026 12:49:17 +0100 Subject: [PATCH 12/12] fix: selecting apps in UserList displays checkmark --- .../components/UserSearchableList/UserSearchableList.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/apps/webapp/src/script/components/UserSearchableList/UserSearchableList.tsx b/apps/webapp/src/script/components/UserSearchableList/UserSearchableList.tsx index d21b0b7dbd9..4678a81158a 100644 --- a/apps/webapp/src/script/components/UserSearchableList/UserSearchableList.tsx +++ b/apps/webapp/src/script/components/UserSearchableList/UserSearchableList.tsx @@ -53,7 +53,7 @@ export type UserListProps = React.ComponentProps & { /** will do an extra request to the server when user types in (otherwise will only lookup given local users) */ allowRemoteSearch?: boolean; filterRemoteTeamUsers?: boolean; - userType: UserType; + userType?: UserType; }; export const UserSearchableList = ({ @@ -76,9 +76,7 @@ export const UserSearchableList = ({ const [filteredUsers, setFilteredUsers] = useState([]); const [remoteTeamMembers, setRemoteTeamMembers] = useState([]); - const filteredSelectedUsers = selectedUsers - ? searchRepository.searchUserInSet(filter, selectedUsers).filter(u => u.type === UserType.REGULAR) - : undefined; + const filteredSelectedUsers = selectedUsers ? searchRepository.searchUserInSet(filter, selectedUsers) : undefined; const selfInTeam = teamState.isInTeam(selfUser);