Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
1 change: 1 addition & 0 deletions src/i18n/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,7 @@
"callAccept": "Accept",
"callChooseSharedScreen": "Choose a screen to share",
"callChooseSharedWindow": "Choose a window to share",
"callChooseScreenCancel": "Close-screen sharing",
"callConversationAcceptOrDecline": "{conversationName} is calling. Press control + enter to accept the call or press control + shift + enter to decline the call.",
"callDecline": "Decline",
"callDegradationAction": "OK",
Expand Down
2 changes: 2 additions & 0 deletions src/script/components/calling/ChooseScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@ function ChooseScreen({choose, callState = container.resolve(CallState)}: Choose
className="choose-screen-controls-button button-round button-round-dark button-round-md icon-close"
data-uie-name="do-choose-screen-cancel"
onClick={cancel}
aria-label={t('callChooseScreenCancel')}
title={t('callChooseScreenCancel')}
></button>
</div>
</div>
Expand Down
99 changes: 99 additions & 0 deletions src/script/components/calling/GroupVideoGridTile.styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/*
* Wire
* Copyright (C) 2025 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';

const participantNameColor = (isActivelySpeaking: boolean, isAudioEstablished: boolean) => {
if (!isAudioEstablished) {
return 'var(--gray-60)';
}
if (isActivelySpeaking) {
return 'var(--app-bg-secondary)';
}
return 'var(--white)';
};

export const activelySpeakingBoxShadow = `inset 0px 0px 0px 1px var(--group-video-bg), inset 0px 0px 0px 4px var(--accent-color), inset 0px 0px 0px 7px var(--app-bg-secondary)`;

export const groupVideoBoxShadow = (participantCount: number): string =>
participantCount > 1 ? 'inset 0px 0px 0px 2px var(--group-video-bg)' : 'initial';

export const groupVideoTileWrapper: CSSObject = {
alignItems: 'center',
backgroundColor: 'var(--group-video-tile-bg)',
borderRadius: '10px',
display: 'flex',
height: '100%',
justifyContent: 'center',
width: '100%',
};

export const groupVideoActiveSpeakerTile = (isActivelySpeaking: boolean, participantCount: number): CSSObject => ({
borderRadius: '8px',
bottom: 0,
boxShadow: isActivelySpeaking ? activelySpeakingBoxShadow : groupVideoBoxShadow(participantCount),
left: 0,
position: 'absolute',
right: 0,
top: 0,
transition: 'box-shadow 0.3s ease-in-out',
});

export const groupVideoActiveSpeaker = (isActivelySpeaking: boolean): CSSObject => ({
filter: isActivelySpeaking ? 'brightness(0.8)' : 'none',
});

export const groupVideoParticipantNameWrapper = (
isActivelySpeaking: boolean,
isAudioEstablished: boolean,
): CSSObject => ({
overflow: 'hidden',
display: 'flex',
color: participantNameColor(isActivelySpeaking, isAudioEstablished),
});

export const groupVideoParticipantName: CSSObject = {
textOverflow: 'ellipsis',
overflow: 'hidden',
};

export const groupVideoParticipantAudioStatus = (
isActivelySpeaking: boolean,
isAudioEstablished: boolean,
): CSSObject => {
const color = participantNameColor(isActivelySpeaking, isAudioEstablished);
return {
color: 'var(--participant-audio-connecting-color)',
flexShrink: 0,
'&::before': {
content: "' • '",
whiteSpace: 'pre',
color,
},
};
};

export const groupVideoElementVideo = (fitContain: boolean, mirrorSelf: boolean): CSSObject => ({
objectFit: fitContain ? 'contain' : 'cover',
transform: mirrorSelf ? 'rotateY(180deg)' : 'initial',
});

export const groupVideoPauseOverlayLabel = (minimized: boolean): CSSObject => ({
fontSize: minimized ? '0.6875rem' : '0.875rem',
});
105 changes: 26 additions & 79 deletions src/script/components/calling/GroupVideoGridTile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,16 @@
import {TabIndex} from '@wireapp/react-ui-kit';

import {Avatar, AVATAR_SIZE} from 'Components/Avatar';
import {
groupVideoActiveSpeaker,
groupVideoActiveSpeakerTile,
groupVideoElementVideo,
groupVideoParticipantAudioStatus,
groupVideoParticipantName,
groupVideoParticipantNameWrapper,
groupVideoPauseOverlayLabel,
groupVideoTileWrapper,
} from 'Components/calling/GroupVideoGridTile.styles';
import * as Icon from 'Components/Icon';
import type {Participant} from 'Repositories/calling/Participant';
import {useKoSubscribableChildren} from 'Util/ComponentUtil';
Expand All @@ -42,24 +52,6 @@
selfParticipant: Participant;
}

const getParticipantNameColor = ({
isActivelySpeaking,
isAudioEstablished,
}: {
isActivelySpeaking: boolean;
isAudioEstablished: boolean;
}) => {
if (!isAudioEstablished) {
return 'var(--gray-60)';
}

if (isActivelySpeaking) {
return 'var(--app-bg-secondary)';
}

return 'var(--white)';
};

const GroupVideoGridTile = ({
minimized,
participant,
Expand Down Expand Up @@ -95,8 +87,6 @@
const hasPausedVideo = videoState === VIDEO_STATE.PAUSED;
const doVideoReconnecting = videoState === VIDEO_STATE.RECONNECTING;
const hasActiveVideo = (sharesCamera || sharesScreen) && !!videoStream;
const activelySpeakingBoxShadow = `inset 0px 0px 0px 1px var(--group-video-bg), inset 0px 0px 0px 4px var(--accent-color), inset 0px 0px 0px 7px var(--app-bg-secondary)`;
const groupVideoBoxShadow = participantCount > 1 ? 'inset 0px 0px 0px 2px var(--group-video-bg)' : 'initial';

const handleTileClick = () => onTileDoubleClick(participant?.user.qualifiedId, participant?.clientId);

Expand All @@ -106,45 +96,21 @@
}
};

const participantNameColor = getParticipantNameColor({isActivelySpeaking, isAudioEstablished});

const nameContainer = !minimized && (
<div
className="group-video-grid__element__label"
css={{
backgroundColor: isActivelySpeaking ? 'var(--accent-color)' : 'var(--black)',
}}
>
<div className="group-video-grid__element__label" css={groupVideoActiveSpeaker(isActivelySpeaking)}>

Check warning on line 100 in src/script/components/calling/GroupVideoGridTile.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Unknown property 'css' found

See more on https://sonarcloud.io/project/issues?id=wireapp_wire-webapp&issues=AZrr6Qtn_7Q74VH45c_3&open=AZrr6Qtn_7Q74VH45c_3&pullRequest=19846
<span
data-uie-name={isActivelySpeaking ? 'status-active-speaking' : isMuted ? 'status-audio-off' : 'status-audio-on'}
css={{
overflow: 'hidden',
display: 'flex',
color: participantNameColor,
}}
css={groupVideoParticipantNameWrapper(isActivelySpeaking, isAudioEstablished)}

Check warning on line 103 in src/script/components/calling/GroupVideoGridTile.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Unknown property 'css' found

See more on https://sonarcloud.io/project/issues?id=wireapp_wire-webapp&issues=AZrr6Qtn_7Q74VH45c_4&open=AZrr6Qtn_7Q74VH45c_4&pullRequest=19846
>
<span
data-uie-value={participant?.user.id}
data-uie-name="call-participant-name"
css={{
textOverflow: 'ellipsis',
overflow: 'hidden',
}}
css={groupVideoParticipantName}

Check warning on line 108 in src/script/components/calling/GroupVideoGridTile.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Unknown property 'css' found

See more on https://sonarcloud.io/project/issues?id=wireapp_wire-webapp&issues=AZrr6Qtn_7Q74VH45c_5&open=AZrr6Qtn_7Q74VH45c_5&pullRequest=19846
>
{name}
</span>
{!isAudioEstablished && (
<span
css={{
color: 'var(--participant-audio-connecting-color)',
flexShrink: 0,
'&::before': {
content: "' • '",
whiteSpace: 'pre',
color: participantNameColor,
},
}}
>
<span css={groupVideoParticipantAudioStatus(isActivelySpeaking, isAudioEstablished)}>

Check warning on line 113 in src/script/components/calling/GroupVideoGridTile.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Unknown property 'css' found

See more on https://sonarcloud.io/project/issues?id=wireapp_wire-webapp&issues=AZrr6Qtn_7Q74VH45c_6&open=AZrr6Qtn_7Q74VH45c_6&pullRequest=19846
{t('videoCallParticipantConnecting')}
</span>
)}
Expand All @@ -153,14 +119,19 @@
);

return (
<button
<div
data-uie-name="item-grid"
data-user-id={participant?.user.id}
className="group-video-grid__element"
onDoubleClick={handleTileClick}
onKeyDown={handleEnterTileClick}
tabIndex={isMaximized ? TabIndex.FOCUSABLE : TabIndex.UNFOCUSABLE}
role="button"
// minimized is passed only from CallingCell where we don't want to focus individual the tile on the tab press
tabIndex={
(!minimized || isMaximized) && participant !== selfParticipant ? TabIndex.FOCUSABLE : TabIndex.UNFOCUSABLE
}
aria-label={`Focus video ${participant?.user.id}`}
>

Check warning on line 134 in src/script/components/calling/GroupVideoGridTile.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use <input type="button">, <input type="image">, <input type="reset">, <input type="submit">, or <button> instead of the "button" role to ensure accessibility across all devices.

See more on https://sonarcloud.io/project/issues?id=wireapp_wire-webapp&issues=AZrr6Qtn_7Q74VH45c_7&open=AZrr6Qtn_7Q74VH45c_7&pullRequest=19846
{hasActiveVideo ? (
<div className="tile-wrapper">
<Video
Expand All @@ -173,24 +144,11 @@
muted
srcObject={blurredVideoStream?.stream ?? videoStream}
className="group-video-grid__element-video"
css={{
objectFit: isMaximized || sharesScreen ? 'contain' : 'cover',
transform: participant === selfParticipant && sharesCamera ? 'rotateY(180deg)' : 'initial',
}}
css={groupVideoElementVideo(isMaximized || sharesScreen, participant === selfParticipant && sharesCamera)}
/>
</div>
) : (
<div
css={{
alignItems: 'center',
backgroundColor: 'var(--group-video-tile-bg)',
borderRadius: '10px',
display: 'flex',
height: '100%',
justifyContent: 'center',
width: '100%',
}}
>
<div css={groupVideoTileWrapper}>

Check warning on line 151 in src/script/components/calling/GroupVideoGridTile.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Unknown property 'css' found

See more on https://sonarcloud.io/project/issues?id=wireapp_wire-webapp&issues=AZrr6Qtn_7Q74VH45c_8&open=AZrr6Qtn_7Q74VH45c_8&pullRequest=19846
<Avatar
avatarSize={minimized ? AVATAR_SIZE.MEDIUM : AVATAR_SIZE.LARGE}
participant={participant?.user}
Expand All @@ -199,18 +157,7 @@
</div>
)}

<div
css={{
borderRadius: '8px',
bottom: 0,
boxShadow: isActivelySpeaking ? activelySpeakingBoxShadow : groupVideoBoxShadow,
left: 0,
position: 'absolute',
right: 0,
top: 0,
transition: 'box-shadow 0.3s ease-in-out',
}}
/>
<div css={groupVideoActiveSpeakerTile(isActivelySpeaking, participantCount)} />

Check warning on line 160 in src/script/components/calling/GroupVideoGridTile.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Unknown property 'css' found

See more on https://sonarcloud.io/project/issues?id=wireapp_wire-webapp&issues=AZrr6Qtn_7Q74VH45c_9&open=AZrr6Qtn_7Q74VH45c_9&pullRequest=19846

{!minimized && isMuted && (
<span className="group-video-grid__element__label__icon">
Expand Down Expand Up @@ -243,15 +190,15 @@

<div
className="group-video-grid__pause-overlay__label"
css={{fontsize: minimized ? '0.6875rem' : '0.875rem'}}
css={groupVideoPauseOverlayLabel(minimized)}

Check warning on line 193 in src/script/components/calling/GroupVideoGridTile.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Unknown property 'css' found

See more on https://sonarcloud.io/project/issues?id=wireapp_wire-webapp&issues=AZrr6Qtn_7Q74VH45c_-&open=AZrr6Qtn_7Q74VH45c_-&pullRequest=19846
data-uie-name="status-video-paused"
>
{hasPausedVideo ? t('videoCallPaused') : t('videoCallParticipantConnecting')}
</div>
{nameContainer}
</div>
)}
</button>
</div>
);
};

Expand Down
8 changes: 8 additions & 0 deletions src/script/util/KeyboardUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,17 @@
event: ReactKeyboardEvent<Element> | KeyboardEvent;
callback: (event?: ReactKeyboardEvent<Element> | KeyboardEvent) => void;
keys: Array<(typeof KEY)[keyof typeof KEY]>;
}) => {

Check failure on line 91 in src/script/util/KeyboardUtil.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this function to not always return the same value.

See more on https://sonarcloud.io/project/issues?id=wireapp_wire-webapp&issues=AZrr6QyR_7Q74VH45c__&open=AZrr6QyR_7Q74VH45c__&pullRequest=19846
if (keys.includes(event.key as (typeof KEY)[keyof typeof KEY])) {
if ('preventDefault' in event) {
event.preventDefault();
}
if ('stopPropagation' in event) {
event.stopPropagation();
}

callback(event);
return true;
}
return true;
};
Expand Down
11 changes: 6 additions & 5 deletions src/types/i18n.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -274,8 +274,6 @@ declare module 'I18n/en-US.json' {
'authLoginTitle': `Log in`;
'authPlaceholderEmail': `Email`;
'authPlaceholderPassword': `Password`;
'showTogglePasswordLabel': `Show password`;
'hideTogglePasswordLabel': `Hide password`;
'authPostedResend': `Resend to {email}`;
'authPostedResendAction': `No email showing up?`;
'authPostedResendDetail': `Check your email inbox and follow the instructions.`;
Expand Down Expand Up @@ -335,6 +333,7 @@ declare module 'I18n/en-US.json' {
'callAccept': `Accept`;
'callChooseSharedScreen': `Choose a screen to share`;
'callChooseSharedWindow': `Choose a window to share`;
'callChooseScreenCancel': `Close-screen sharing`;
'callConversationAcceptOrDecline': `{conversationName} is calling. Press control + enter to accept the call or press control + shift + enter to decline the call.`;
'callDecline': `Decline`;
'callDegradationAction': `OK`;
Expand Down Expand Up @@ -434,14 +433,14 @@ declare module 'I18n/en-US.json' {
'cells.options.delete': `Delete`;
'cells.options.deletePermanently': `Delete permanently`;
'cells.options.download': `Download`;
'cells.options.edit': `Edit`;
'cells.options.label': `More options`;
'cells.options.move': `Move to folder`;
'cells.options.open': `Open`;
'cells.options.rename': `Rename`;
'cells.options.restore': `Restore`;
'cells.options.share': `Share`;
'cells.options.tags': `Add or Remove Tags`;
'cells.options.edit': `Edit`;
'cells.pagination.loadMoreResults': `Load More Items`;
'cells.pagination.nextPage': `Next Page`;
'cells.pagination.previousPage': `Previous Page`;
Expand Down Expand Up @@ -1094,6 +1093,7 @@ declare module 'I18n/en-US.json' {
'guestRoomToggleInfoExtended': `Open this conversation to people outside your team. You can always change it later.`;
'guestRoomToggleInfoHead': `Guest Links`;
'guestRoomToggleName': `Allow Guests`;
'hideTogglePasswordLabel': `Hide password`;
'historyInfo.learnMore': `Learn more`;
'historyInfo.noHistoryHeadline': `It’s the first time you’re using {brandName} on this device.`;
'historyInfo.noHistoryInfo': `For privacy reasons, your history will not appear here.`;
Expand Down Expand Up @@ -1825,6 +1825,7 @@ declare module 'I18n/en-US.json' {
'setPassword.button': `Set password`;
'setPassword.headline': `Set password`;
'setPassword.passwordPlaceholder': `Password`;
'showTogglePasswordLabel': `Show password`;
'ssoLogin.codeInputPlaceholder': `SSO code`;
'ssoLogin.codeOrMailInputPlaceholder': `Email or SSO code`;
'ssoLogin.headline': `Company log in`;
Expand Down Expand Up @@ -1975,11 +1976,11 @@ declare module 'I18n/en-US.json' {
'userRemainingTimeHours': `{time}h left`;
'userRemainingTimeMinutes': `Less than {time}m left`;
'verify.changeEmail': `Change email`;
'verify.codeLabel': `Six-digit code`;
'verify.codePlaceholder': `Input field, enter digit`;
'verify.headline': `You’ve got mail`;
'verify.resendCode': `Resend code`;
'verify.subhead': `Enter the six-digit verification code we sent to{newline}{email}`;
'verify.codeLabel': `Six-digit code`;
'verify.codePlaceholder': `Input field, enter digit`;
'videoCallMenuMoreAddReaction': `Add reaction`;
'videoCallMenuMoreAudioSettings': `Audio Settings`;
'videoCallMenuMoreChangeView': `Change view`;
Expand Down