MM-51228 - Calls: UX redesign & theming support (#7283)
* UX redesign * fix call bar profile on Android * update @mattermost/calls; use common color utils * collapseIcon and time colour fixes (not correct yet on Onyx or Quartz) * fix time and collapseIcon colors; fix unavailable wrapper * center users, scroll when full; statusBar bg and styling * better spacing; better screenshare; no reaction bar when no reactions * remove margins from bottom; speaker/react buttonOn colour is now callsBg * update calls-common; update raised hand button colors * i18n * cleaning up style sheets * fix package-lock * fix vertical alignment of phone icon * add a noBorder prop to UserAvatarsStack * typography, icon sizing, spacing changes * add rounded header; UI improvements; refactor observables * updating phone icon, join call margins * join call text; color theming * remove unneeded container * phone-outline -> phone * split CallsChannelState into boolean components * use sidebar bg to generate calls bg * fix hand icon, button texts
This commit is contained in:
parent
34df44ab53
commit
f8f6839945
26 changed files with 1036 additions and 537 deletions
|
|
@ -31,6 +31,7 @@ type Props = {
|
|||
users: UserModel[];
|
||||
breakAt?: number;
|
||||
style?: StyleProp<ViewStyle>;
|
||||
noBorder?: boolean;
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
|
|
@ -51,6 +52,9 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
|||
backgroundColor: theme.centerChannelBg,
|
||||
borderRadius: size / 2,
|
||||
},
|
||||
noBorder: {
|
||||
borderWidth: 0,
|
||||
},
|
||||
notFirstAvatars: {
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
|
|
@ -99,7 +103,14 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
|||
};
|
||||
});
|
||||
|
||||
const UserAvatarsStack = ({breakAt = 3, channelId, location, style: baseContainerStyle, users}: Props) => {
|
||||
const UserAvatarsStack = ({
|
||||
breakAt = 3,
|
||||
channelId,
|
||||
location,
|
||||
style: baseContainerStyle,
|
||||
users,
|
||||
noBorder = false,
|
||||
}: Props) => {
|
||||
const theme = useTheme();
|
||||
const intl = useIntl();
|
||||
const isTablet = useIsTablet();
|
||||
|
|
@ -154,14 +165,14 @@ const UserAvatarsStack = ({breakAt = 3, channelId, location, style: baseContaine
|
|||
{displayUsers.map((user, index) => (
|
||||
<UserAvatar
|
||||
key={user.id}
|
||||
style={index === 0 ? style.firstAvatar : style.notFirstAvatars}
|
||||
style={index === 0 ? [style.firstAvatar, noBorder && style.noBorder] : [style.notFirstAvatars, noBorder && style.noBorder]}
|
||||
user={user}
|
||||
/>
|
||||
))}
|
||||
{Boolean(overflowUsersCount) && (
|
||||
<View style={style.overflowContainer}>
|
||||
<View style={style.overflowItem}>
|
||||
<Text style={style.overflowText} >
|
||||
<View style={[style.overflowContainer, noBorder && style.noBorder]}>
|
||||
<View style={[style.overflowItem, noBorder && style.noBorder]}>
|
||||
<Text style={style.overflowText}>
|
||||
{'+' + overflowUsersCount.toString()}
|
||||
</Text>
|
||||
</View>
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ export const SEARCH_INPUT_HEIGHT = Platform.select({android: 40, default: 36});
|
|||
export const SEARCH_INPUT_MARGIN = 5;
|
||||
|
||||
export const JOIN_CALL_BAR_HEIGHT = 38;
|
||||
export const CURRENT_CALL_BAR_HEIGHT = 74;
|
||||
export const CURRENT_CALL_BAR_HEIGHT = 68;
|
||||
export const CALL_ERROR_BAR_HEIGHT = 62;
|
||||
|
||||
export const ANNOUNCEMENT_BAR_HEIGHT = 40;
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ export const AudioDeviceButton = ({pressableStyle, iconStyle, buttonTextStyle, c
|
|||
>
|
||||
<CompassIcon
|
||||
name={'volume-high'}
|
||||
size={24}
|
||||
size={32}
|
||||
style={iconStyle}
|
||||
/>
|
||||
<Text style={buttonTextStyle}>{speakerLabel}</Text>
|
||||
|
|
|
|||
|
|
@ -115,7 +115,7 @@ export const AudioDeviceButton = ({pressableStyle, iconStyle, buttonTextStyle, c
|
|||
>
|
||||
<CompassIcon
|
||||
name={icon}
|
||||
size={24}
|
||||
size={32}
|
||||
style={iconStyle}
|
||||
/>
|
||||
<Text style={buttonTextStyle}>{label}</Text>
|
||||
|
|
|
|||
|
|
@ -2,12 +2,16 @@
|
|||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useMemo} from 'react';
|
||||
import {View, StyleSheet, Text, Platform} from 'react-native';
|
||||
import {View, StyleSheet, Platform} from 'react-native';
|
||||
|
||||
import {makeCallsTheme} from '@calls/utils';
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import Emoji from '@components/emoji';
|
||||
import ProfilePicture from '@components/profile_picture';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {changeOpacity} from '@utils/theme';
|
||||
|
||||
import type {CallsTheme} from '@calls/types/calls';
|
||||
import type {EmojiData} from '@mattermost/calls/lib/types';
|
||||
import type UserModel from '@typings/database/models/servers/user';
|
||||
|
||||
|
|
@ -15,60 +19,74 @@ type Props = {
|
|||
userModel?: UserModel;
|
||||
volume: number;
|
||||
serverUrl: string;
|
||||
size: number;
|
||||
muted?: boolean;
|
||||
sharingScreen?: boolean;
|
||||
raisedHand?: boolean;
|
||||
reaction?: EmojiData;
|
||||
size?: 'm' | 'l';
|
||||
}
|
||||
|
||||
const getStyleSheet = ({volume, muted, size}: { volume: number; muted?: boolean; size?: 'm' | 'l' }) => {
|
||||
const baseSize = size === 'm' || !size ? 40 : 72;
|
||||
const smallIcon = size === 'm' || !size;
|
||||
const widthHeight = smallIcon ? 20 : 24;
|
||||
const borderRadius = smallIcon ? 10 : 12;
|
||||
const padding = smallIcon ? 1 : 2;
|
||||
// Note: microSize is 32, smallSize is 72
|
||||
const mediumSize = 96;
|
||||
|
||||
const getStyleSheet = ({
|
||||
theme,
|
||||
volume,
|
||||
size,
|
||||
}: { theme: CallsTheme; volume: number; size: number }) => {
|
||||
// Note: we are using the same mute/reaction sizes for small and medium sizes
|
||||
const mediumIcon = size <= mediumSize;
|
||||
const muteWidthHeight = mediumIcon ? 28 : 36;
|
||||
const muteBorderRadius = mediumIcon ? 14 : 18;
|
||||
const reactWidthHeight = mediumIcon ? 32 : 40;
|
||||
const reactBorderRadius = mediumIcon ? 16 : 20;
|
||||
|
||||
return StyleSheet.create({
|
||||
pictureHalo: {
|
||||
backgroundColor: 'rgba(61, 184, 135,' + (0.24 * volume) + ')',
|
||||
height: baseSize + 16,
|
||||
width: baseSize + 16,
|
||||
backgroundColor: changeOpacity(theme.onlineIndicator, 0.24 * volume),
|
||||
height: size + 16,
|
||||
width: size + 16,
|
||||
padding: 4,
|
||||
marginRight: 4,
|
||||
borderRadius: (baseSize + 16) / 2,
|
||||
borderRadius: (size + 16) / 2,
|
||||
},
|
||||
pictureHalo2: {
|
||||
backgroundColor: 'rgba(61, 184, 135,' + (0.32 * volume) + ')',
|
||||
height: baseSize + 8,
|
||||
width: baseSize + 8,
|
||||
backgroundColor: changeOpacity(theme.onlineIndicator, 0.32 * volume),
|
||||
height: size + 8,
|
||||
width: size + 8,
|
||||
padding: 3,
|
||||
borderRadius: (baseSize + 8) / 2,
|
||||
borderRadius: (size + 8) / 2,
|
||||
},
|
||||
picture: {
|
||||
borderRadius: baseSize / 2,
|
||||
height: baseSize,
|
||||
width: baseSize,
|
||||
borderRadius: size / 2,
|
||||
height: size,
|
||||
width: size,
|
||||
marginBottom: 5,
|
||||
},
|
||||
profileIcon: {
|
||||
color: changeOpacity(theme.buttonColor, 0.16),
|
||||
},
|
||||
voiceShadow: {
|
||||
shadowColor: 'rgb(61, 184, 135)',
|
||||
shadowOffset: {width: 0, height: 0},
|
||||
shadowOpacity: 1,
|
||||
shadowRadius: 10,
|
||||
},
|
||||
mute: {
|
||||
muteIconContainer: {
|
||||
position: 'absolute',
|
||||
bottom: -5,
|
||||
bottom: 0,
|
||||
right: -5,
|
||||
width: widthHeight,
|
||||
height: widthHeight,
|
||||
borderRadius,
|
||||
padding,
|
||||
backgroundColor: muted ? 'black' : '#3DB887',
|
||||
borderColor: 'black',
|
||||
borderWidth: 2,
|
||||
color: 'white',
|
||||
width: muteWidthHeight,
|
||||
height: muteWidthHeight,
|
||||
borderRadius: muteBorderRadius,
|
||||
backgroundColor: theme.callsBg,
|
||||
},
|
||||
muteIcon: {
|
||||
width: muteWidthHeight,
|
||||
height: muteWidthHeight,
|
||||
borderRadius: muteBorderRadius,
|
||||
paddingTop: 6,
|
||||
backgroundColor: changeOpacity(theme.buttonColor, 0.16),
|
||||
color: theme.buttonColor,
|
||||
textAlign: 'center',
|
||||
textAlignVertical: 'center',
|
||||
overflow: 'hidden',
|
||||
|
|
@ -80,81 +98,101 @@ const getStyleSheet = ({volume, muted, size}: { volume: number; muted?: boolean;
|
|||
},
|
||||
),
|
||||
},
|
||||
reaction: {
|
||||
muteIconUnmuted: {
|
||||
backgroundColor: theme.onlineIndicator,
|
||||
},
|
||||
reactionContainer: {
|
||||
position: 'absolute',
|
||||
top: -5,
|
||||
right: -8,
|
||||
width: reactWidthHeight,
|
||||
height: reactWidthHeight,
|
||||
borderRadius: reactBorderRadius,
|
||||
backgroundColor: theme.callsBg,
|
||||
},
|
||||
reaction: {
|
||||
width: reactWidthHeight,
|
||||
height: reactWidthHeight,
|
||||
borderRadius: reactBorderRadius,
|
||||
textAlign: 'center',
|
||||
textAlignVertical: 'center',
|
||||
backgroundColor: changeOpacity(theme.buttonColor, 0.16),
|
||||
overflow: 'hidden',
|
||||
top: 0,
|
||||
right: -5,
|
||||
width: widthHeight,
|
||||
height: widthHeight,
|
||||
borderRadius,
|
||||
padding,
|
||||
backgroundColor: 'black',
|
||||
borderColor: 'black',
|
||||
borderWidth: 2,
|
||||
fontSize: smallIcon ? 10 : 12,
|
||||
},
|
||||
raisedHand: {
|
||||
backgroundColor: 'white',
|
||||
right: -5,
|
||||
backgroundColor: theme.buttonColor,
|
||||
color: theme.awayIndicator,
|
||||
...Platform.select(
|
||||
{
|
||||
android: {
|
||||
paddingLeft: 5,
|
||||
paddingTop: 3,
|
||||
color: 'rgb(255, 188, 66)',
|
||||
ios: {
|
||||
paddingRight: 1,
|
||||
paddingTop: 5,
|
||||
},
|
||||
default: {
|
||||
paddingLeft: 0,
|
||||
paddingTop: 0,
|
||||
},
|
||||
},
|
||||
),
|
||||
},
|
||||
screenSharing: {
|
||||
padding: padding + 1,
|
||||
backgroundColor: '#D24B4E',
|
||||
color: 'white',
|
||||
backgroundColor: theme.dndIndicator,
|
||||
color: theme.buttonColor,
|
||||
textAlign: 'center',
|
||||
textAlignVertical: 'center',
|
||||
paddingTop: Platform.select({ios: 3}),
|
||||
paddingTop: Platform.select({ios: 5}),
|
||||
},
|
||||
emoji: {
|
||||
paddingLeft: 1,
|
||||
paddingTop: Platform.select({ios: 2, default: 1}),
|
||||
paddingLeft: Platform.select({ios: 5, default: 5}),
|
||||
paddingTop: Platform.select({ios: 7, default: 3}),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const CallAvatar = ({userModel, volume, serverUrl, sharingScreen, size, muted, raisedHand, reaction}: Props) => {
|
||||
const style = useMemo(() => getStyleSheet({volume, muted, size}), [volume, muted, size]);
|
||||
const profileSize = size === 'm' || !size ? 40 : 72;
|
||||
const iconSize = size === 'm' || !size ? 12 : 16;
|
||||
const theme = useTheme();
|
||||
const callsTheme = useMemo(() => makeCallsTheme(theme), [theme]);
|
||||
const style = useMemo(() => getStyleSheet({theme: callsTheme, volume, size}), [callsTheme, volume, size]);
|
||||
|
||||
const iconSize = size <= mediumSize ? 18 : 24;
|
||||
const reactionSize = size <= mediumSize ? 22 : 26;
|
||||
const styleShadow = volume > 0 ? style.voiceShadow : undefined;
|
||||
|
||||
// Only show one or the other.
|
||||
let topRightIcon: JSX.Element | null = null;
|
||||
if (sharingScreen) {
|
||||
topRightIcon = (
|
||||
<CompassIcon
|
||||
name={'monitor'}
|
||||
size={iconSize}
|
||||
style={[style.reaction, style.screenSharing]}
|
||||
/>
|
||||
<View style={style.reactionContainer}>
|
||||
<CompassIcon
|
||||
name={'monitor'}
|
||||
size={reactionSize}
|
||||
style={[style.reaction, style.screenSharing]}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
} else if (raisedHand) {
|
||||
topRightIcon = (
|
||||
<Text style={[style.reaction, style.raisedHand]}>
|
||||
{'✋'}
|
||||
</Text>
|
||||
<View style={style.reactionContainer}>
|
||||
<CompassIcon
|
||||
name={'hand-right'}
|
||||
size={reactionSize}
|
||||
style={[style.reaction, style.raisedHand]}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
// An emoji will override the top right indicator.
|
||||
if (reaction) {
|
||||
topRightIcon = (
|
||||
<View style={[style.reaction, style.emoji]}>
|
||||
<Emoji
|
||||
emojiName={reaction.name}
|
||||
literal={reaction.literal}
|
||||
size={iconSize - 3}
|
||||
/>
|
||||
<View style={style.reactionContainer}>
|
||||
<View style={[style.reaction, style.emoji]}>
|
||||
<Emoji
|
||||
emojiName={reaction.name}
|
||||
literal={reaction.literal}
|
||||
size={reactionSize - Platform.select({ios: 6, default: 4})}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
|
@ -162,14 +200,15 @@ const CallAvatar = ({userModel, volume, serverUrl, sharingScreen, size, muted, r
|
|||
const profile = userModel ? (
|
||||
<ProfilePicture
|
||||
author={userModel}
|
||||
size={profileSize}
|
||||
size={size}
|
||||
showStatus={false}
|
||||
url={serverUrl}
|
||||
/>
|
||||
) : (
|
||||
<CompassIcon
|
||||
name='account-outline'
|
||||
size={profileSize}
|
||||
size={size}
|
||||
style={style.profileIcon}
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
@ -178,11 +217,13 @@ const CallAvatar = ({userModel, volume, serverUrl, sharingScreen, size, muted, r
|
|||
{profile}
|
||||
{
|
||||
muted !== undefined &&
|
||||
<CompassIcon
|
||||
name={muted ? 'microphone-off' : 'microphone'}
|
||||
size={iconSize}
|
||||
style={style.mute}
|
||||
/>
|
||||
<View style={style.muteIconContainer}>
|
||||
<CompassIcon
|
||||
name={muted ? 'microphone-off' : 'microphone'}
|
||||
size={iconSize}
|
||||
style={[style.muteIcon, !muted && style.muteIconUnmuted]}
|
||||
/>
|
||||
</View>
|
||||
}
|
||||
{topRightIcon}
|
||||
</View>
|
||||
|
|
|
|||
|
|
@ -50,7 +50,11 @@ const CallDuration = ({value, style, updateIntervalInSeconds}: CallDurationProps
|
|||
}, [updateIntervalInSeconds]);
|
||||
|
||||
return (
|
||||
<Text style={style}>
|
||||
<Text
|
||||
style={style}
|
||||
numberOfLines={1}
|
||||
ellipsizeMode={'clip'}
|
||||
>
|
||||
{formattedTime}
|
||||
</Text>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ const styles = StyleSheet.create({
|
|||
loading: {
|
||||
paddingLeft: 8,
|
||||
paddingRight: 8,
|
||||
marginRight: 8,
|
||||
color: 'white',
|
||||
height: 34,
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.16)',
|
||||
|
|
@ -27,6 +28,7 @@ const styles = StyleSheet.create({
|
|||
recording: {
|
||||
paddingLeft: 8,
|
||||
paddingRight: 8,
|
||||
marginRight: 8,
|
||||
color: 'white',
|
||||
height: 34,
|
||||
backgroundColor: '#D24B4E',
|
||||
|
|
|
|||
|
|
@ -39,39 +39,39 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
|||
},
|
||||
messageStyle: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
color: changeOpacity(theme.centerChannelColor, 0.6),
|
||||
fontSize: 15,
|
||||
lineHeight: 20,
|
||||
paddingTop: 5,
|
||||
paddingBottom: 5,
|
||||
},
|
||||
messageText: {
|
||||
flex: 1,
|
||||
paddingLeft: 12,
|
||||
paddingRight: 4,
|
||||
},
|
||||
joinCallIcon: {
|
||||
padding: 12,
|
||||
backgroundColor: '#339970',
|
||||
borderRadius: 8,
|
||||
marginRight: 5,
|
||||
color: 'white',
|
||||
padding: 8,
|
||||
backgroundColor: theme.onlineIndicator,
|
||||
borderRadius: 4,
|
||||
color: theme.buttonColor,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
phoneHangupIcon: {
|
||||
padding: 12,
|
||||
padding: 8,
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.6),
|
||||
borderRadius: 8,
|
||||
marginRight: 5,
|
||||
color: 'white',
|
||||
borderRadius: 4,
|
||||
color: theme.buttonColor,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
joinCallButtonText: {
|
||||
color: 'white',
|
||||
color: theme.buttonColor,
|
||||
...typography('Body', 75, 'SemiBold'),
|
||||
},
|
||||
joinCallButtonTextRestricted: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.32),
|
||||
},
|
||||
joinCallButtonIcon: {
|
||||
color: 'white',
|
||||
color: theme.buttonColor,
|
||||
marginRight: 5,
|
||||
},
|
||||
joinCallButtonIconRestricted: {
|
||||
|
|
@ -79,13 +79,13 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
|||
},
|
||||
startedText: {
|
||||
color: theme.centerChannelColor,
|
||||
fontWeight: 'bold',
|
||||
...typography('Body', 100, 'SemiBold'),
|
||||
},
|
||||
joinCallButton: {
|
||||
flexDirection: 'row',
|
||||
padding: 12,
|
||||
backgroundColor: '#339970',
|
||||
borderRadius: 8,
|
||||
backgroundColor: theme.onlineIndicator,
|
||||
borderRadius: 4,
|
||||
alignItems: 'center',
|
||||
alignContent: 'center',
|
||||
},
|
||||
|
|
@ -93,7 +93,8 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
|||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.08),
|
||||
},
|
||||
timeText: {
|
||||
color: theme.centerChannelColor,
|
||||
...typography('Body', 75),
|
||||
color: changeOpacity(theme.centerChannelColor, 0.64),
|
||||
},
|
||||
endCallInfo: {
|
||||
flexDirection: 'row',
|
||||
|
|
@ -147,7 +148,7 @@ export const CallsCustomMessage = ({
|
|||
<View style={style.messageStyle}>
|
||||
<CompassIcon
|
||||
name='phone-hangup'
|
||||
size={16}
|
||||
size={24}
|
||||
style={style.phoneHangupIcon}
|
||||
/>
|
||||
<View style={style.messageText}>
|
||||
|
|
@ -190,7 +191,7 @@ export const CallsCustomMessage = ({
|
|||
<View style={style.messageStyle}>
|
||||
<CompassIcon
|
||||
name='phone-in-talk'
|
||||
size={16}
|
||||
size={24}
|
||||
style={style.joinCallIcon}
|
||||
/>
|
||||
<View style={style.messageText}>
|
||||
|
|
@ -212,8 +213,8 @@ export const CallsCustomMessage = ({
|
|||
onPress={joinHandler}
|
||||
>
|
||||
<CompassIcon
|
||||
name='phone-outline'
|
||||
size={16}
|
||||
name='phone'
|
||||
size={14}
|
||||
style={[style.joinCallButtonIcon, isLimitRestricted && style.joinCallButtonIconRestricted]}
|
||||
/>
|
||||
{
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ const ChannelInfoEnableCalls = ({channelId, enabled}: Props) => {
|
|||
<OptionItem
|
||||
action={preventDoubleTap(tryOnPress)}
|
||||
label={(enabled ? disableText : enableText) + msgPostfix}
|
||||
icon='phone-outline'
|
||||
icon='phone'
|
||||
type='default'
|
||||
testID='channel_info.options.enable_disable_calls.option'
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ const ChannelInfoStartButton = ({
|
|||
<OptionBox
|
||||
onPress={preventDoubleTap(tryJoin)}
|
||||
text={startText + msgPostfix}
|
||||
iconName='phone-outline'
|
||||
iconName='phone'
|
||||
activeText={joinText + msgPostfix}
|
||||
activeIconName='phone-in-talk'
|
||||
isActive={isACallInCurrentChannel}
|
||||
|
|
|
|||
|
|
@ -1,25 +1,28 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback} from 'react';
|
||||
import React, {useCallback, useMemo} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {View, Text, TouchableOpacity, Pressable, Platform} from 'react-native';
|
||||
import {View, Text, Pressable, Platform} from 'react-native';
|
||||
|
||||
import {muteMyself, unmuteMyself} from '@calls/actions';
|
||||
import {leaveCall, muteMyself, unmuteMyself} from '@calls/actions';
|
||||
import {recordingAlert, recordingWillBePostedAlert, recordingErrorAlert} from '@calls/alerts';
|
||||
import CallAvatar from '@calls/components/call_avatar';
|
||||
import CallDuration from '@calls/components/call_duration';
|
||||
import PermissionErrorBar from '@calls/components/permission_error_bar';
|
||||
import UnavailableIconWrapper from '@calls/components/unavailable_icon_wrapper';
|
||||
import {usePermissionsChecker} from '@calls/hooks';
|
||||
import {makeCallsTheme} from '@calls/utils';
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import {Screens} from '@constants';
|
||||
import {CURRENT_CALL_BAR_HEIGHT} from '@constants/view';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {allOrientations, dismissAllModalsAndPopToScreen} from '@screens/navigation';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
import {displayUsername} from '@utils/user';
|
||||
|
||||
import type {CurrentCall} from '@calls/types/calls';
|
||||
import type {CallsTheme, CurrentCall} from '@calls/types/calls';
|
||||
import type UserModel from '@typings/database/models/servers/user';
|
||||
import type {Options} from 'react-native-navigation';
|
||||
|
||||
|
|
@ -32,56 +35,94 @@ type Props = {
|
|||
threadScreen?: boolean;
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: CallsTheme) => {
|
||||
return {
|
||||
wrapper: {
|
||||
padding: 10,
|
||||
marginTop: 8,
|
||||
marginRight: 6,
|
||||
marginBottom: 8,
|
||||
marginLeft: 6,
|
||||
backgroundColor: theme.callsBg,
|
||||
borderRadius: 8,
|
||||
},
|
||||
container: {
|
||||
flexDirection: 'row',
|
||||
backgroundColor: '#3F4350',
|
||||
width: '100%',
|
||||
borderRadius: 5,
|
||||
padding: 4,
|
||||
height: CURRENT_CALL_BAR_HEIGHT - 10,
|
||||
alignItems: 'center',
|
||||
backgroundColor: changeOpacity(theme.buttonColor, 0.08),
|
||||
borderRadius: 8,
|
||||
borderWidth: 2,
|
||||
borderStyle: 'solid',
|
||||
borderColor: changeOpacity(theme.buttonColor, 0.16),
|
||||
width: '100%',
|
||||
paddingTop: 8,
|
||||
paddingRight: 12,
|
||||
paddingBottom: 8,
|
||||
paddingLeft: 12,
|
||||
height: CURRENT_CALL_BAR_HEIGHT - 10,
|
||||
},
|
||||
pressable: {
|
||||
zIndex: 10,
|
||||
},
|
||||
profilePic: {
|
||||
marginTop: 4,
|
||||
marginRight: Platform.select({android: -8}),
|
||||
marginLeft: Platform.select({android: -8}),
|
||||
},
|
||||
userInfo: {
|
||||
flex: 1,
|
||||
paddingLeft: 10,
|
||||
paddingLeft: 6,
|
||||
},
|
||||
speakingUser: {
|
||||
color: theme.sidebarText,
|
||||
fontWeight: '600',
|
||||
fontSize: 16,
|
||||
color: theme.buttonColor,
|
||||
...typography('Body', 200, 'SemiBold'),
|
||||
},
|
||||
currentChannel: {
|
||||
color: theme.sidebarText,
|
||||
opacity: 0.64,
|
||||
speakingPostfix: {
|
||||
...typography('Body', 200, 'Regular'),
|
||||
},
|
||||
channelAndTime: {
|
||||
color: changeOpacity(theme.buttonColor, 0.56),
|
||||
...typography('Body', 75, 'Regular'),
|
||||
},
|
||||
separator: {
|
||||
color: changeOpacity(theme.buttonColor, 0.32),
|
||||
...typography('Body', 75, 'Regular'),
|
||||
},
|
||||
buttonContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-evenly',
|
||||
},
|
||||
micIconContainer: {
|
||||
width: 42,
|
||||
height: 42,
|
||||
width: 40,
|
||||
height: 40,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
backgroundColor: theme.onlineIndicator,
|
||||
borderRadius: 4,
|
||||
margin: 4,
|
||||
padding: 9,
|
||||
borderRadius: 20,
|
||||
},
|
||||
micIcon: {
|
||||
color: theme.sidebarText,
|
||||
color: changeOpacity(theme.buttonColor, 0.56),
|
||||
},
|
||||
muted: {
|
||||
backgroundColor: 'transparent',
|
||||
backgroundColor: changeOpacity(theme.buttonColor, 0.08),
|
||||
},
|
||||
expandIcon: {
|
||||
color: theme.sidebarText,
|
||||
padding: 8,
|
||||
marginRight: 8,
|
||||
verticalLine: {
|
||||
height: 42,
|
||||
width: 1,
|
||||
backgroundColor: changeOpacity(theme.buttonColor, 0.16),
|
||||
marginLeft: 12,
|
||||
marginRight: 12,
|
||||
},
|
||||
hangupIconContainer: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
backgroundColor: theme.dndIndicator,
|
||||
borderRadius: 20,
|
||||
},
|
||||
hangupIcon: {
|
||||
color: theme.buttonColor,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
|
@ -95,7 +136,8 @@ const CurrentCallBar = ({
|
|||
threadScreen,
|
||||
}: Props) => {
|
||||
const theme = useTheme();
|
||||
const style = getStyleSheet(theme);
|
||||
const callsTheme = useMemo(() => makeCallsTheme(theme), [theme]);
|
||||
const style = getStyleSheet(callsTheme);
|
||||
const intl = useIntl();
|
||||
const {formatMessage} = intl;
|
||||
usePermissionsChecker(micPermissionsGranted);
|
||||
|
|
@ -118,21 +160,35 @@ const CurrentCallBar = ({
|
|||
await dismissAllModalsAndPopToScreen(Screens.CALL, title, {fromThreadScreen: threadScreen}, options);
|
||||
}, [formatMessage, threadScreen]);
|
||||
|
||||
const leaveCallHandler = useCallback(() => {
|
||||
leaveCall();
|
||||
}, []);
|
||||
|
||||
const myParticipant = currentCall?.participants[currentCall.myUserId];
|
||||
|
||||
// Since we can only see one user talking, it doesn't really matter who we show here (e.g., we can't
|
||||
// tell who is speaking louder).
|
||||
const talkingUsers = Object.keys(currentCall?.voiceOn || {});
|
||||
const speaker = talkingUsers.length > 0 ? talkingUsers[0] : '';
|
||||
let talkingMessage = formatMessage({
|
||||
id: 'mobile.calls_noone_talking',
|
||||
defaultMessage: 'No one is talking',
|
||||
});
|
||||
let talkingMessage = (
|
||||
<Text style={style.speakingUser}>
|
||||
{formatMessage({
|
||||
id: 'mobile.calls_noone_talking',
|
||||
defaultMessage: 'No one is talking',
|
||||
})}
|
||||
</Text>);
|
||||
if (speaker) {
|
||||
talkingMessage = formatMessage({
|
||||
id: 'mobile.calls_name_is_talking',
|
||||
defaultMessage: '{name} is talking',
|
||||
}, {name: displayUsername(userModelsDict[speaker], intl.locale, teammateNameDisplay)});
|
||||
talkingMessage = (
|
||||
<Text style={style.speakingUser}>
|
||||
{displayUsername(userModelsDict[speaker], intl.locale, teammateNameDisplay)}
|
||||
{' '}
|
||||
<Text style={style.speakingPostfix}>{
|
||||
formatMessage({
|
||||
id: 'mobile.calls_name_is_talking_postfix',
|
||||
defaultMessage: 'is talking...',
|
||||
})}
|
||||
</Text>
|
||||
</Text>);
|
||||
}
|
||||
|
||||
const muteUnmute = () => {
|
||||
|
|
@ -166,39 +222,56 @@ const CurrentCallBar = ({
|
|||
return (
|
||||
<>
|
||||
<View style={style.wrapper}>
|
||||
<View style={style.container}>
|
||||
<CallAvatar
|
||||
userModel={userModelsDict[speaker || '']}
|
||||
volume={speaker ? 0.5 : 0}
|
||||
serverUrl={currentCall?.serverUrl || ''}
|
||||
/>
|
||||
<View style={style.userInfo}>
|
||||
<Text style={style.speakingUser}>{talkingMessage}</Text>
|
||||
<Text style={style.currentChannel}>{`~${displayName}`}</Text>
|
||||
<Pressable
|
||||
style={style.container}
|
||||
onPress={goToCallScreen}
|
||||
>
|
||||
<View style={style.profilePic}>
|
||||
<CallAvatar
|
||||
userModel={userModelsDict[speaker || '']}
|
||||
volume={speaker ? 0.5 : 0}
|
||||
serverUrl={currentCall?.serverUrl || ''}
|
||||
size={32}
|
||||
/>
|
||||
</View>
|
||||
<Pressable
|
||||
onPressIn={goToCallScreen}
|
||||
style={style.pressable}
|
||||
>
|
||||
<CompassIcon
|
||||
name='arrow-expand'
|
||||
size={24}
|
||||
style={style.expandIcon}
|
||||
/>
|
||||
</Pressable>
|
||||
<TouchableOpacity
|
||||
onPress={muteUnmute}
|
||||
style={[style.pressable, style.micIconContainer, myParticipant?.muted && style.muted]}
|
||||
disabled={!micPermissionsGranted}
|
||||
>
|
||||
<UnavailableIconWrapper
|
||||
name={myParticipant?.muted ? 'microphone-off' : 'microphone'}
|
||||
size={24}
|
||||
unavailable={!micPermissionsGranted}
|
||||
style={[style.micIcon]}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<View style={style.userInfo}>
|
||||
{talkingMessage}
|
||||
<Text style={style.channelAndTime}>
|
||||
{`~${displayName}`}
|
||||
<Text style={style.separator}>{' • '}</Text>
|
||||
<CallDuration
|
||||
style={style.channelAndTime}
|
||||
value={currentCall?.startTime || Date.now()}
|
||||
updateIntervalInSeconds={1}
|
||||
/>
|
||||
</Text>
|
||||
</View>
|
||||
<View style={style.buttonContainer}>
|
||||
<Pressable
|
||||
onPress={muteUnmute}
|
||||
style={[style.pressable, style.micIconContainer, myParticipant?.muted && style.muted]}
|
||||
disabled={!micPermissionsGranted}
|
||||
>
|
||||
<UnavailableIconWrapper
|
||||
name={myParticipant?.muted ? 'microphone-off' : 'microphone'}
|
||||
size={24}
|
||||
unavailable={!micPermissionsGranted}
|
||||
style={style.micIcon}
|
||||
/>
|
||||
</Pressable>
|
||||
<View style={style.verticalLine}/>
|
||||
<Pressable
|
||||
onPress={leaveCallHandler}
|
||||
style={[style.pressable, style.hangupIconContainer]}
|
||||
>
|
||||
<CompassIcon
|
||||
name='phone-hangup'
|
||||
size={24}
|
||||
style={style.hangupIcon}
|
||||
/>
|
||||
</Pressable>
|
||||
</View>
|
||||
</Pressable>
|
||||
</View>
|
||||
{micPermissionsError && <PermissionErrorBar/>}
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -1,11 +1,14 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import React, {useMemo} from 'react';
|
||||
import {StyleSheet, View} from 'react-native';
|
||||
import LinearGradient from 'react-native-linear-gradient';
|
||||
|
||||
import EmojiPill from '@calls/components/emoji_pill';
|
||||
import {makeCallsTheme} from '@calls/utils';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {changeOpacity} from '@utils/theme';
|
||||
|
||||
import type {ReactionStreamEmoji} from '@calls/types/calls';
|
||||
|
||||
|
|
@ -35,7 +38,6 @@ const styles = StyleSheet.create({
|
|||
const gradient = {
|
||||
start: {x: 0.75, y: 0},
|
||||
end: {x: 1, y: 0},
|
||||
colors: ['#00000000', '#000000'],
|
||||
};
|
||||
|
||||
interface Props {
|
||||
|
|
@ -43,6 +45,9 @@ interface Props {
|
|||
}
|
||||
|
||||
const EmojiList = ({reactionStream}: Props) => {
|
||||
const theme = useTheme();
|
||||
const callsTheme = useMemo(() => makeCallsTheme(theme), [theme]);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View style={styles.emojiList}>
|
||||
|
|
@ -58,7 +63,7 @@ const EmojiList = ({reactionStream}: Props) => {
|
|||
<LinearGradient
|
||||
start={gradient.start}
|
||||
end={gradient.end}
|
||||
colors={gradient.colors}
|
||||
colors={[changeOpacity(callsTheme.callsBg, 0), callsTheme.callsBg]}
|
||||
style={styles.gradient}
|
||||
/>
|
||||
</View>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,10 @@ import React from 'react';
|
|||
import {View, Platform, StyleSheet} from 'react-native';
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context';
|
||||
|
||||
import CurrentCallBar from '@calls/components/current_call_bar';
|
||||
import JoinCallBanner from '@calls/components/join_call_banner';
|
||||
import {DEFAULT_HEADER_HEIGHT} from '@constants/view';
|
||||
import {useServerUrl} from '@context/server';
|
||||
|
||||
const topBarHeight = DEFAULT_HEADER_HEIGHT;
|
||||
|
||||
|
|
@ -25,11 +28,14 @@ const style = StyleSheet.create({
|
|||
});
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode;
|
||||
channelId: string;
|
||||
showJoinCallBanner: boolean;
|
||||
isInACall: boolean;
|
||||
threadScreen?: boolean;
|
||||
}
|
||||
|
||||
const FloatingCallContainer = ({threadScreen, ...props}: Props) => {
|
||||
const FloatingCallContainer = ({channelId, showJoinCallBanner, isInACall, threadScreen}: Props) => {
|
||||
const serverUrl = useServerUrl();
|
||||
const insets = useSafeAreaInsets();
|
||||
const wrapperTop = {
|
||||
top: insets.top + (threadScreen ? 0 : topBarHeight),
|
||||
|
|
@ -37,7 +43,13 @@ const FloatingCallContainer = ({threadScreen, ...props}: Props) => {
|
|||
|
||||
return (
|
||||
<View style={[style.wrapper, wrapperTop]}>
|
||||
{props.children}
|
||||
{showJoinCallBanner &&
|
||||
<JoinCallBanner
|
||||
serverUrl={serverUrl}
|
||||
channelId={channelId}
|
||||
/>
|
||||
}
|
||||
{isInACall && <CurrentCallBar/>}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import Screens from '@constants/screens';
|
|||
import {JOIN_CALL_BAR_HEIGHT} from '@constants/view';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
import type {LimitRestrictedInfo} from '@calls/observers';
|
||||
import type UserModel from '@typings/database/models/servers/user';
|
||||
|
|
@ -28,13 +29,18 @@ type Props = {
|
|||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
outerContainer: {
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
backgroundColor: theme.sidebarBg,
|
||||
},
|
||||
innerContainer: {
|
||||
flexDirection: 'row',
|
||||
backgroundColor: '#3DB887',
|
||||
backgroundColor: '#339970', // intentionally not themed
|
||||
width: '100%',
|
||||
padding: 5,
|
||||
borderTopLeftRadius: 12,
|
||||
borderTopRightRadius: 12,
|
||||
paddingTop: 5,
|
||||
paddingBottom: 5,
|
||||
paddingLeft: 12,
|
||||
paddingRight: 12,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
height: JOIN_CALL_BAR_HEIGHT,
|
||||
|
|
@ -43,19 +49,17 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
|||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.48),
|
||||
},
|
||||
joinCallIcon: {
|
||||
color: theme.sidebarText,
|
||||
marginLeft: 10,
|
||||
marginRight: 5,
|
||||
color: theme.buttonColor,
|
||||
marginRight: 7,
|
||||
},
|
||||
joinCall: {
|
||||
color: theme.sidebarText,
|
||||
fontWeight: 'bold',
|
||||
fontSize: 16,
|
||||
color: theme.buttonColor,
|
||||
...typography('Body', 100, 'SemiBold'),
|
||||
},
|
||||
started: {
|
||||
flex: 1,
|
||||
color: theme.sidebarText,
|
||||
fontWeight: '400',
|
||||
color: changeOpacity(theme.buttonColor, 0.84),
|
||||
...typography(),
|
||||
marginLeft: 10,
|
||||
},
|
||||
limitReached: {
|
||||
|
|
@ -63,12 +67,9 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
|||
display: 'flex',
|
||||
textAlign: 'right',
|
||||
marginRight: 10,
|
||||
color: '#FFFFFFD6',
|
||||
color: changeOpacity(theme.sidebarText, 0.84),
|
||||
fontWeight: '400',
|
||||
},
|
||||
avatars: {
|
||||
marginRight: 5,
|
||||
},
|
||||
headerText: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.56),
|
||||
fontSize: 12,
|
||||
|
|
@ -107,7 +108,7 @@ const JoinCallBanner = ({
|
|||
>
|
||||
<CompassIcon
|
||||
name='phone-in-talk'
|
||||
size={16}
|
||||
size={18}
|
||||
style={style.joinCallIcon}
|
||||
/>
|
||||
<FormattedText
|
||||
|
|
@ -128,14 +129,13 @@ const JoinCallBanner = ({
|
|||
style={style.started}
|
||||
/>
|
||||
)}
|
||||
<View style={style.avatars}>
|
||||
<UserAvatarsStack
|
||||
channelId={channelId}
|
||||
location={Screens.CHANNEL}
|
||||
users={participants}
|
||||
breakAt={1}
|
||||
/>
|
||||
</View>
|
||||
<UserAvatarsStack
|
||||
channelId={channelId}
|
||||
location={Screens.CHANNEL}
|
||||
users={participants}
|
||||
breakAt={1}
|
||||
noBorder={true}
|
||||
/>
|
||||
</Pressable>
|
||||
</View>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,45 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {View} from 'react-native';
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context';
|
||||
|
||||
import {DEFAULT_HEADER_HEIGHT, JOIN_CALL_BAR_HEIGHT} from '@constants/view';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
type Props = {
|
||||
threadScreen?: boolean;
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
container: {
|
||||
backgroundColor: '#339970', // intentionally not themed
|
||||
height: 40,
|
||||
width: '100%',
|
||||
position: 'absolute',
|
||||
},
|
||||
content: {
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
borderTopLeftRadius: 12,
|
||||
borderTopRightRadius: 12,
|
||||
flex: 1,
|
||||
},
|
||||
}));
|
||||
|
||||
export const RoundedHeaderCalls = ({threadScreen}: Props) => {
|
||||
const theme = useTheme();
|
||||
const insets = useSafeAreaInsets();
|
||||
|
||||
const styles = getStyleSheet(theme);
|
||||
const containerTop = {
|
||||
top: insets.top + (threadScreen ? JOIN_CALL_BAR_HEIGHT : JOIN_CALL_BAR_HEIGHT + DEFAULT_HEADER_HEIGHT),
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={[styles.container, containerTop]}>
|
||||
<View style={styles.content}/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,35 +1,44 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback} from 'react';
|
||||
import React, {useCallback, useMemo} from 'react';
|
||||
import {Pressable, StyleSheet, useWindowDimensions, View} from 'react-native';
|
||||
|
||||
import {raiseHand, unraiseHand} from '@calls/actions';
|
||||
import {sendReaction} from '@calls/actions/calls';
|
||||
import EmojiButton from '@calls/components/emoji_button';
|
||||
import {makeCallsTheme} from '@calls/utils';
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {changeOpacity} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
import type {CallsTheme} from '@calls/types/calls';
|
||||
|
||||
const getStyleSheet = ((theme: CallsTheme) => StyleSheet.create({
|
||||
outerContainer: {
|
||||
flexDirection: 'row',
|
||||
},
|
||||
container: {
|
||||
display: 'flex',
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'flex-end',
|
||||
justifyContent: 'space-between',
|
||||
width: '100%',
|
||||
height: 64,
|
||||
paddingLeft: 16,
|
||||
paddingRight: 16,
|
||||
},
|
||||
containerInLandscape: {
|
||||
paddingBottom: 6,
|
||||
containerLandscape: {
|
||||
height: 60,
|
||||
paddingBottom: 12,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
button: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: 'rgba(255,255,255,0.08)',
|
||||
backgroundColor: changeOpacity(theme.buttonColor, 0.08),
|
||||
borderRadius: 30,
|
||||
height: 48,
|
||||
maxWidth: 160,
|
||||
|
|
@ -41,18 +50,19 @@ const styles = StyleSheet.create({
|
|||
marginLeft: 12,
|
||||
},
|
||||
buttonPressed: {
|
||||
backgroundColor: 'rgba(245, 171, 0, 0.24)',
|
||||
backgroundColor: theme.buttonColor,
|
||||
},
|
||||
unPressed: {
|
||||
color: 'white',
|
||||
color: changeOpacity(theme.buttonColor, 0.56),
|
||||
},
|
||||
pressed: {
|
||||
color: '#F5AB00',
|
||||
color: theme.callsBg,
|
||||
},
|
||||
buttonText: {
|
||||
marginLeft: 8,
|
||||
...typography('Body', 200, 'SemiBold'),
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
const predefinedReactions = [['+1', '1F44D'], ['clap', '1F44F'], ['joy', '1F602'], ['heart', '2764-FE0F']];
|
||||
|
||||
|
|
@ -61,6 +71,9 @@ interface Props {
|
|||
}
|
||||
|
||||
const ReactionBar = ({raisedHand}: Props) => {
|
||||
const theme = useTheme();
|
||||
const callsTheme = useMemo(() => makeCallsTheme(theme), [theme]);
|
||||
const style = getStyleSheet(callsTheme);
|
||||
const {width, height} = useWindowDimensions();
|
||||
const isLandscape = width > height;
|
||||
|
||||
|
|
@ -68,13 +81,13 @@ const ReactionBar = ({raisedHand}: Props) => {
|
|||
<FormattedText
|
||||
id={'mobile.calls_lower_hand'}
|
||||
defaultMessage={'Lower hand'}
|
||||
style={[styles.buttonText, raisedHand ? styles.pressed : styles.unPressed]}
|
||||
style={[style.buttonText, raisedHand ? style.pressed : style.unPressed]}
|
||||
/>);
|
||||
const RaiseHandText = (
|
||||
<FormattedText
|
||||
id={'mobile.calls_raise_hand'}
|
||||
defaultMessage={'Raise hand'}
|
||||
style={[styles.buttonText, raisedHand ? styles.pressed : styles.unPressed]}
|
||||
style={[style.buttonText, raisedHand ? style.pressed : style.unPressed]}
|
||||
/>);
|
||||
|
||||
const toggleRaiseHand = useCallback(() => {
|
||||
|
|
@ -87,28 +100,30 @@ const ReactionBar = ({raisedHand}: Props) => {
|
|||
}, [raisedHand]);
|
||||
|
||||
return (
|
||||
<View style={[styles.container, isLandscape && styles.containerInLandscape]}>
|
||||
<Pressable
|
||||
style={[styles.button, isLandscape && styles.buttonLandscape, Boolean(raisedHand) && styles.buttonPressed]}
|
||||
onPress={toggleRaiseHand}
|
||||
>
|
||||
<CompassIcon
|
||||
name={raisedHand ? 'hand-right-outline-off' : 'hand-right-outline'}
|
||||
size={24}
|
||||
style={[raisedHand ? styles.pressed : styles.unPressed]}
|
||||
/>
|
||||
{raisedHand ? LowerHandText : RaiseHandText}
|
||||
</Pressable>
|
||||
{
|
||||
predefinedReactions.map(([name, unified]) => (
|
||||
<EmojiButton
|
||||
key={name}
|
||||
emojiName={name}
|
||||
style={[styles.button, isLandscape && styles.buttonLandscape]}
|
||||
onPress={() => sendReaction({name, unified})}
|
||||
<View style={style.outerContainer}>
|
||||
<View style={[style.container, isLandscape && style.containerLandscape]}>
|
||||
<Pressable
|
||||
style={[style.button, isLandscape && style.buttonLandscape, Boolean(raisedHand) && style.buttonPressed]}
|
||||
onPress={toggleRaiseHand}
|
||||
>
|
||||
<CompassIcon
|
||||
name={raisedHand ? 'hand-right-outline-off' : 'hand-right'}
|
||||
size={24}
|
||||
style={[raisedHand ? style.pressed : style.unPressed]}
|
||||
/>
|
||||
))
|
||||
}
|
||||
{raisedHand ? LowerHandText : RaiseHandText}
|
||||
</Pressable>
|
||||
{
|
||||
predefinedReactions.map(([name, unified]) => (
|
||||
<EmojiButton
|
||||
key={name}
|
||||
emojiName={name}
|
||||
style={[style.button, isLandscape && style.buttonLandscape]}
|
||||
onPress={() => sendReaction({name, unified})}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {type StyleProp, type TextStyle, View} from 'react-native';
|
||||
import {type StyleProp, type TextStyle, View, type ViewStyle} from 'react-native';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import {useTheme} from '@context/theme';
|
||||
|
|
@ -13,6 +13,7 @@ type Props = {
|
|||
size: number;
|
||||
style: StyleProp<TextStyle>;
|
||||
unavailable: boolean;
|
||||
errorContainerStyle?: StyleProp<ViewStyle>;
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
|
|
@ -21,16 +22,16 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
|||
position: 'relative',
|
||||
},
|
||||
unavailable: {
|
||||
color: changeOpacity(theme.sidebarText, 0.32),
|
||||
color: changeOpacity(theme.buttonColor, 0.32),
|
||||
},
|
||||
errorContainer: {
|
||||
position: 'absolute',
|
||||
right: 0,
|
||||
backgroundColor: '#3F4350',
|
||||
backgroundColor: theme.centerChannelColor,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
borderWidth: 0.5,
|
||||
borderColor: '#3F4350',
|
||||
borderColor: theme.centerChannelColor,
|
||||
},
|
||||
errorIcon: {
|
||||
color: theme.dndIndicator,
|
||||
|
|
@ -38,7 +39,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
|||
};
|
||||
});
|
||||
|
||||
const UnavailableIconWrapper = ({name, size, style: providedStyle, unavailable}: Props) => {
|
||||
const UnavailableIconWrapper = ({name, size, style: providedStyle, unavailable, errorContainerStyle}: Props) => {
|
||||
const theme = useTheme();
|
||||
const style = getStyleSheet(theme);
|
||||
const errorIconSize = size / 2;
|
||||
|
|
@ -52,7 +53,11 @@ const UnavailableIconWrapper = ({name, size, style: providedStyle, unavailable}:
|
|||
/>
|
||||
{unavailable &&
|
||||
<View
|
||||
style={[style.errorContainer, {borderRadius: errorIconSize / 2}]}
|
||||
style={[
|
||||
style.errorContainer,
|
||||
errorContainerStyle,
|
||||
{borderRadius: errorIconSize / 2},
|
||||
]}
|
||||
>
|
||||
<CompassIcon
|
||||
name={'close-circle'}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,19 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useEffect, useState} from 'react';
|
||||
import React, {useCallback, useEffect, useMemo, useState} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {
|
||||
DeviceEventEmitter,
|
||||
Keyboard,
|
||||
type LayoutChangeEvent,
|
||||
type LayoutRectangle,
|
||||
NativeModules,
|
||||
Platform,
|
||||
Pressable,
|
||||
SafeAreaView,
|
||||
ScrollView,
|
||||
StatusBar,
|
||||
Text,
|
||||
useWindowDimensions,
|
||||
View,
|
||||
|
|
@ -31,8 +34,9 @@ import PermissionErrorBar from '@calls/components/permission_error_bar';
|
|||
import ReactionBar from '@calls/components/reaction_bar';
|
||||
import UnavailableIconWrapper from '@calls/components/unavailable_icon_wrapper';
|
||||
import {usePermissionsChecker} from '@calls/hooks';
|
||||
import {RaisedHandBanner} from '@calls/screens/call_screen/raised_hand_banner';
|
||||
import {useCallsConfig} from '@calls/state';
|
||||
import {sortParticipants} from '@calls/utils';
|
||||
import {getHandsRaised, makeCallsTheme, sortParticipants} from '@calls/utils';
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import SlideUpPanelItem, {ITEM_HEIGHT} from '@components/slide_up_panel_item';
|
||||
|
|
@ -57,11 +61,17 @@ import {freezeOtherScreens} from '@utils/gallery';
|
|||
import {bottomSheetSnapPoint} from '@utils/helpers';
|
||||
import {mergeNavigationOptions} from '@utils/navigation';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
import {displayUsername} from '@utils/user';
|
||||
|
||||
import type {CallParticipant, CurrentCall} from '@calls/types/calls';
|
||||
import type {CallParticipant, CallsTheme, CurrentCall} from '@calls/types/calls';
|
||||
import type {AvailableScreens} from '@typings/screens/navigation';
|
||||
|
||||
const avatarL = 96;
|
||||
const avatarM = 72;
|
||||
const usernameL = 110;
|
||||
const usernameM = 92;
|
||||
|
||||
export type Props = {
|
||||
componentId: AvailableScreens;
|
||||
currentCall: CurrentCall | null;
|
||||
|
|
@ -71,9 +81,10 @@ export type Props = {
|
|||
fromThreadScreen?: boolean;
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: CallsTheme) => ({
|
||||
wrapper: {
|
||||
flex: 1,
|
||||
backgroundColor: theme.callsBg,
|
||||
},
|
||||
container: {
|
||||
...Platform.select({
|
||||
|
|
@ -85,32 +96,26 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
|||
},
|
||||
}),
|
||||
flexDirection: 'column',
|
||||
backgroundColor: 'black',
|
||||
backgroundColor: theme.callsBg,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
borderRadius: 5,
|
||||
alignItems: 'center',
|
||||
},
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
width: '100%',
|
||||
paddingTop: 10,
|
||||
paddingLeft: 14,
|
||||
paddingRight: 14,
|
||||
...Platform.select({
|
||||
android: {
|
||||
elevation: 4,
|
||||
},
|
||||
ios: {
|
||||
zIndex: 4,
|
||||
},
|
||||
}),
|
||||
height: 56,
|
||||
paddingLeft: 24,
|
||||
paddingRight: 16,
|
||||
},
|
||||
headerPortraitSpacer: {
|
||||
height: 12,
|
||||
},
|
||||
headerLandscape: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
backgroundColor: 'rgba(0,0,0,0.64)',
|
||||
backgroundColor: 'rgba(0,0,0,0.5)', // not themed
|
||||
height: 52,
|
||||
paddingTop: 0,
|
||||
},
|
||||
|
|
@ -118,56 +123,83 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
|||
top: -1000,
|
||||
},
|
||||
time: {
|
||||
flex: 1,
|
||||
color: theme.sidebarText,
|
||||
color: theme.buttonColor,
|
||||
...typography('Heading', 200),
|
||||
width: 60,
|
||||
},
|
||||
collapseIconContainer: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
width: 48,
|
||||
height: 48,
|
||||
},
|
||||
collapseIcon: {
|
||||
color: changeOpacity(theme.buttonColor, 0.56),
|
||||
},
|
||||
collapseIconLandscape: {
|
||||
margin: 10,
|
||||
padding: 10,
|
||||
padding: 0,
|
||||
backgroundColor: 'transparent',
|
||||
borderRadius: 0,
|
||||
},
|
||||
usersScrollContainer: {
|
||||
flex: 1,
|
||||
width: '100%',
|
||||
},
|
||||
usersScrollContainerScreenOn: {
|
||||
marginTop: -20,
|
||||
},
|
||||
usersScrollViewCentered: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
users: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
alignContent: 'flex-start',
|
||||
},
|
||||
usersScrollLandscapeScreenOn: {
|
||||
position: 'absolute',
|
||||
height: 0,
|
||||
},
|
||||
user: {
|
||||
flexGrow: 1,
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
marginTop: 10,
|
||||
marginBottom: 10,
|
||||
marginLeft: 10,
|
||||
marginRight: 10,
|
||||
margin: 10,
|
||||
},
|
||||
userScreenOn: {
|
||||
marginTop: 0,
|
||||
marginTop: 5,
|
||||
marginBottom: 0,
|
||||
},
|
||||
username: {
|
||||
color: theme.sidebarText,
|
||||
marginTop: 10,
|
||||
width: usernameL,
|
||||
textAlign: 'center',
|
||||
color: theme.buttonColor,
|
||||
...typography('Body', 100, 'SemiBold'),
|
||||
},
|
||||
usernameShort: {
|
||||
marginTop: 0,
|
||||
width: usernameM,
|
||||
},
|
||||
buttonsContainer: {
|
||||
alignItems: 'center',
|
||||
},
|
||||
buttons: {
|
||||
flexDirection: 'column',
|
||||
backgroundColor: 'rgba(255,255,255,0.16)',
|
||||
width: '100%',
|
||||
paddingBottom: 10,
|
||||
...Platform.select({
|
||||
android: {
|
||||
elevation: 4,
|
||||
},
|
||||
ios: {
|
||||
zIndex: 4,
|
||||
},
|
||||
}),
|
||||
alignItems: 'center',
|
||||
paddingBottom: 12,
|
||||
borderTopLeftRadius: 20,
|
||||
borderTopRightRadius: 20,
|
||||
gap: 4,
|
||||
backgroundColor: changeOpacity(theme.buttonColor, 0.08),
|
||||
},
|
||||
buttonsLandscape: {
|
||||
height: 110,
|
||||
position: 'absolute',
|
||||
backgroundColor: 'rgba(0,0,0,0.64)',
|
||||
backgroundColor: 'rgba(0,0,0,0.5)', // not themed
|
||||
width: '100%',
|
||||
bottom: 0,
|
||||
paddingTop: 16,
|
||||
borderTopLeftRadius: 0,
|
||||
borderTopRightRadius: 0,
|
||||
},
|
||||
buttonsLandscapeWithReactions: {
|
||||
height: 174,
|
||||
|
|
@ -184,25 +216,26 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
|||
flex: 0,
|
||||
},
|
||||
mute: {
|
||||
flexDirection: 'column',
|
||||
alignSelf: 'stretch',
|
||||
alignItems: 'center',
|
||||
gap: 4,
|
||||
padding: 24,
|
||||
backgroundColor: '#3DB887',
|
||||
backgroundColor: theme.onlineIndicator,
|
||||
borderRadius: 20,
|
||||
marginBottom: 10,
|
||||
marginTop: 20,
|
||||
marginLeft: 16,
|
||||
marginRight: 16,
|
||||
marginTop: 20,
|
||||
marginBottom: 20,
|
||||
},
|
||||
muteMuted: {
|
||||
backgroundColor: 'rgba(255,255,255,0.16)',
|
||||
backgroundColor: changeOpacity(theme.buttonColor, 0.12),
|
||||
},
|
||||
speakerphoneIcon: {
|
||||
color: theme.sidebarText,
|
||||
backgroundColor: 'rgba(255,255,255,0.12)',
|
||||
backgroundColor: changeOpacity(theme.buttonColor, 0.12),
|
||||
},
|
||||
buttonOn: {
|
||||
color: 'black',
|
||||
color: theme.callsBg,
|
||||
backgroundColor: 'white',
|
||||
},
|
||||
otherButtons: {
|
||||
|
|
@ -212,58 +245,47 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
|||
otherButtonsLandscape: {
|
||||
justifyContent: 'center',
|
||||
},
|
||||
collapseIcon: {
|
||||
color: theme.sidebarText,
|
||||
margin: 10,
|
||||
padding: 10,
|
||||
backgroundColor: 'rgba(255,255,255,0.12)',
|
||||
borderRadius: 4,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
collapseIconLandscape: {
|
||||
margin: 10,
|
||||
padding: 0,
|
||||
backgroundColor: 'transparent',
|
||||
borderRadius: 0,
|
||||
},
|
||||
muteIcon: {
|
||||
color: theme.sidebarText,
|
||||
color: theme.buttonColor,
|
||||
},
|
||||
muteIconLandscape: {
|
||||
backgroundColor: '#3DB887',
|
||||
backgroundColor: theme.onlineIndicator,
|
||||
padding: 11,
|
||||
},
|
||||
muteIconLandscapeMuted: {
|
||||
backgroundColor: 'rgba(255,255,255,0.16)',
|
||||
backgroundColor: changeOpacity(theme.buttonColor, 0.12),
|
||||
},
|
||||
buttonText: {
|
||||
color: theme.sidebarText,
|
||||
color: changeOpacity(theme.buttonColor, 0.72),
|
||||
...typography('Body', 75, 'SemiBold'),
|
||||
},
|
||||
buttonIcon: {
|
||||
color: theme.sidebarText,
|
||||
backgroundColor: 'rgba(255,255,255,0.12)',
|
||||
color: theme.buttonColor,
|
||||
backgroundColor: changeOpacity(theme.buttonColor, 0.08),
|
||||
borderRadius: 34,
|
||||
padding: 22,
|
||||
padding: 18,
|
||||
width: 68,
|
||||
height: 68,
|
||||
margin: 10,
|
||||
marginBottom: 8,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
buttonIconLandscape: {
|
||||
borderRadius: 26,
|
||||
paddingTop: 14,
|
||||
paddingRight: 16,
|
||||
paddingBottom: 16,
|
||||
paddingLeft: 14,
|
||||
padding: 10,
|
||||
width: 52,
|
||||
height: 52,
|
||||
marginLeft: 12,
|
||||
marginRight: 12,
|
||||
},
|
||||
errorContainerLandscape: {
|
||||
right: 20,
|
||||
top: 10,
|
||||
},
|
||||
hangUpIcon: {
|
||||
backgroundColor: Preferences.THEMES.denim.dndIndicator,
|
||||
},
|
||||
screenShareImage: {
|
||||
flex: 7,
|
||||
flex: 2,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
alignItems: 'center',
|
||||
|
|
@ -273,7 +295,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
|||
margin: 3,
|
||||
},
|
||||
unavailableText: {
|
||||
color: changeOpacity(theme.sidebarText, 0.32),
|
||||
color: changeOpacity(theme.buttonColor, 0.32),
|
||||
},
|
||||
denimDND: {
|
||||
color: Preferences.THEMES.denim.dndIndicator,
|
||||
|
|
@ -298,11 +320,18 @@ const CallScreen = ({
|
|||
usePermissionsChecker(micPermissionsGranted);
|
||||
const [showControlsInLandscape, setShowControlsInLandscape] = useState(false);
|
||||
const [showReactions, setShowReactions] = useState(false);
|
||||
const callsTheme = useMemo(() => makeCallsTheme(theme), [theme]);
|
||||
const style = getStyleSheet(callsTheme);
|
||||
const [centerUsers, setCenterUsers] = useState(false);
|
||||
const [layout, setLayout] = useState<LayoutRectangle | null>(null);
|
||||
|
||||
const style = getStyleSheet(theme);
|
||||
const isLandscape = width > height;
|
||||
const myParticipant = currentCall?.participants[currentCall.myUserId];
|
||||
const micPermissionsError = !micPermissionsGranted && !currentCall?.micPermissionsErrorDismissed;
|
||||
const screenShareOn = Boolean(currentCall?.screenOn);
|
||||
const isLandscape = width > height;
|
||||
const smallerAvatar = isLandscape || screenShareOn;
|
||||
const avatarSize = smallerAvatar ? avatarM : avatarL;
|
||||
const numParticipants = Object.keys(participantsDict).length;
|
||||
|
||||
const callThreadOptionTitle = intl.formatMessage({id: 'mobile.calls_call_thread', defaultMessage: 'Call Thread'});
|
||||
const recordOptionTitle = intl.formatMessage({id: 'mobile.calls_record', defaultMessage: 'Record'});
|
||||
|
|
@ -318,7 +347,7 @@ const CallScreen = ({
|
|||
useEffect(() => {
|
||||
mergeNavigationOptions('Call', {
|
||||
layout: {
|
||||
componentBackgroundColor: 'black',
|
||||
componentBackgroundColor: callsTheme.callsBg,
|
||||
orientation: allOrientations,
|
||||
},
|
||||
topBar: {
|
||||
|
|
@ -426,13 +455,15 @@ const CallScreen = ({
|
|||
// The user should see the loading only if:
|
||||
// - Recording has been initialized, recording has not been started, and recording has not ended
|
||||
const waitingForRecording = Boolean(currentCall?.recState?.init_at && !currentCall.recState.start_at && !currentCall.recState.end_at && isHost);
|
||||
const showStartRecording = isHost && EnableRecordings && !(waitingForRecording || recording);
|
||||
const showStopRecording = isHost && EnableRecordings && (waitingForRecording || recording);
|
||||
|
||||
const showOtherActions = useCallback(async () => {
|
||||
const renderContent = () => {
|
||||
return (
|
||||
<View>
|
||||
{
|
||||
isHost && EnableRecordings && !(waitingForRecording || recording) &&
|
||||
showStartRecording &&
|
||||
<SlideUpPanelItem
|
||||
icon={'record-circle-outline'}
|
||||
onPress={startRecording}
|
||||
|
|
@ -440,7 +471,7 @@ const CallScreen = ({
|
|||
/>
|
||||
}
|
||||
{
|
||||
isHost && EnableRecordings && (waitingForRecording || recording) &&
|
||||
showStopRecording &&
|
||||
<SlideUpPanelItem
|
||||
icon={'record-square-outline'}
|
||||
onPress={stopRecording}
|
||||
|
|
@ -493,6 +524,31 @@ const CallScreen = ({
|
|||
return () => didDismissListener.remove();
|
||||
}, [isTablet]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!layout || !layout.height || !layout.width) {
|
||||
return;
|
||||
}
|
||||
|
||||
const avatarCellHeight = avatarSize + 20 + 20 + 20; // avatar + name + host pill + padding
|
||||
const usernameSize = smallerAvatar ? usernameM : usernameL;
|
||||
const avatarCellWidth = usernameSize + 20; // name width + padding
|
||||
|
||||
const perRow = Math.floor(layout.width / avatarCellWidth);
|
||||
const totalHeight = Math.ceil(numParticipants / perRow) * avatarCellHeight;
|
||||
const totalWidth = numParticipants * avatarCellWidth;
|
||||
|
||||
// If screenShareOn, we care about width, otherwise we care about height.
|
||||
if ((screenShareOn && totalWidth > layout.width) || (!screenShareOn && totalHeight > layout.height)) {
|
||||
setCenterUsers(false);
|
||||
} else {
|
||||
setCenterUsers(true);
|
||||
}
|
||||
}, [layout, numParticipants]);
|
||||
|
||||
const onLayout = useCallback((e: LayoutChangeEvent) => {
|
||||
setLayout(e.nativeEvent.layout);
|
||||
}, []);
|
||||
|
||||
if (!currentCall || !myParticipant) {
|
||||
// Note: this happens because the screen is "rendered", even after the screen has been popped, and the
|
||||
// currentCall will have already been set to null when those extra renders run. We probably don't ever need
|
||||
|
|
@ -505,7 +561,7 @@ const CallScreen = ({
|
|||
}
|
||||
|
||||
let screenShareView = null;
|
||||
if (currentCall.screenShareURL && currentCall.screenOn) {
|
||||
if (currentCall.screenShareURL && screenShareOn) {
|
||||
screenShareView = (
|
||||
<Pressable
|
||||
testID='screen-share-container'
|
||||
|
|
@ -528,48 +584,55 @@ const CallScreen = ({
|
|||
);
|
||||
}
|
||||
|
||||
const participants = sortParticipants(teammateNameDisplay, participantsDict, currentCall.screenOn);
|
||||
const raisedHands = getHandsRaised(participantsDict);
|
||||
const participants = sortParticipants(intl.locale, teammateNameDisplay, participantsDict, currentCall.screenOn);
|
||||
let usersList = null;
|
||||
if (!currentCall.screenOn || !isLandscape) {
|
||||
if (!screenShareOn || !isLandscape) {
|
||||
usersList = (
|
||||
<ScrollView
|
||||
alwaysBounceVertical={false}
|
||||
horizontal={currentCall.screenOn !== ''}
|
||||
contentContainerStyle={[isLandscape && Boolean(currentCall.screenOn) && style.usersScrollLandscapeScreenOn]}
|
||||
>
|
||||
<Pressable
|
||||
testID='users-list'
|
||||
onPress={toggleControlsInLandscape}
|
||||
style={style.users}
|
||||
<View style={[style.usersScrollContainer, screenShareOn && style.usersScrollContainerScreenOn]}>
|
||||
<ScrollView
|
||||
alwaysBounceVertical={false}
|
||||
horizontal={screenShareOn}
|
||||
onLayout={onLayout}
|
||||
contentContainerStyle={centerUsers && style.usersScrollViewCentered}
|
||||
>
|
||||
{participants.map((user) => {
|
||||
return (
|
||||
<View
|
||||
style={[style.user, Boolean(currentCall.screenOn) && style.userScreenOn]}
|
||||
key={user.id}
|
||||
>
|
||||
<CallAvatar
|
||||
userModel={user.userModel}
|
||||
volume={currentCall.voiceOn[user.id] ? 1 : 0}
|
||||
muted={user.muted}
|
||||
sharingScreen={user.id === currentCall.screenOn}
|
||||
raisedHand={Boolean(user.raisedHand)}
|
||||
reaction={user.reaction?.emoji}
|
||||
size={currentCall.screenOn ? 'm' : 'l'}
|
||||
serverUrl={currentCall.serverUrl}
|
||||
/>
|
||||
<Text style={style.username}>
|
||||
{displayUsername(user.userModel, intl.locale, teammateNameDisplay)}
|
||||
{user.id === myParticipant.id &&
|
||||
` ${intl.formatMessage({id: 'mobile.calls_you', defaultMessage: '(you)'})}`
|
||||
}
|
||||
</Text>
|
||||
{user.id === currentCall.hostId && <CallsBadge type={CallsBadgeType.Host}/>}
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
</Pressable>
|
||||
</ScrollView>
|
||||
<Pressable
|
||||
testID='users-list'
|
||||
onPress={toggleControlsInLandscape}
|
||||
style={style.users}
|
||||
>
|
||||
{participants.map((user) => {
|
||||
return (
|
||||
<View
|
||||
style={[style.user, screenShareOn && style.userScreenOn]}
|
||||
key={user.id}
|
||||
>
|
||||
<CallAvatar
|
||||
userModel={user.userModel}
|
||||
volume={currentCall.voiceOn[user.id] ? 1 : 0}
|
||||
muted={user.muted}
|
||||
sharingScreen={user.id === currentCall.screenOn}
|
||||
raisedHand={Boolean(user.raisedHand)}
|
||||
reaction={user.reaction?.emoji}
|
||||
size={avatarSize}
|
||||
serverUrl={currentCall.serverUrl}
|
||||
/>
|
||||
<Text
|
||||
style={[style.username, smallerAvatar && style.usernameShort]}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{displayUsername(user.userModel, intl.locale, teammateNameDisplay)}
|
||||
{user.id === myParticipant.id &&
|
||||
` ${intl.formatMessage({id: 'mobile.calls_you', defaultMessage: '(you)'})}`
|
||||
}
|
||||
</Text>
|
||||
{user.id === currentCall.hostId && <CallsBadge type={CallsBadgeType.Host}/>}
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
</Pressable>
|
||||
</ScrollView>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -586,141 +649,205 @@ const CallScreen = ({
|
|||
style={[style.buttonText, !micPermissionsGranted && style.unavailableText]}
|
||||
/>);
|
||||
|
||||
const header = (
|
||||
<View
|
||||
style={[
|
||||
style.header,
|
||||
isLandscape && style.headerLandscape,
|
||||
isLandscape && !showControlsInLandscape && style.headerLandscapeNoControls,
|
||||
]}
|
||||
>
|
||||
{waitingForRecording && <CallsBadge type={CallsBadgeType.Waiting}/>}
|
||||
{recording && <CallsBadge type={CallsBadgeType.Rec}/>}
|
||||
<CallDuration
|
||||
style={style.time}
|
||||
value={currentCall.startTime}
|
||||
updateIntervalInSeconds={1}
|
||||
/>
|
||||
<RaisedHandBanner
|
||||
raisedHands={raisedHands}
|
||||
currentUserId={currentCall.myUserId}
|
||||
teammateNameDisplay={teammateNameDisplay}
|
||||
/>
|
||||
<Pressable
|
||||
onPress={() => popTopScreen()}
|
||||
style={style.collapseIconContainer}
|
||||
>
|
||||
<CompassIcon
|
||||
name='arrow-collapse'
|
||||
size={28}
|
||||
style={[style.collapseIcon, isLandscape && style.collapseIconLandscape]}
|
||||
/>
|
||||
</Pressable>
|
||||
</View>
|
||||
);
|
||||
|
||||
return (
|
||||
<SafeAreaView style={style.wrapper}>
|
||||
<StatusBar barStyle={'light-content'}/>
|
||||
<View style={style.container}>
|
||||
<View
|
||||
style={[
|
||||
style.header,
|
||||
isLandscape && style.headerLandscape,
|
||||
isLandscape && !showControlsInLandscape && style.headerLandscapeNoControls,
|
||||
]}
|
||||
>
|
||||
{waitingForRecording && <CallsBadge type={CallsBadgeType.Waiting}/>}
|
||||
{recording && <CallsBadge type={CallsBadgeType.Rec}/>}
|
||||
<CallDuration
|
||||
style={style.time}
|
||||
value={currentCall.startTime}
|
||||
updateIntervalInSeconds={1}
|
||||
/>
|
||||
<Pressable onPress={() => popTopScreen()}>
|
||||
<CompassIcon
|
||||
name='arrow-collapse'
|
||||
size={24}
|
||||
style={[style.collapseIcon, isLandscape && style.collapseIconLandscape]}
|
||||
/>
|
||||
</Pressable>
|
||||
</View>
|
||||
{!isLandscape && header}
|
||||
{!isLandscape && <View style={style.headerPortraitSpacer}/>}
|
||||
{usersList}
|
||||
{screenShareView}
|
||||
{micPermissionsError && <PermissionErrorBar/>}
|
||||
{!isLandscape &&
|
||||
{isLandscape && header}
|
||||
{!isLandscape && currentCall.reactionStream.length > 0 &&
|
||||
<EmojiList reactionStream={currentCall.reactionStream}/>
|
||||
}
|
||||
<View
|
||||
style={[
|
||||
style.buttons,
|
||||
isLandscape && style.buttonsLandscape,
|
||||
isLandscape && showReactions && style.buttonsLandscapeWithReactions,
|
||||
isLandscape && !showControlsInLandscape && style.buttonsLandscapeNoControls,
|
||||
]}
|
||||
>
|
||||
{showReactions &&
|
||||
<ReactionBar raisedHand={myParticipant.raisedHand}/>
|
||||
}
|
||||
{!isLandscape &&
|
||||
<Pressable
|
||||
testID='mute-unmute'
|
||||
style={[style.mute, myParticipant.muted && style.muteMuted]}
|
||||
onPress={muteUnmuteHandler}
|
||||
disabled={!micPermissionsGranted}
|
||||
>
|
||||
<UnavailableIconWrapper
|
||||
name={myParticipant.muted ? 'microphone-off' : 'microphone'}
|
||||
size={24}
|
||||
unavailable={!micPermissionsGranted}
|
||||
style={style.muteIcon}
|
||||
/>
|
||||
{myParticipant.muted ? UnmuteText : MuteText}
|
||||
</Pressable>
|
||||
}
|
||||
<View style={[style.otherButtons, isLandscape && style.otherButtonsLandscape]}>
|
||||
<Pressable
|
||||
testID='leave'
|
||||
style={[style.button, isLandscape && style.buttonLandscape]}
|
||||
onPress={leaveCallHandler}
|
||||
>
|
||||
<CompassIcon
|
||||
name='phone-hangup'
|
||||
size={24}
|
||||
style={[style.buttonIcon, isLandscape && style.buttonIconLandscape, style.hangUpIcon]}
|
||||
/>
|
||||
<FormattedText
|
||||
id={'mobile.calls_leave'}
|
||||
defaultMessage={'Leave'}
|
||||
style={style.buttonText}
|
||||
/>
|
||||
</Pressable>
|
||||
<AudioDeviceButton
|
||||
pressableStyle={[style.button, isLandscape && style.buttonLandscape]}
|
||||
iconStyle={[
|
||||
style.buttonIcon,
|
||||
isLandscape && style.buttonIconLandscape,
|
||||
style.speakerphoneIcon,
|
||||
currentCall.speakerphoneOn && style.buttonOn,
|
||||
]}
|
||||
buttonTextStyle={style.buttonText}
|
||||
currentCall={currentCall}
|
||||
/>
|
||||
<Pressable
|
||||
style={[style.button, isLandscape && style.buttonLandscape]}
|
||||
onPress={toggleReactions}
|
||||
>
|
||||
<CompassIcon
|
||||
name={'emoticon-happy-outline'}
|
||||
size={24}
|
||||
style={[style.buttonIcon, isLandscape && style.buttonIconLandscape, showReactions && style.buttonOn]}
|
||||
/>
|
||||
<FormattedText
|
||||
id={'mobile.calls_react'}
|
||||
defaultMessage={'React'}
|
||||
style={style.buttonText}
|
||||
/>
|
||||
</Pressable>
|
||||
<Pressable
|
||||
style={[style.button, isLandscape && style.buttonLandscape]}
|
||||
onPress={showOtherActions}
|
||||
>
|
||||
<CompassIcon
|
||||
name='dots-horizontal'
|
||||
size={24}
|
||||
style={[style.buttonIcon, isLandscape && style.buttonIconLandscape]}
|
||||
/>
|
||||
<FormattedText
|
||||
id={'mobile.calls_more'}
|
||||
defaultMessage={'More'}
|
||||
style={style.buttonText}
|
||||
/>
|
||||
</Pressable>
|
||||
{isLandscape &&
|
||||
{micPermissionsError && <PermissionErrorBar/>}
|
||||
<View style={[style.buttonsContainer]}>
|
||||
<View
|
||||
style={[
|
||||
style.buttons,
|
||||
isLandscape && style.buttonsLandscape,
|
||||
isLandscape && showReactions && style.buttonsLandscapeWithReactions,
|
||||
isLandscape && !showControlsInLandscape && style.buttonsLandscapeNoControls,
|
||||
]}
|
||||
>
|
||||
{showReactions &&
|
||||
<ReactionBar raisedHand={myParticipant.raisedHand}/>
|
||||
}
|
||||
{!isLandscape &&
|
||||
<Pressable
|
||||
testID='mute-unmute'
|
||||
style={[style.button, style.buttonLandscape]}
|
||||
style={[style.mute, myParticipant.muted && style.muteMuted]}
|
||||
onPress={muteUnmuteHandler}
|
||||
disabled={!micPermissionsGranted}
|
||||
>
|
||||
<CompassIcon
|
||||
<UnavailableIconWrapper
|
||||
name={myParticipant.muted ? 'microphone-off' : 'microphone'}
|
||||
size={24}
|
||||
style={[
|
||||
style.buttonIcon,
|
||||
isLandscape && style.buttonIconLandscape,
|
||||
style.muteIconLandscape,
|
||||
myParticipant?.muted && style.muteIconLandscapeMuted,
|
||||
]}
|
||||
size={32}
|
||||
unavailable={!micPermissionsGranted}
|
||||
style={style.muteIcon}
|
||||
/>
|
||||
{myParticipant.muted ? UnmuteText : MuteText}
|
||||
</Pressable>
|
||||
}
|
||||
<View style={[style.otherButtons, isLandscape && style.otherButtonsLandscape]}>
|
||||
<Pressable
|
||||
testID='leave'
|
||||
style={[style.button, isLandscape && style.buttonLandscape]}
|
||||
onPress={leaveCallHandler}
|
||||
>
|
||||
<CompassIcon
|
||||
name='phone-hangup'
|
||||
size={32}
|
||||
style={[style.buttonIcon, isLandscape && style.buttonIconLandscape, style.hangUpIcon]}
|
||||
/>
|
||||
<FormattedText
|
||||
id={'mobile.calls_leave'}
|
||||
defaultMessage={'Leave'}
|
||||
style={style.buttonText}
|
||||
/>
|
||||
</Pressable>
|
||||
<AudioDeviceButton
|
||||
pressableStyle={[style.button, isLandscape && style.buttonLandscape]}
|
||||
iconStyle={[
|
||||
style.buttonIcon,
|
||||
isLandscape && style.buttonIconLandscape,
|
||||
style.speakerphoneIcon,
|
||||
currentCall.speakerphoneOn && style.buttonOn,
|
||||
]}
|
||||
buttonTextStyle={style.buttonText}
|
||||
currentCall={currentCall}
|
||||
/>
|
||||
<Pressable
|
||||
style={[style.button, isLandscape && style.buttonLandscape]}
|
||||
onPress={toggleReactions}
|
||||
>
|
||||
<CompassIcon
|
||||
name={'emoticon-happy-outline'}
|
||||
size={32}
|
||||
style={[style.buttonIcon, isLandscape && style.buttonIconLandscape, showReactions && style.buttonOn]}
|
||||
/>
|
||||
<FormattedText
|
||||
id={'mobile.calls_react'}
|
||||
defaultMessage={'React'}
|
||||
style={style.buttonText}
|
||||
/>
|
||||
</Pressable>
|
||||
{!isLandscape && isHost &&
|
||||
<Pressable
|
||||
style={[style.button, isLandscape && style.buttonLandscape]}
|
||||
onPress={showOtherActions}
|
||||
>
|
||||
<CompassIcon
|
||||
name='dots-horizontal'
|
||||
size={32}
|
||||
style={[style.buttonIcon, isLandscape && style.buttonIconLandscape]}
|
||||
/>
|
||||
<FormattedText
|
||||
id={'mobile.calls_more'}
|
||||
defaultMessage={'More'}
|
||||
style={style.buttonText}
|
||||
/>
|
||||
</Pressable>
|
||||
}
|
||||
{isLandscape &&
|
||||
<Pressable
|
||||
testID='mute-unmute'
|
||||
style={[style.button, style.buttonLandscape]}
|
||||
onPress={muteUnmuteHandler}
|
||||
>
|
||||
<UnavailableIconWrapper
|
||||
name={myParticipant.muted ? 'microphone-off' : 'microphone'}
|
||||
size={32}
|
||||
unavailable={!micPermissionsGranted}
|
||||
style={[
|
||||
style.buttonIcon,
|
||||
isLandscape && style.buttonIconLandscape,
|
||||
style.muteIconLandscape,
|
||||
myParticipant?.muted && style.muteIconLandscapeMuted,
|
||||
]}
|
||||
errorContainerStyle={isLandscape && style.errorContainerLandscape}
|
||||
/>
|
||||
{myParticipant.muted ? UnmuteText : MuteText}
|
||||
</Pressable>
|
||||
}
|
||||
{(isLandscape || !isHost) &&
|
||||
<Pressable
|
||||
style={[style.button, isLandscape && style.buttonLandscape]}
|
||||
onPress={switchToThread}
|
||||
>
|
||||
<CompassIcon
|
||||
name='message-text-outline'
|
||||
size={32}
|
||||
style={[style.buttonIcon, isLandscape && style.buttonIconLandscape]}
|
||||
/>
|
||||
<FormattedText
|
||||
id={'mobile.calls_thread'}
|
||||
defaultMessage={'Thread'}
|
||||
style={style.buttonText}
|
||||
/>
|
||||
</Pressable>
|
||||
}
|
||||
{isLandscape && showStartRecording &&
|
||||
<Pressable
|
||||
style={[style.button, isLandscape && style.buttonLandscape]}
|
||||
onPress={startRecording}
|
||||
>
|
||||
<CompassIcon
|
||||
name='record-circle-outline'
|
||||
size={32}
|
||||
style={[style.buttonIcon, isLandscape && style.buttonIconLandscape]}
|
||||
/>
|
||||
<Text style={style.buttonText}>{recordOptionTitle}</Text>
|
||||
</Pressable>
|
||||
}
|
||||
{isLandscape && showStopRecording &&
|
||||
<Pressable
|
||||
style={[style.button, isLandscape && style.buttonLandscape]}
|
||||
onPress={stopRecording}
|
||||
>
|
||||
<CompassIcon
|
||||
name='record-square-outline'
|
||||
size={32}
|
||||
style={[style.buttonIcon, isLandscape && style.buttonIconLandscape]}
|
||||
/>
|
||||
<Text style={style.buttonText}>{stopRecordingOptionTitle}</Text>
|
||||
</Pressable>
|
||||
}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,90 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useMemo} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {Text, View} from 'react-native';
|
||||
|
||||
import {getHandsRaisedNames, makeCallsTheme} from '@calls/utils';
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
import type {CallParticipant, CallsTheme} from '@calls/types/calls';
|
||||
|
||||
export type Props = {
|
||||
raisedHands: CallParticipant[];
|
||||
currentUserId: string;
|
||||
teammateNameDisplay: string;
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: CallsTheme) => ({
|
||||
raisedHandBannerContainer: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
raisedHandBanner: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingTop: 4,
|
||||
paddingRight: 18,
|
||||
paddingBottom: 4,
|
||||
paddingLeft: 6,
|
||||
marginRight: 6,
|
||||
marginLeft: 4,
|
||||
gap: 4,
|
||||
borderRadius: 18,
|
||||
backgroundColor: theme.sidebarText,
|
||||
},
|
||||
raisedHandIcon: {
|
||||
color: theme.awayIndicator,
|
||||
},
|
||||
raisedHandName: {
|
||||
...typography('Body', 100, 'SemiBold'),
|
||||
color: theme.sidebarTeamBarBg,
|
||||
},
|
||||
raisedHandText: {
|
||||
...typography(),
|
||||
color: theme.sidebarTeamBarBg,
|
||||
},
|
||||
}));
|
||||
|
||||
export const RaisedHandBanner = ({raisedHands, currentUserId, teammateNameDisplay}: Props) => {
|
||||
const intl = useIntl();
|
||||
const theme = useTheme();
|
||||
const callsTheme = useMemo(() => makeCallsTheme(theme), [theme]);
|
||||
const style = getStyleSheet(callsTheme);
|
||||
|
||||
if (raisedHands.length === 0) {
|
||||
return <View style={style.raisedHandBannerContainer}/>;
|
||||
}
|
||||
|
||||
const names = getHandsRaisedNames(raisedHands, currentUserId, intl.locale, teammateNameDisplay, intl);
|
||||
|
||||
return (
|
||||
<View style={style.raisedHandBannerContainer}>
|
||||
<View style={style.raisedHandBanner}>
|
||||
<CompassIcon
|
||||
name={'hand-right'}
|
||||
size={16}
|
||||
style={style.raisedHandIcon}
|
||||
/>
|
||||
<FormattedText
|
||||
style={style.raisedHandText}
|
||||
id='mobile.calls_raised_hand'
|
||||
defaultMessage='<bold>{name} {num, plural, =0 {} other {+# more }}</bold>raised a hand'
|
||||
values={{
|
||||
name: names[0],
|
||||
num: names.length - 1,
|
||||
bold: (str: string) => <Text style={style.raisedHandName}>{str}</Text>,
|
||||
}}
|
||||
numberOfLines={1}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
|
@ -136,6 +136,12 @@ export type ReactionStreamEmoji = {
|
|||
literal?: string;
|
||||
};
|
||||
|
||||
export type CallsTheme = Theme & {
|
||||
callsBg: string;
|
||||
callsBgRgb: string;
|
||||
callsBadgeBg: string;
|
||||
};
|
||||
|
||||
export type AudioDeviceInfoRaw = {
|
||||
availableAudioDeviceList: string;
|
||||
selectedAudioDevice: AudioDevice;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {makeCallsBaseAndBadgeRGB, rgbToCSS} from '@mattermost/calls/lib/utils';
|
||||
import {Alert} from 'react-native';
|
||||
|
||||
import {Post} from '@constants';
|
||||
|
|
@ -8,26 +9,26 @@ import Calls from '@constants/calls';
|
|||
import {isMinimumServerVersion} from '@utils/helpers';
|
||||
import {displayUsername} from '@utils/user';
|
||||
|
||||
import type {CallParticipant} from '@calls/types/calls';
|
||||
import type {CallParticipant, CallsTheme} from '@calls/types/calls';
|
||||
import type {CallsConfig} from '@mattermost/calls/lib/types';
|
||||
import type PostModel from '@typings/database/models/servers/post';
|
||||
import type {IntlShape} from 'react-intl';
|
||||
import type {RTCIceServer} from 'react-native-webrtc';
|
||||
|
||||
export function sortParticipants(teammateNameDisplay: string, participants?: Dictionary<CallParticipant>, presenterID?: string): CallParticipant[] {
|
||||
export function sortParticipants(locale: string, teammateNameDisplay: string, participants?: Dictionary<CallParticipant>, presenterID?: string): CallParticipant[] {
|
||||
if (!participants) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const users = Object.values(participants);
|
||||
|
||||
return users.sort(sortByName(teammateNameDisplay)).sort(sortByState(presenterID));
|
||||
return users.sort(sortByName(locale, teammateNameDisplay)).sort(sortByState(presenterID));
|
||||
}
|
||||
|
||||
const sortByName = (teammateNameDisplay: string) => {
|
||||
const sortByName = (locale: string, teammateNameDisplay: string) => {
|
||||
return (a: CallParticipant, b: CallParticipant) => {
|
||||
const nameA = displayUsername(a.userModel, teammateNameDisplay);
|
||||
const nameB = displayUsername(b.userModel, teammateNameDisplay);
|
||||
const nameA = displayUsername(a.userModel, locale, teammateNameDisplay);
|
||||
const nameB = displayUsername(b.userModel, locale, teammateNameDisplay);
|
||||
return nameA.localeCompare(nameB);
|
||||
};
|
||||
};
|
||||
|
|
@ -58,6 +59,19 @@ const sortByState = (presenterID?: string) => {
|
|||
};
|
||||
};
|
||||
|
||||
export function getHandsRaised(participants: Dictionary<CallParticipant>) {
|
||||
return Object.values(participants).filter((p) => p.raisedHand);
|
||||
}
|
||||
|
||||
export function getHandsRaisedNames(participants: CallParticipant[], currentUserId: string, locale: string, teammateNameDisplay: string, intl: IntlShape) {
|
||||
return participants.sort((a, b) => a.raisedHand - b.raisedHand).map((p) => {
|
||||
if (p.id === currentUserId) {
|
||||
return intl.formatMessage({id: 'mobile.calls_you_2', defaultMessage: 'You'});
|
||||
}
|
||||
return displayUsername(p.userModel, locale, teammateNameDisplay);
|
||||
});
|
||||
}
|
||||
|
||||
export function isSupportedServerCalls(serverVersion?: string) {
|
||||
if (serverVersion) {
|
||||
return isMinimumServerVersion(
|
||||
|
|
@ -126,3 +140,14 @@ export function getICEServersConfigs(config: CallsConfig): RTCIceServer[] {
|
|||
|
||||
return [];
|
||||
}
|
||||
|
||||
export function makeCallsTheme(theme: Theme): CallsTheme {
|
||||
const {baseColorRGB, badgeBgRGB} = makeCallsBaseAndBadgeRGB(theme.sidebarBg);
|
||||
|
||||
const newTheme = {...theme} as CallsTheme;
|
||||
newTheme.callsBg = rgbToCSS(baseColorRGB);
|
||||
newTheme.callsBgRgb = `${baseColorRGB.r},${baseColorRGB.g},${baseColorRGB.b}`;
|
||||
newTheme.callsBadgeBg = rgbToCSS(badgeBgRGB);
|
||||
|
||||
return newTheme;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,9 +5,8 @@ import React, {useCallback, useEffect, useRef, useState} from 'react';
|
|||
import {type LayoutChangeEvent, StyleSheet, View} from 'react-native';
|
||||
import {type Edge, SafeAreaView, useSafeAreaInsets} from 'react-native-safe-area-context';
|
||||
|
||||
import CurrentCallBar from '@calls/components/current_call_bar';
|
||||
import FloatingCallContainer from '@calls/components/floating_call_container';
|
||||
import JoinCallBanner from '@calls/components/join_call_banner';
|
||||
import {RoundedHeaderCalls} from '@calls/components/join_call_banner/rounded_header_calls';
|
||||
import FreezeScreen from '@components/freeze_screen';
|
||||
import PostDraft from '@components/post_draft';
|
||||
import {Screens} from '@constants';
|
||||
|
|
@ -28,7 +27,6 @@ import type {AvailableScreens} from '@typings/screens/navigation';
|
|||
import type {KeyboardTrackingViewRef} from 'react-native-keyboard-tracking-view';
|
||||
|
||||
type ChannelProps = {
|
||||
serverUrl: string;
|
||||
channelId: string;
|
||||
componentId?: AvailableScreens;
|
||||
isCallInCurrentChannel: boolean;
|
||||
|
|
@ -48,7 +46,6 @@ const styles = StyleSheet.create({
|
|||
});
|
||||
|
||||
const Channel = ({
|
||||
serverUrl,
|
||||
channelId,
|
||||
componentId,
|
||||
isCallInCurrentChannel,
|
||||
|
|
@ -97,21 +94,8 @@ const Channel = ({
|
|||
setContainerHeight(e.nativeEvent.layout.height);
|
||||
}, []);
|
||||
|
||||
let callsComponents: JSX.Element | null = null;
|
||||
const showJoinCallBanner = isCallInCurrentChannel && !isInCurrentChannelCall;
|
||||
if (showJoinCallBanner || isInACall) {
|
||||
callsComponents = (
|
||||
<FloatingCallContainer>
|
||||
{showJoinCallBanner &&
|
||||
<JoinCallBanner
|
||||
serverUrl={serverUrl}
|
||||
channelId={channelId}
|
||||
/>
|
||||
}
|
||||
{isInACall && <CurrentCallBar/>}
|
||||
</FloatingCallContainer>
|
||||
);
|
||||
}
|
||||
const renderCallsComponents = showJoinCallBanner || isInACall;
|
||||
|
||||
return (
|
||||
<FreezeScreen>
|
||||
|
|
@ -128,6 +112,7 @@ const Channel = ({
|
|||
callsEnabledInChannel={isCallsEnabledInChannel}
|
||||
isTabletView={isTabletView}
|
||||
/>
|
||||
{showJoinCallBanner && <RoundedHeaderCalls/>}
|
||||
{shouldRender &&
|
||||
<>
|
||||
<View style={[styles.flex, {marginTop}]}>
|
||||
|
|
@ -150,7 +135,13 @@ const Channel = ({
|
|||
/>
|
||||
</>
|
||||
}
|
||||
{callsComponents}
|
||||
{renderCallsComponents &&
|
||||
<FloatingCallContainer
|
||||
channelId={channelId}
|
||||
showJoinCallBanner={showJoinCallBanner}
|
||||
isInACall={isInACall}
|
||||
/>
|
||||
}
|
||||
</SafeAreaView>
|
||||
</FreezeScreen>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -3,8 +3,7 @@
|
|||
|
||||
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
|
||||
import withObservables from '@nozbe/with-observables';
|
||||
import {combineLatest, of as of$} from 'rxjs';
|
||||
import {distinctUntilChanged, switchMap} from 'rxjs/operators';
|
||||
import {combineLatest, distinctUntilChanged, of as of$, switchMap} from 'rxjs';
|
||||
|
||||
import {observeIsCallsEnabledInChannel} from '@calls/observers';
|
||||
import {observeChannelsWithCalls, observeCurrentCall} from '@calls/state';
|
||||
|
|
@ -21,6 +20,7 @@ type EnhanceProps = WithDatabaseArgs & {
|
|||
|
||||
const enhanced = withObservables([], ({database, serverUrl}: EnhanceProps) => {
|
||||
const channelId = observeCurrentChannelId(database);
|
||||
|
||||
const isCallInCurrentChannel = combineLatest([channelId, observeChannelsWithCalls(serverUrl)]).pipe(
|
||||
switchMap(([id, calls]) => of$(Boolean(calls[id]))),
|
||||
distinctUntilChanged(),
|
||||
|
|
@ -38,14 +38,13 @@ const enhanced = withObservables([], ({database, serverUrl}: EnhanceProps) => {
|
|||
switchMap(([id, ccId]) => of$(id === ccId)),
|
||||
distinctUntilChanged(),
|
||||
);
|
||||
const isCallsEnabledInChannel = observeIsCallsEnabledInChannel(database, serverUrl, channelId);
|
||||
|
||||
return {
|
||||
channelId,
|
||||
isCallInCurrentChannel,
|
||||
isInACall,
|
||||
isInCurrentChannelCall,
|
||||
isCallsEnabledInChannel,
|
||||
isCallsEnabledInChannel: observeIsCallsEnabledInChannel(database, serverUrl, channelId),
|
||||
};
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -3,10 +3,10 @@
|
|||
|
||||
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
|
||||
import withObservables from '@nozbe/with-observables';
|
||||
import {of as of$} from 'rxjs';
|
||||
import {distinctUntilChanged, switchMap} from 'rxjs/operators';
|
||||
import {distinctUntilChanged, switchMap, combineLatest, of as of$} from 'rxjs';
|
||||
|
||||
import {observeCurrentCall} from '@calls/state';
|
||||
import {observeChannelsWithCalls, observeCurrentCall} from '@calls/state';
|
||||
import {withServerUrl} from '@context/server';
|
||||
import {observePost} from '@queries/servers/post';
|
||||
import {observeIsCRTEnabled} from '@queries/servers/thread';
|
||||
|
||||
|
|
@ -14,17 +14,43 @@ import Thread from './thread';
|
|||
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
|
||||
const enhanced = withObservables(['rootId'], ({database, rootId}: WithDatabaseArgs & {rootId: string}) => {
|
||||
const isInACall = observeCurrentCall().pipe(
|
||||
type EnhanceProps = WithDatabaseArgs & {
|
||||
serverUrl: string;
|
||||
rootId: string;
|
||||
}
|
||||
|
||||
const enhanced = withObservables(['rootId'], ({database, serverUrl, rootId}: EnhanceProps) => {
|
||||
const rootPost = observePost(database, rootId);
|
||||
|
||||
const channelId = rootPost.pipe(
|
||||
switchMap((r) => of$(r?.channelId || '')),
|
||||
distinctUntilChanged(),
|
||||
);
|
||||
const isCallInCurrentChannel = combineLatest([channelId, observeChannelsWithCalls(serverUrl)]).pipe(
|
||||
switchMap(([id, calls]) => of$(Boolean(calls[id]))),
|
||||
distinctUntilChanged(),
|
||||
);
|
||||
const currentCall = observeCurrentCall();
|
||||
const ccChannelId = currentCall.pipe(
|
||||
switchMap((call) => of$(call?.channelId)),
|
||||
distinctUntilChanged(),
|
||||
);
|
||||
const isInACall = currentCall.pipe(
|
||||
switchMap((call) => of$(Boolean(call?.connected))),
|
||||
distinctUntilChanged(),
|
||||
);
|
||||
const isInCurrentChannelCall = combineLatest([channelId, ccChannelId]).pipe(
|
||||
switchMap(([id, ccId]) => of$(id === ccId)),
|
||||
distinctUntilChanged(),
|
||||
);
|
||||
|
||||
return {
|
||||
isCRTEnabled: observeIsCRTEnabled(database),
|
||||
isCallInCurrentChannel,
|
||||
isInACall,
|
||||
rootPost: observePost(database, rootId),
|
||||
isInCurrentChannelCall,
|
||||
rootPost,
|
||||
};
|
||||
});
|
||||
|
||||
export default withDatabase(enhanced(Thread));
|
||||
export default withDatabase(withServerUrl(enhanced(Thread)));
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@ import React, {useCallback, useEffect, useRef, useState} from 'react';
|
|||
import {type LayoutChangeEvent, StyleSheet, View} from 'react-native';
|
||||
import {type Edge, SafeAreaView} from 'react-native-safe-area-context';
|
||||
|
||||
import CurrentCallBar from '@calls/components/current_call_bar';
|
||||
import FloatingCallContainer from '@calls/components/floating_call_container';
|
||||
import {RoundedHeaderCalls} from '@calls/components/join_call_banner/rounded_header_calls';
|
||||
import FreezeScreen from '@components/freeze_screen';
|
||||
import PostDraft from '@components/post_draft';
|
||||
import RoundedHeaderContext from '@components/rounded_header_context';
|
||||
|
|
@ -27,7 +27,9 @@ import type {KeyboardTrackingViewRef} from 'react-native-keyboard-tracking-view'
|
|||
type ThreadProps = {
|
||||
componentId: AvailableScreens;
|
||||
isCRTEnabled: boolean;
|
||||
isCallInCurrentChannel: boolean;
|
||||
isInACall: boolean;
|
||||
isInCurrentChannelCall: boolean;
|
||||
rootId: string;
|
||||
rootPost?: PostModel;
|
||||
};
|
||||
|
|
@ -39,7 +41,15 @@ const styles = StyleSheet.create({
|
|||
flex: {flex: 1},
|
||||
});
|
||||
|
||||
const Thread = ({componentId, isCRTEnabled, rootId, rootPost, isInACall}: ThreadProps) => {
|
||||
const Thread = ({
|
||||
componentId,
|
||||
isCRTEnabled,
|
||||
rootId,
|
||||
rootPost,
|
||||
isCallInCurrentChannel,
|
||||
isInACall,
|
||||
isInCurrentChannelCall,
|
||||
}: ThreadProps) => {
|
||||
const postDraftRef = useRef<KeyboardTrackingViewRef>(null);
|
||||
const [containerHeight, setContainerHeight] = useState(0);
|
||||
|
||||
|
|
@ -84,6 +94,9 @@ const Thread = ({componentId, isCRTEnabled, rootId, rootPost, isInACall}: Thread
|
|||
setContainerHeight(e.nativeEvent.layout.height);
|
||||
}, []);
|
||||
|
||||
const showJoinCallBanner = isCallInCurrentChannel && !isInCurrentChannelCall;
|
||||
const renderCallsComponents = showJoinCallBanner || isInACall;
|
||||
|
||||
return (
|
||||
<FreezeScreen>
|
||||
<SafeAreaView
|
||||
|
|
@ -94,6 +107,7 @@ const Thread = ({componentId, isCRTEnabled, rootId, rootPost, isInACall}: Thread
|
|||
onLayout={onLayout}
|
||||
>
|
||||
<RoundedHeaderContext/>
|
||||
{showJoinCallBanner && <RoundedHeaderCalls threadScreen={true}/>}
|
||||
{Boolean(rootPost) &&
|
||||
<>
|
||||
<View style={styles.flex}>
|
||||
|
|
@ -114,10 +128,13 @@ const Thread = ({componentId, isCRTEnabled, rootId, rootPost, isInACall}: Thread
|
|||
/>
|
||||
</>
|
||||
}
|
||||
{isInACall &&
|
||||
<FloatingCallContainer threadScreen={true}>
|
||||
<CurrentCallBar threadScreen={true}/>
|
||||
</FloatingCallContainer>
|
||||
{renderCallsComponents &&
|
||||
<FloatingCallContainer
|
||||
channelId={rootPost!.channelId}
|
||||
showJoinCallBanner={showJoinCallBanner}
|
||||
isInACall={isInACall}
|
||||
threadScreen={true}
|
||||
/>
|
||||
}
|
||||
</SafeAreaView>
|
||||
</FreezeScreen>
|
||||
|
|
|
|||
|
|
@ -452,7 +452,7 @@
|
|||
"mobile.calls_mic_error": "To participate, open Settings to grant Mattermost access to your microphone.",
|
||||
"mobile.calls_more": "More",
|
||||
"mobile.calls_mute": "Mute",
|
||||
"mobile.calls_name_is_talking": "{name} is talking",
|
||||
"mobile.calls_name_is_talking_postfix": "is talking...",
|
||||
"mobile.calls_name_started_call": "{name} started a call",
|
||||
"mobile.calls_noone_talking": "No one is talking",
|
||||
"mobile.calls_not_available_msg": "Please contact your System Admin to enable the feature.",
|
||||
|
|
@ -467,6 +467,7 @@
|
|||
"mobile.calls_participant_rec_title": "Recording is in progress",
|
||||
"mobile.calls_phone": "Phone",
|
||||
"mobile.calls_raise_hand": "Raise hand",
|
||||
"mobile.calls_raised_hand": "<bold>{name} {num, plural, =0 {} other {+# more }}</bold>raised a hand",
|
||||
"mobile.calls_react": "React",
|
||||
"mobile.calls_rec": "rec",
|
||||
"mobile.calls_record": "Record",
|
||||
|
|
@ -481,9 +482,12 @@
|
|||
"mobile.calls_start_call": "Start Call",
|
||||
"mobile.calls_start_call_exists": "A call is already ongoing in the channel.",
|
||||
"mobile.calls_stop_recording": "Stop Recording",
|
||||
"mobile.calls_tablet": "Tablet",
|
||||
"mobile.calls_thread": "Thread",
|
||||
"mobile.calls_unmute": "Unmute",
|
||||
"mobile.calls_viewing_screen": "You are viewing {name}'s screen",
|
||||
"mobile.calls_you": "(you)",
|
||||
"mobile.calls_you_2": "You",
|
||||
"mobile.camera_photo_permission_denied_description": "Take photos and upload them to your server or save them to your device. Open Settings to grant {applicationName} read and write access to your camera.",
|
||||
"mobile.camera_photo_permission_denied_title": "{applicationName} would like to access your camera",
|
||||
"mobile.camera_type.title": "Camera options",
|
||||
|
|
|
|||
Loading…
Reference in a new issue