Skip to content
Open
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: 9 additions & 0 deletions apps/webapp/src/i18n/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -1843,6 +1850,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.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,15 @@
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';
import {Config} from 'src/script/Config';
import {useKoSubscribableChildren} from 'Util/ComponentUtil';
import {t} from 'Util/LocalizerUtil';

import {useCreateConversationModal} from '../hooks/useCreateConversationModal';
import {ConversationType} from '../types';

export const Preference = () => {
const {
Expand All @@ -44,9 +45,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;
Expand All @@ -55,7 +63,13 @@ 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 &&
conversationType !== ConversationType.Channel);

// Read receipts are temporarily disabled for MLS groups and channels until it is supported
const areReadReceiptsEnabled = defaultProtocol !== CONVERSATION_PROTOCOL.MLS;

return (
Expand All @@ -70,17 +84,17 @@ export const Preference = () => {
dataUieName="read-receipts"
/>

{conversationType === ConversationType.Group && (
<InfoToggle
className="modal-style"
dataUieName="services"
info={t('servicesRoomToggleInfoExtended')}
setIsChecked={setIsServicesEnabled}
isDisabled={false}
name={t('servicesOptionsTitle')}
isChecked={isServicesEnabled}
/>
)}
<InfoToggle
className="modal-style"
dataUieName="services"
info={t('servicesRoomToggleInfoExtended')}
setIsChecked={setIsServicesEnabled}
isDisabled={!isAppsFeatureAvailable}
name={t('servicesOptionsTitle')}
isChecked={isServicesEnabled && isAppsFeatureAvailable}
label={!isAppsFeatureAvailable && <AppsDisabledNote />}
/>

{areReadReceiptsEnabled && (
<InfoToggle
className="modal-style"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {WebAppEvents} from '@wireapp/webapp-events';
import {FadingScrollbar} from 'Components/FadingScrollbar';
import * as Icon from 'Components/Icon';
import {ModalComponent} from 'Components/Modals/ModalComponent';
import {AppsDisabledNote} from 'Components/Note/AppsDisabledNote/AppsDisabledNote';
import {SearchInput} from 'Components/SearchInput';
import {TextInput} from 'Components/TextInput';
import {InfoToggle} from 'Components/toggle/InfoToggle';
Expand Down Expand Up @@ -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']);

Expand Down Expand Up @@ -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));
Comment on lines +161 to +164
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have this logic in two places, it is possible to keep it in a single places? A hook or reusable function?


const isServicesEnabled = isAppsFeatureAvailable && (isServicesRoom || isGuestAndServicesRoom);

const {setCurrentTab: setCurrentSidebarTab} = useSidebarStore();

Expand Down Expand Up @@ -504,17 +513,18 @@ const GroupCreationModal = ({
name={t('guestOptionsTitle')}
info={t('guestRoomToggleInfo')}
/>
{selectedProtocol.value !== CONVERSATION_PROTOCOL.MLS && (
<InfoToggle
className="modal-style"
dataUieName="services"
isChecked={isServicesEnabled}
setIsChecked={clickOnToggleServicesMode}
isDisabled={false}
name={t('servicesOptionsTitle')}
info={t('servicesRoomToggleInfo')}
/>
)}
<InfoToggle
className="modal-style"
dataUieName="services"
isChecked={isServicesEnabled}
setIsChecked={clickOnToggleServicesMode}
isDisabled={!isAppsFeatureAvailable}
name={t('servicesOptionsTitle')}
info={t('servicesRoomToggleInfo')}
/>

{!isAppsFeatureAvailable && <AppsDisabledNote />}

{areReadReceiptsEnabled && (
<InfoToggle
className="modal-style"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* 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/.
*
*/

import {Note} from 'Components/Note/Note';
import {t} from 'Util/LocalizerUtil';

const AppsDisabledNote = () => {
return (
<Note title={t('servicesNotEnabledNoteTitle')}>
<span className={'subline'}>{t('servicesNotEnabledBody')}</span>
</Note>
);
};

export {AppsDisabledNote};
49 changes: 49 additions & 0 deletions apps/webapp/src/script/components/Note/Note.styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* 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/.
*
*/

import {CSSObject} from '@emotion/react';

export const ContainerStyle: CSSObject = {
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',
lineHeight: '1.5',
marginTop: '0.3rem',
};

export const HeaderStyle: CSSObject = {
display: 'flex',
alignItems: 'center',
fontWeight: 'var(--font-weight-semibold)',
gap: '0.5rem',
};

export const ContentStyle: CSSObject = {
display: 'flex',
flexDirection: 'column',
gap: '4px',
};
45 changes: 45 additions & 0 deletions apps/webapp/src/script/components/Note/Note.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* 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/.
*
*/

import {ReactNode} from 'react';

import {InfoIcon} from 'Components/Icon';
import {ContainerStyle, ContentStyle, HeaderStyle} from 'Components/Note/Note.styles';

interface NoteProps {
title: string;
children?: ReactNode;
}

const Note = ({title, children}: NoteProps) => {
return (
<div css={ContainerStyle}>
<div css={HeaderStyle}>
<InfoIcon />
<span className="heading-h4">{title}</span>
</div>

<div css={ContentStyle}>
<div>{children}</div>
</div>
</div>
);
};

export {Note};
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -53,6 +53,7 @@ export type UserListProps = React.ComponentProps<typeof UserList> & {
/** 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 = ({
Expand All @@ -66,6 +67,7 @@ export const UserSearchableList = ({
selfUser,
users,
teamState = container.resolve(TeamState),
userType,
...props
}: UserListProps) => {
const {searchRepository, teamRepository, selfFirst, ...userListProps} = props;
Expand All @@ -74,7 +76,9 @@ export const UserSearchableList = ({
const [filteredUsers, setFilteredUsers] = useState<User[]>([]);
const [remoteTeamMembers, setRemoteTeamMembers] = useState<User[]>([]);

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);

Expand All @@ -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));

Expand All @@ -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) {
Expand Down
5 changes: 4 additions & 1 deletion apps/webapp/src/script/components/toggle/InfoToggle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
*
*/

import {useId} from 'react';
import React, {useId} from 'react';

import cx from 'classnames';

Expand All @@ -29,6 +29,7 @@ interface InfoToggleProps {
name: string;
className?: string;
setIsChecked: (checked: boolean) => void;
label?: React.ReactNode;
}

const InfoToggle = ({
Expand All @@ -39,6 +40,7 @@ const InfoToggle = ({
isDisabled,
name,
setIsChecked,
label,
}: InfoToggleProps) => {
const dataUieNameInfoText = `status-info-toggle-${dataUieName}`;
const dataUieNameLabelText = `do-toggle-${dataUieName}`;
Expand Down Expand Up @@ -77,6 +79,7 @@ const InfoToggle = ({
</button>
</div>
</div>
{label}
</div>
);
};
Expand Down
Loading
Loading