diff --git a/apps/webapp/src/i18n/en-US.json b/apps/webapp/src/i18n/en-US.json index a31fe29da77..f8336c1a566 100644 --- a/apps/webapp/src/i18n/en-US.json +++ b/apps/webapp/src/i18n/en-US.json @@ -57,6 +57,7 @@ "E2EI.certificateDetails": "Certificate details (PEM format)", "E2EI.certificateExpired": "End-to-end identity certificate expired", "E2EI.certificateExpiresSoon": "End-to-end identity certificate expires soon", + "E2EI.certificateNotActivated": "End-to-end identity certificate not activated", "E2EI.certificateNotDownloaded": "End-to-end identity certificate not downloaded", "E2EI.certificateRevoked": "End-to-end identity certificate revoked", "E2EI.certificateTitle": "End-to-end identity certificate", diff --git a/apps/webapp/src/script/components/badge/components/verificationBadges/verificationBadges.test.tsx b/apps/webapp/src/script/components/badge/components/verificationBadges/verificationBadges.test.tsx index 197852839df..ac6d8e8aab0 100644 --- a/apps/webapp/src/script/components/badge/components/verificationBadges/verificationBadges.test.tsx +++ b/apps/webapp/src/script/components/badge/components/verificationBadges/verificationBadges.test.tsx @@ -18,6 +18,7 @@ */ import {render} from '@testing-library/react'; +import {Maybe} from 'true-myth'; import {withTheme} from 'src/script/auth/util/test/testUtil'; import {MLSStatuses} from 'src/script/e2eIdentity'; @@ -26,7 +27,7 @@ import { createRootProviderWrapperForTest, } from 'src/script/page/testSupport/rootContextTestSupport'; -import {VerificationBadges} from './verificationBadges'; +import {VerificationBadges, getUserVerificationBadgeLabel} from './verificationBadges'; import {translateForTest} from 'Util/test/translateForTest'; const rootContextValue = createRootContextValueForTest({translate: translateForTest}); @@ -92,3 +93,37 @@ describe('VerificationBadges', () => { expect(E2EIdentityStatus.getAttribute('data-uie-value')).toEqual(MLSStatuses.EXPIRES_SOON); }); }); + +describe('getUserVerificationBadgeLabel', () => { + it('returns MLS verified label for VALID status', () => { + const label = getUserVerificationBadgeLabel(translateForTest, { + mlsStatus: Maybe.just(MLSStatuses.VALID), + isProteusVerified: false, + }); + expect(label).toBe(translateForTest('E2EI.userDevicesVerified')); + }); + + it('returns Proteus device verified label when isProteusVerified is true', () => { + const label = getUserVerificationBadgeLabel(translateForTest, { + mlsStatus: Maybe.nothing(), + isProteusVerified: true, + }); + expect(label).toBe(translateForTest('proteusDeviceVerified')); + }); + + it('returns composed labels when both MLS and Proteus are verified', () => { + const label = getUserVerificationBadgeLabel(translateForTest, { + mlsStatus: Maybe.just(MLSStatuses.VALID), + isProteusVerified: true, + }); + expect(label).toBe(`${translateForTest('E2EI.userDevicesVerified')}, ${translateForTest('proteusDeviceVerified')}`); + }); + + it('returns undefined when neither MLS nor Proteus is verified', () => { + const label = getUserVerificationBadgeLabel(translateForTest, { + mlsStatus: Maybe.nothing(), + isProteusVerified: false, + }); + expect(label).toBeUndefined(); + }); +}); diff --git a/apps/webapp/src/script/components/badge/components/verificationBadges/verificationBadges.tsx b/apps/webapp/src/script/components/badge/components/verificationBadges/verificationBadges.tsx index e9425d70438..12274bbe54c 100644 --- a/apps/webapp/src/script/components/badge/components/verificationBadges/verificationBadges.tsx +++ b/apps/webapp/src/script/components/badge/components/verificationBadges/verificationBadges.tsx @@ -23,6 +23,7 @@ import {CSSObject} from '@emotion/react'; import {CONVERSATION_PROTOCOL} from '@wireapp/api-client/lib/team'; import {stringifyQualifiedId} from '@wireapp/core/lib/util/qualifiedIdUtil'; import {container} from 'tsyringe'; +import {Maybe} from 'true-myth'; import { TabIndex, @@ -98,7 +99,7 @@ const getMLSStatuses = ({identities, user}: {identities?: WireIdentity[]; user?: }); }; -export const UserVerificationBadges = ({ +export const useUserVerificationStatus = ({ user, groupId, isSelfUser, @@ -115,12 +116,57 @@ export const UserVerificationBadges = ({ user, }); - let status: MLSStatuses | undefined = undefined; - if (mlsStatuses && mlsStatuses.length > 0 && mlsStatuses.every(status => status === MLSStatuses.VALID)) { - status = MLSStatuses.VALID; + const mlsStatus = Maybe.of( + mlsStatuses && mlsStatuses.length > 0 && mlsStatuses.every(status => status === MLSStatuses.VALID) + ? MLSStatuses.VALID + : undefined, + ); + + return {mlsStatus, isProteusVerified}; +}; + +export const getUserVerificationBadgeLabel = ( + translate: RootContextValue['translate'], + {mlsStatus, isProteusVerified}: {mlsStatus: Maybe; isProteusVerified: boolean}, +): string | undefined => { + const labels: string[] = []; + + mlsStatus.map(() => labels.push(translate('E2EI.userDevicesVerified'))); + + if (isProteusVerified) { + labels.push(translate('proteusDeviceVerified')); } - return ; + return labels.length > 0 ? labels.join(', ') : undefined; +}; + +export const UserVerificationBadgesContent = ({ + mlsStatus, + isProteusVerified, +}: { + mlsStatus: Maybe; + isProteusVerified: boolean; +}) => { + return ( + + ); +}; + +export const UserVerificationBadges = ({ + user, + groupId, + isSelfUser, +}: { + user: User; + groupId?: string; + isSelfUser?: boolean; +}) => { + const {mlsStatus, isProteusVerified} = useUserVerificationStatus({user, groupId, isSelfUser}); + return ; }; export const DeviceVerificationBadges = ({ diff --git a/apps/webapp/src/script/page/leftSidebar/userDetails/userDetails.tsx b/apps/webapp/src/script/page/leftSidebar/userDetails/userDetails.tsx index e490fe5262a..1b1be37e6bc 100644 --- a/apps/webapp/src/script/page/leftSidebar/userDetails/userDetails.tsx +++ b/apps/webapp/src/script/page/leftSidebar/userDetails/userDetails.tsx @@ -19,12 +19,18 @@ import {memo} from 'react'; +import is from '@sindresorhus/is'; import cx from 'classnames'; +import {Availability} from '@wireapp/protocol-messaging'; import {TabIndex} from '@wireapp/react-ui-kit'; import {Avatar, AVATAR_SIZE} from 'Components/avatar'; -import {UserVerificationBadges} from 'Components/badge'; +import { + getUserVerificationBadgeLabel, + useUserVerificationStatus, + UserVerificationBadgesContent, +} from 'Components/badge'; import {LegalHoldDot} from 'Components/LegalHoldDot'; import {User} from 'Repositories/entity/User'; import {useApplicationContext} from 'src/script/page/rootProvider'; @@ -37,18 +43,38 @@ import {AvailabilityContextMenu} from '../../../ui/availabilityContextMenu'; interface AvailabilityStateButtonWrapperProps { children: React.ReactElement; isTeam: boolean; + ariaLabel: string; showAvailabilityContextMenu: (event: MouseEvent) => void; } +const isAvailabilityType = (value: unknown): value is Availability.Type => + Object.values(Availability.Type).some(availabilityType => availabilityType === value); + +const getAvailabilityTranslationKey = (availability: Availability.Type) => { + switch (availability) { + case Availability.Type.AVAILABLE: + return 'availability.available'; + case Availability.Type.BUSY: + return 'availability.busy'; + case Availability.Type.AWAY: + return 'availability.away'; + case Availability.Type.NONE: + return 'availability.none'; + } +}; + const AvailabilityStateButtonWrapper = ({ children, isTeam = false, + ariaLabel, showAvailabilityContextMenu, }: AvailabilityStateButtonWrapperProps) => { return isTeam ? ( @@ -69,9 +95,11 @@ const UserDetailsComponent = ({user, isTeam = false, groupId, isSideBarOpen = fa const { name: userName, username: userHandle, + availability, isOnLegalHold, hasPendingLegalHold, - } = useKoSubscribableChildren(user, ['hasPendingLegalHold', 'isOnLegalHold', 'name', 'username']); + } = useKoSubscribableChildren(user, ['availability', 'hasPendingLegalHold', 'isOnLegalHold', 'name', 'username']); + const verificationStatus = useUserVerificationStatus({user, groupId, isSelfUser: isTeam}); const showLegalHold = isOnLegalHold || hasPendingLegalHold; @@ -84,14 +112,27 @@ const UserDetailsComponent = ({user, isTeam = false, groupId, isSideBarOpen = fa }); }; + const avatarAriaLabel = [ + userName, + userHandle, + isTeam && isAvailabilityType(availability) ? translate(getAvailabilityTranslationKey(availability)) : undefined, + getUserVerificationBadgeLabel(translate, verificationStatus), + ] + .filter((label): label is string => is.nonEmptyString(label)) + .join(', '); + return (
- + @@ -99,13 +140,17 @@ const UserDetailsComponent = ({user, isTeam = false, groupId, isSideBarOpen = fa {isTeam ? ( <>
- - +
{showLegalHold && (