Merge branch 'main' into gm_to_channel

This commit is contained in:
harshil Sharma 2023-11-06 10:12:49 +05:30
commit aff310471a
25 changed files with 524 additions and 537 deletions

View file

@ -110,7 +110,7 @@ android {
applicationId "com.mattermost.rnbeta"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 490
versionCode 492
versionName "2.10.0"
testBuildType System.getProperty('testBuildType', 'debug')
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'

View file

@ -7,16 +7,15 @@ import Animated, {interpolate, useAnimatedStyle, useSharedValue, withSpring} fro
import {useSafeAreaInsets} from 'react-native-safe-area-context';
import {resetMessageCount} from '@actions/local/channel';
import {useCallsAdjustment} from '@app/products/calls/hooks';
import CompassIcon from '@components/compass_icon';
import FormattedText from '@components/formatted_text';
import TouchableWithFeedback from '@components/touchable_with_feedback';
import {Events} from '@constants';
import {CURRENT_CALL_BAR_HEIGHT, JOIN_CALL_BAR_HEIGHT} from '@constants/view';
import {useServerUrl} from '@context/server';
import {useIsTablet} from '@hooks/device';
import useDidUpdate from '@hooks/did_update';
import EphemeralStore from '@store/ephemeral_store';
import {makeStyleSheetFromTheme, hexToHue} from '@utils/theme';
import {makeStyleSheetFromTheme, hexToHue, changeOpacity} from '@utils/theme';
import {typography} from '@utils/typography';
import type {PostList} from '@typings/components/post_list';
@ -34,8 +33,6 @@ type Props = {
unreadCount: number;
theme: Theme;
testID: string;
currentCallBarVisible: boolean;
joinCallBannerVisible: boolean;
}
const HIDDEN_TOP = -60;
@ -60,9 +57,13 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
flexDirection: 'row',
justifyContent: 'space-evenly',
alignItems: 'center',
paddingLeft: 12,
width: '100%',
height: 42,
height: 40,
borderRadius: 8,
paddingTop: 4,
paddingRight: 4,
paddingBottom: 4,
paddingLeft: 8,
shadowColor: theme.centerChannelColor,
shadowOffset: {
width: 0,
@ -70,29 +71,30 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
},
shadowOpacity: 0.12,
shadowRadius: 4,
elevation: 4,
},
roundBorder: {
borderRadius: 8,
iconContainer: {
top: 1,
width: 32,
},
icon: {
fontSize: 18,
color: theme.buttonColor,
alignSelf: 'center',
},
iconContainer: {
top: 2,
width: 22,
closeIcon: {
color: changeOpacity(theme.buttonColor, 0.56),
},
pressContainer: {
flex: 1,
flexDirection: 'row',
},
textContainer: {
paddingLeft: 4,
marginLeft: 8,
},
text: {
color: theme.buttonColor,
...typography('Body', 200, 'SemiBold'),
...typography('Body', 100, 'SemiBold'),
},
};
});
@ -110,11 +112,8 @@ const MoreMessages = ({
unreadCount,
testID,
theme,
currentCallBarVisible,
joinCallBannerVisible,
}: Props) => {
const serverUrl = useServerUrl();
const isTablet = useIsTablet();
const insets = useSafeAreaInsets();
const pressed = useRef(false);
const resetting = useRef(false);
@ -122,12 +121,14 @@ const MoreMessages = ({
const [loading, setLoading] = useState(EphemeralStore.isLoadingMessagesForChannel(serverUrl, channelId));
const [remaining, setRemaining] = useState(0);
const underlayColor = useMemo(() => `hsl(${hexToHue(theme.buttonBg)}, 50%, 38%)`, [theme]);
const top = useSharedValue(0);
const adjustedShownTop = SHOWN_TOP + (currentCallBarVisible ? CURRENT_CALL_BAR_HEIGHT : 0) + (joinCallBannerVisible ? JOIN_CALL_BAR_HEIGHT : 0);
const adjustTop = isTablet || (isCRTEnabled && rootId);
const shownTop = adjustTop ? SHOWN_TOP : adjustedShownTop;
const BARS_FACTOR = Math.abs((1) / (HIDDEN_TOP - SHOWN_TOP));
const styles = getStyleSheet(theme);
const top = useSharedValue(0);
const callsAdjustment = useCallsAdjustment(serverUrl, channelId);
// The final top:
const adjustedTop = insets.top + callsAdjustment;
const BARS_FACTOR = Math.abs((1) / (HIDDEN_TOP - SHOWN_TOP));
const animatedStyle = useAnimatedStyle(() => ({
transform: [{
@ -142,13 +143,13 @@ const MoreMessages = ({
[
HIDDEN_TOP,
HIDDEN_TOP,
shownTop + (adjustTop ? 0 : insets.top),
shownTop + (adjustTop ? 0 : insets.top),
adjustedTop,
adjustedTop,
],
'clamp',
), {damping: 15}),
}],
}), [shownTop, insets.top, adjustTop]);
}), [adjustedTop]);
// Due to the implementation differences "unreadCount" gets updated for a channel on reset but not for a thread.
// So we maintain a localUnreadCount to hide the indicator when the count is reset.
@ -254,7 +255,7 @@ const MoreMessages = ({
return (
<Animated.View style={[styles.animatedContainer, animatedStyle]}>
<View style={[styles.container, styles.roundBorder]}>
<View style={[styles.container]}>
<TouchableWithFeedback
type={'opacity'}
onPress={onPress}
@ -295,7 +296,7 @@ const MoreMessages = ({
<View style={styles.cancelContainer}>
<CompassIcon
name='close'
style={styles.icon}
style={[styles.icon, styles.closeIcon]}
/>
</View>
</TouchableWithFeedback>

View file

@ -52,7 +52,6 @@ type Props = {
header?: ReactElement;
testID: string;
currentCallBarVisible?: boolean;
joinCallBannerVisible?: boolean;
savedPostIds: Set<string>;
}
@ -111,8 +110,6 @@ const PostList = ({
showMoreMessages,
showNewMessageLine = true,
testID,
currentCallBarVisible,
joinCallBannerVisible,
savedPostIds,
}: Props) => {
const listRef = useRef<FlatList<string | PostModel>>(null);
@ -377,8 +374,6 @@ const PostList = ({
scrollToIndex={scrollToIndex}
theme={theme}
testID={`${testID}.more_messages_button`}
currentCallBarVisible={Boolean(currentCallBarVisible)}
joinCallBannerVisible={Boolean(joinCallBannerVisible)}
/>
}
</>

View file

@ -3,7 +3,7 @@
import React, {useCallback} from 'react';
import {useIntl} from 'react-intl';
import {type StyleProp, Text, TouchableOpacity, View, type ViewStyle} from 'react-native';
import {type StyleProp, Text, type TextStyle, TouchableOpacity, View, type ViewStyle} from 'react-native';
import {useSafeAreaInsets} from 'react-native-safe-area-context';
import FormattedText from '@components/formatted_text';
@ -32,6 +32,10 @@ type Props = {
breakAt?: number;
style?: StyleProp<ViewStyle>;
noBorder?: boolean;
avatarStyle?: StyleProp<ViewStyle>;
overflowContainerStyle?: StyleProp<ViewStyle>;
overflowItemStyle?: StyleProp<ViewStyle>;
overflowTextStyle?: StyleProp<TextStyle>;
}
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
@ -42,7 +46,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
container: {
flexDirection: 'row',
},
firstAvatar: {
avatarCommon: {
justifyContent: 'center',
alignItems: 'center',
width: size,
@ -56,35 +60,16 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
borderWidth: 0,
},
notFirstAvatars: {
justifyContent: 'center',
alignItems: 'center',
width: size,
height: size,
borderWidth: (size / 2) + 1,
borderColor: theme.centerChannelBg,
backgroundColor: theme.centerChannelBg,
borderRadius: size / 2,
marginLeft: imgOverlap,
},
overflowContainer: {
justifyContent: 'center',
alignItems: 'center',
width: size,
height: size,
borderRadius: size / 2,
borderWidth: 1,
borderColor: theme.centerChannelBg,
backgroundColor: theme.centerChannelBg,
marginLeft: imgOverlap,
},
overflowItem: {
justifyContent: 'center',
alignItems: 'center',
width: size,
height: size,
borderRadius: size / 2,
borderWidth: 1,
borderColor: theme.centerChannelBg,
backgroundColor: changeOpacity(theme.centerChannelColor, 0.08),
},
overflowText: {
@ -110,6 +95,10 @@ const UserAvatarsStack = ({
style: baseContainerStyle,
users,
noBorder = false,
avatarStyle,
overflowContainerStyle,
overflowItemStyle,
overflowTextStyle,
}: Props) => {
const theme = useTheme();
const intl = useIntl();
@ -165,14 +154,14 @@ const UserAvatarsStack = ({
{displayUsers.map((user, index) => (
<UserAvatar
key={user.id}
style={index === 0 ? [style.firstAvatar, noBorder && style.noBorder] : [style.notFirstAvatars, noBorder && style.noBorder]}
style={index === 0 ? [style.avatarCommon, noBorder && style.noBorder, avatarStyle] : [style.avatarCommon, style.notFirstAvatars, noBorder && style.noBorder, avatarStyle]}
user={user}
/>
))}
{Boolean(overflowUsersCount) && (
<View style={[style.overflowContainer, noBorder && style.noBorder]}>
<View style={[style.overflowItem, noBorder && style.noBorder]}>
<Text style={style.overflowText}>
<View style={[style.avatarCommon, style.overflowContainer, noBorder && style.noBorder, overflowContainerStyle]}>
<View style={[style.avatarCommon, style.overflowItem, noBorder && style.noBorder, overflowItemStyle]}>
<Text style={[style.overflowText, overflowTextStyle]}>
{'+' + overflowUsersCount.toString()}
</Text>
</View>

View file

@ -21,10 +21,10 @@ export const KEYBOARD_TRACKING_OFFSET = 72;
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 = 68;
export const CALL_ERROR_BAR_HEIGHT = 62;
export const CALL_NOTIFICATION_BAR_HEIGHT = 60;
export const JOIN_CALL_BAR_HEIGHT = 40;
export const CURRENT_CALL_BAR_HEIGHT = 60;
export const CALL_ERROR_BAR_HEIGHT = 52;
export const CALL_NOTIFICATION_BAR_HEIGHT = 40;
export const ANNOUNCEMENT_BAR_HEIGHT = 40;

View file

@ -17,7 +17,7 @@ import type UserModel from '@typings/database/models/servers/user';
type Props = {
userModel?: UserModel;
volume?: number;
speaking?: boolean;
serverUrl: string;
size: number;
muted?: boolean;
@ -27,49 +27,47 @@ type Props = {
}
// Note: microSize is 32, smallSize is 72
const mediumSize = 96;
const mediumSize = 72;
const mediumBorderWidth = 3;
const largeBorderWidth = 6;
const getStyleSheet = ({
theme,
volume,
size,
}: { theme: CallsTheme; volume: number; size: number }) => {
const getStyleSheet = ({theme, size}: { theme: CallsTheme; 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;
const borderWidth = size <= mediumSize ? mediumBorderWidth : largeBorderWidth;
return StyleSheet.create({
pictureContainer: {
justifyContent: 'center',
alignItems: 'center',
height: size + (borderWidth * 4),
width: size + (borderWidth * 4),
},
pictureHalo: {
backgroundColor: changeOpacity(theme.onlineIndicator, 0.24 * volume),
height: size + 16,
width: size + 16,
padding: 4,
borderRadius: (size + 16) / 2,
backgroundColor: changeOpacity(theme.onlineIndicator, 0.24),
height: size + (borderWidth * 4),
width: size + (borderWidth * 4),
padding: borderWidth,
borderRadius: (size + (borderWidth * 4)) / 2,
},
pictureHalo2: {
backgroundColor: changeOpacity(theme.onlineIndicator, 0.32 * volume),
height: size + 8,
width: size + 8,
padding: 3,
borderRadius: (size + 8) / 2,
backgroundColor: changeOpacity(theme.onlineIndicator, 0.32),
height: size + (borderWidth * 2),
width: size + (borderWidth * 2),
padding: borderWidth,
borderRadius: (size + (borderWidth * 4)) / 2,
},
picture: {
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,
color: changeOpacity(theme.buttonColor, 0.56),
},
muteIconContainer: {
position: 'absolute',
@ -143,23 +141,31 @@ const getStyleSheet = ({
paddingTop: Platform.select({ios: 5}),
},
emoji: {
paddingLeft: Platform.select({ios: 5, default: 5}),
paddingTop: Platform.select({ios: 7, default: 3}),
paddingLeft: Platform.select({ios: mediumIcon ? 4 : 6, default: 5}),
paddingTop: Platform.select({ios: 5, default: 3}),
},
});
};
const CallAvatar = ({userModel, volume = 0, serverUrl, sharingScreen, size, muted, raisedHand, reaction}: Props) => {
const CallAvatar = ({
userModel,
speaking = false,
serverUrl,
sharingScreen,
size,
muted,
raisedHand,
reaction,
}: Props) => {
const theme = useTheme();
const callsTheme = useMemo(() => makeCallsTheme(theme), [theme]);
const style = useMemo(() => getStyleSheet({theme: callsTheme, volume, size}), [callsTheme, volume, size]);
const style = useMemo(() => getStyleSheet({theme: callsTheme, size}), [callsTheme, size]);
const iconSize = size <= mediumSize ? 18 : 24;
const reactionSize = size <= mediumSize ? 22 : 26;
const styleShadow = volume > 0 ? style.voiceShadow : undefined;
const reactionSize = size <= mediumSize ? 22 : 28;
// Only show one or the other.
let topRightIcon: JSX.Element | null = null;
let topRightIcon: React.JSX.Element | null = null;
if (sharingScreen) {
topRightIcon = (
<View style={style.reactionContainer}>
@ -190,7 +196,7 @@ const CallAvatar = ({userModel, volume = 0, serverUrl, sharingScreen, size, mute
<Emoji
emojiName={reaction.name}
literal={reaction.literal}
size={reactionSize - Platform.select({ios: 6, default: 4})}
size={reactionSize - Platform.select({ios: 3, default: 4})}
/>
</View>
</View>
@ -212,34 +218,28 @@ const CallAvatar = ({userModel, volume = 0, serverUrl, sharingScreen, size, mute
/>
);
const view = (
<View style={[style.picture, styleShadow]}>
{profile}
{
muted !== undefined &&
<View style={style.muteIconContainer}>
<CompassIcon
name={muted ? 'microphone-off' : 'microphone'}
size={iconSize}
style={[style.muteIcon, !muted && style.muteIconUnmuted]}
/>
</View>
}
{topRightIcon}
</View>
);
if (Platform.OS === 'android') {
return (
<View style={style.pictureHalo}>
<View style={style.pictureHalo2}>
{view}
return (
<View style={style.pictureContainer}>
<View style={[speaking && style.pictureHalo]}>
<View style={[speaking && style.pictureHalo2]}>
<View style={[style.picture]}>
{profile}
{
muted !== undefined &&
<View style={style.muteIconContainer}>
<CompassIcon
name={muted ? 'microphone-off' : 'microphone'}
size={iconSize}
style={[style.muteIcon, !muted && style.muteIconUnmuted]}
/>
</View>
}
{topRightIcon}
</View>
</View>
</View>
);
}
return view;
</View>
);
};
export default CallAvatar;

View file

@ -3,108 +3,94 @@
import React, {useCallback, useEffect} from 'react';
import {useIntl} from 'react-intl';
import {Pressable, StyleSheet, Text, View} from 'react-native';
import {Pressable, Text, View} from 'react-native';
import {switchToChannelById} from '@actions/remote/channel';
import {fetchProfilesInChannel} from '@actions/remote/user';
import {dismissIncomingCall} from '@calls/actions/calls';
import {leaveAndJoinWithAlert} from '@calls/alerts';
import {removeIncomingCall} from '@calls/state';
import {ChannelType, type IncomingCallNotification} from '@calls/types/calls';
import CompassIcon from '@components/compass_icon';
import FormattedText from '@components/formatted_text';
import ProfilePicture from '@components/profile_picture';
import {Preferences} from '@constants';
import {CALL_NOTIFICATION_BAR_HEIGHT} from '@constants/view';
import {useServerUrl} from '@context/server';
import {useTheme} from '@context/theme';
import DatabaseManager from '@database/manager';
import WebsocketManager from '@managers/websocket_manager';
import ChannelMembershipModel from '@typings/database/models/servers/channel_membership';
import {changeOpacity} from '@utils/theme';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {typography} from '@utils/typography';
import {displayUsername} from '@utils/user';
const style = StyleSheet.create({
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
outerContainer: {
backgroundColor: Preferences.THEMES.denim.onlineIndicator,
borderRadius: 8,
height: CALL_NOTIFICATION_BAR_HEIGHT,
marginLeft: 8,
marginRight: 8,
},
outerOnCallsScreen: {
backgroundColor: changeOpacity(Preferences.THEMES.denim.onlineIndicator, 0.40),
backgroundColor: theme.onlineIndicator,
shadowColor: theme.centerChannelColor,
shadowOffset: {
width: 0,
height: 6,
},
shadowOpacity: 0.12,
shadowRadius: 4,
elevation: 4,
},
innerContainer: {
flexDirection: 'row',
width: '100%',
height: '100%',
paddingTop: 8,
paddingBottom: 8,
paddingLeft: 12,
paddingRight: 12,
borderRadius: 8,
borderWidth: 2,
borderStyle: 'solid',
borderColor: changeOpacity(Preferences.THEMES.denim.buttonColor, 0.16),
gap: 8,
paddingTop: 4,
paddingRight: 4,
paddingBottom: 4,
paddingLeft: 8,
alignItems: 'center',
backgroundColor: changeOpacity('#000', 0.16),
},
innerOnCallsScreen: {
borderColor: changeOpacity(Preferences.THEMES.denim.buttonColor, 0.16),
paddingRight: 2,
paddingLeft: 6,
borderStyle: 'solid',
borderWidth: 2,
borderColor: changeOpacity(theme.buttonColor, 0.16),
backgroundColor: changeOpacity('#000', 0.12),
},
text: {
flex: 1,
...typography('Body', 200),
lineHeight: 20,
color: Preferences.THEMES.denim.buttonColor,
},
boldText: {
...typography('Body', 200, 'SemiBold'),
lineHeight: 20,
},
join: {
flexDirection: 'row',
alignItems: 'flex-end',
height: 40,
gap: 7,
backgroundColor: Preferences.THEMES.denim.buttonColor,
paddingTop: 10,
paddingRight: 20,
paddingBottom: 10,
paddingLeft: 20,
borderRadius: 30,
},
joinOnCallsScreen: {
backgroundColor: changeOpacity(Preferences.THEMES.denim.buttonColor, 0.12),
},
joinLabel: {
...typography('Body', 100, 'SemiBold'),
},
joinIconLabel: {
color: Preferences.THEMES.denim.onlineIndicator,
},
joinIconLabelOnCallsScreen: {
color: Preferences.THEMES.denim.buttonColor,
},
dismiss: {
height: 40,
width: 40,
borderRadius: 20,
padding: 0,
backgroundColor: changeOpacity(Preferences.THEMES.denim.buttonColor, 0.08),
profileContainer: {
width: 32,
alignItems: 'center',
justifyContent: 'center',
},
dismissOnCallsScreen: {
backgroundColor: 'transparent',
icon: {
fontSize: 18,
color: theme.buttonColor,
alignSelf: 'center',
},
dismissIcon: {
color: Preferences.THEMES.denim.buttonColor,
textContainer: {
flex: 1,
marginLeft: 8,
},
});
text: {
...typography('Body', 100),
color: theme.buttonColor,
},
boldText: {
...typography('Body', 100, 'SemiBold'),
lineHeight: 20,
},
dismissContainer: {
alignItems: 'center',
width: 32,
height: '100%',
justifyContent: 'center',
},
closeIcon: {
color: changeOpacity(theme.buttonColor, 0.56),
},
}));
type Props = {
incomingCall: IncomingCallNotification;
@ -123,6 +109,8 @@ export const CallNotification = ({
}: Props) => {
const intl = useIntl();
const serverUrl = useServerUrl();
const theme = useTheme();
const style = getStyleSheet(theme);
useEffect(() => {
const channelMembers = members?.filter((m) => m.userId !== currentUserId);
@ -131,10 +119,6 @@ export const CallNotification = ({
}
}, []);
const onJoinPress = useCallback(() => {
leaveAndJoinWithAlert(intl, incomingCall.serverUrl, incomingCall.channelID);
}, [intl, incomingCall]);
const onContainerPress = useCallback(async () => {
if (serverUrl !== incomingCall.serverUrl) {
await DatabaseManager.setActiveServerDatabase(incomingCall.serverUrl);
@ -179,47 +163,31 @@ export const CallNotification = ({
/>
);
}
const joinLabel = (
<FormattedText
id={'mobile.calls_join_button'}
defaultMessage={'Join'}
style={[style.joinIconLabel, style.joinLabel, onCallsScreen && style.joinIconLabelOnCallsScreen]}
/>
);
return (
<View style={[style.outerContainer, onCallsScreen && style.outerOnCallsScreen]}>
<View style={[style.outerContainer]}>
<Pressable
style={[style.innerContainer, onCallsScreen && style.innerOnCallsScreen]}
onPress={onContainerPress}
>
<ProfilePicture
author={incomingCall.callerModel}
url={incomingCall.serverUrl}
size={32}
showStatus={false}
/>
{message}
<Pressable
style={[style.join, onCallsScreen && style.joinOnCallsScreen]}
onPress={onJoinPress}
>
<CompassIcon
name='phone-in-talk'
size={18}
style={[style.joinIconLabel, onCallsScreen && style.joinIconLabelOnCallsScreen]}
/>
{joinLabel}
</Pressable>
<Pressable
style={[style.dismiss, onCallsScreen && style.dismissOnCallsScreen]}
onPress={onDismissPress}
>
<CompassIcon
name={'close'}
<View style={style.profileContainer}>
<ProfilePicture
author={incomingCall.callerModel}
url={incomingCall.serverUrl}
size={24}
style={style.dismissIcon}
showStatus={false}
/>
</View>
<View style={style.textContainer}>
{message}
</View>
<Pressable onPress={onDismissPress}>
<View style={style.dismissContainer}>
<CompassIcon
name='close'
style={[style.icon, style.closeIcon]}
/>
</View>
</Pressable>
</Pressable>
</View>

View file

@ -39,7 +39,8 @@ type Props = {
const getStyleSheet = makeStyleSheetFromTheme((theme: CallsTheme) => {
return {
wrapper: {
margin: 8,
marginRight: 8,
marginLeft: 8,
backgroundColor: theme.callsBg,
borderRadius: 8,
},
@ -55,27 +56,33 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: CallsTheme) => {
paddingTop: 8,
paddingRight: 12,
paddingBottom: 8,
paddingLeft: 12,
height: CURRENT_CALL_BAR_HEIGHT - 10,
paddingLeft: 6,
height: CURRENT_CALL_BAR_HEIGHT,
},
avatarOutline: {
height: 40,
width: 40,
borderRadius: 20,
backgroundColor: changeOpacity(theme.buttonColor, 0.08),
padding: 2,
marginRight: 6,
marginLeft: 6,
},
pressable: {
zIndex: 10,
},
profilePic: {
marginTop: 4,
marginRight: Platform.select({android: -8}),
marginLeft: Platform.select({android: -8}),
},
userInfo: {
text: {
flex: 1,
flexDirection: 'column',
paddingLeft: 6,
gap: 2,
},
speakingUser: {
color: theme.buttonColor,
...typography('Body', 200, 'SemiBold'),
...typography('Body', 100, 'SemiBold'),
},
speakingPostfix: {
...typography('Body', 200, 'Regular'),
...typography('Body', 100, 'Regular'),
},
channelAndTime: {
color: changeOpacity(theme.buttonColor, 0.56),
@ -224,15 +231,15 @@ const CurrentCallBar = ({
style={style.container}
onPress={goToCallScreen}
>
<View style={style.profilePic}>
<View style={[!speaker && style.avatarOutline]}>
<CallAvatar
userModel={userModelsDict[speaker || '']}
volume={speaker ? 0.5 : 0}
speaking={Boolean(speaker)}
serverUrl={currentCall?.serverUrl || ''}
size={32}
size={speaker ? 40 : 24}
/>
</View>
<View style={style.userInfo}>
<View style={style.text}>
{talkingMessage}
<Text style={style.channelAndTime}>
{`~${displayName}`}
@ -274,13 +281,13 @@ const CurrentCallBar = ({
{micPermissionsError &&
<MessageBar
type={Calls.MessageBarType.Microphone}
onPress={setMicPermissionsErrorDismissed}
onDismiss={setMicPermissionsErrorDismissed}
/>
}
{currentCall?.callQualityAlert &&
<MessageBar
type={Calls.MessageBarType.CallQuality}
onPress={setCallQualityAlertDismissed}
onDismiss={setCallQualityAlertDismissed}
/>
}
</>

View file

@ -2,43 +2,42 @@
// See LICENSE.txt for license information.
import React from 'react';
import {View, Platform, StyleSheet} from 'react-native';
import {View, StyleSheet} from 'react-native';
import {useSafeAreaInsets} from 'react-native-safe-area-context';
import CurrentCallBar from '@calls/components/current_call_bar';
import {IncomingCallsContainer} from '@calls/components/incoming_calls_container';
import JoinCallBanner from '@calls/components/join_call_banner';
import {DEFAULT_HEADER_HEIGHT} from '@constants/view';
import {DEFAULT_HEADER_HEIGHT, TABLET_HEADER_HEIGHT} from '@constants/view';
import {useServerUrl} from '@context/server';
const topBarHeight = DEFAULT_HEADER_HEIGHT;
import {useIsTablet} from '@hooks/device';
const style = StyleSheet.create({
wrapper: {
position: 'absolute',
width: '100%',
...Platform.select({
android: {
elevation: 9,
},
ios: {
zIndex: 9,
},
}),
marginTop: 8,
gap: 8,
},
});
type Props = {
channelId: string;
showJoinCallBanner: boolean;
showIncomingCalls: boolean;
isInACall: boolean;
threadScreen?: boolean;
}
const FloatingCallContainer = ({channelId, showJoinCallBanner, isInACall, threadScreen}: Props) => {
const FloatingCallContainer = ({channelId, showJoinCallBanner, showIncomingCalls, isInACall, threadScreen}: Props) => {
const serverUrl = useServerUrl();
const insets = useSafeAreaInsets();
const isTablet = useIsTablet();
const topBarForTablet = (isTablet && !threadScreen) ? TABLET_HEADER_HEIGHT : 0;
const topBarChannel = (!isTablet && !threadScreen) ? DEFAULT_HEADER_HEIGHT : 0;
const wrapperTop = {
top: insets.top + (threadScreen ? 0 : topBarHeight),
top: insets.top + topBarForTablet + topBarChannel,
};
return (
@ -50,6 +49,11 @@ const FloatingCallContainer = ({channelId, showJoinCallBanner, isInACall, thread
/>
}
{isInACall && <CurrentCallBar/>}
{showIncomingCalls &&
<IncomingCallsContainer
channelId={channelId}
/>
}
</View>
);
};

View file

@ -3,45 +3,25 @@
import React from 'react';
import {StyleSheet, View} from 'react-native';
import {useSafeAreaInsets} from 'react-native-safe-area-context';
import CallNotification from '@calls/components/call_notification';
import {useCurrentCall, useGlobalCallsState, useIncomingCalls} from '@calls/state';
import {
CALL_ERROR_BAR_HEIGHT,
CURRENT_CALL_BAR_HEIGHT,
DEFAULT_HEADER_HEIGHT,
JOIN_CALL_BAR_HEIGHT,
} from '@constants/view';
const topBarHeight = DEFAULT_HEADER_HEIGHT;
import {useIncomingCalls} from '@calls/state';
const style = StyleSheet.create({
wrapper: {
position: 'absolute',
width: '100%',
marginTop: 8,
gap: 8,
},
});
type Props = {
channelId: string;
showingJoinCallBanner: boolean;
showingCurrentCallBanner: boolean;
threadScreen?: boolean;
}
export const IncomingCallsContainer = ({
channelId,
showingJoinCallBanner,
showingCurrentCallBanner,
threadScreen,
}: Props) => {
const incomingCalls = useIncomingCalls().incomingCalls;
const insets = useSafeAreaInsets();
const micPermissionsGranted = useGlobalCallsState().micPermissionsGranted;
const currentCall = useCurrentCall();
// If we're in the channel for the incoming call, don't show the incoming call banner.
const calls = incomingCalls.filter((ic) => ic.channelID !== channelId);
@ -49,17 +29,8 @@ export const IncomingCallsContainer = ({
return null;
}
const micPermissionsError = !micPermissionsGranted && (currentCall ? !currentCall.micPermissionsErrorDismissed : false);
const qualityAlert = showingCurrentCallBanner && (currentCall ? currentCall.callQualityAlert && currentCall.callQualityAlertDismissed === 0 : false);
const top = insets.top + (threadScreen ? 0 : topBarHeight) +
(showingJoinCallBanner ? JOIN_CALL_BAR_HEIGHT : 0) +
(showingCurrentCallBanner ? CURRENT_CALL_BAR_HEIGHT - 2 : 0) +
(micPermissionsError ? CALL_ERROR_BAR_HEIGHT + 8 : 0) +
(qualityAlert ? CALL_ERROR_BAR_HEIGHT + 8 : 0);
const wrapperTop = {top};
return (
<View style={[style.wrapper, wrapperTop]}>
<View style={style.wrapper}>
{calls.map((ic) => (
<CallNotification
key={ic.callID}

View file

@ -41,7 +41,12 @@ const enhanced = withObservables(['serverUrl', 'channelId'], ({
distinctUntilChanged(),
);
const callId = callsState.pipe(
switchMap((state) => of$(state?.id || '')),
);
return {
callId,
participants,
channelCallStartTime,
limitRestrictedInfo: observeIsCallLimitRestricted(database, serverUrl, channelId),

View file

@ -5,7 +5,9 @@ import React from 'react';
import {useIntl} from 'react-intl';
import {View, Pressable} from 'react-native';
import {dismissIncomingCall} from '@calls/actions';
import {leaveAndJoinWithAlert, showLimitRestrictedAlert} from '@calls/alerts';
import {removeIncomingCall} from '@calls/state';
import CompassIcon from '@components/compass_icon';
import FormattedRelativeTime from '@components/formatted_relative_time';
import FormattedText from '@components/formatted_text';
@ -21,6 +23,7 @@ import type UserModel from '@typings/database/models/servers/user';
type Props = {
channelId: string;
callId: string;
serverUrl: string;
participants: UserModel[];
channelCallStartTime: number;
@ -29,38 +32,57 @@ type Props = {
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
outerContainer: {
backgroundColor: theme.sidebarBg,
borderRadius: 8,
height: JOIN_CALL_BAR_HEIGHT,
marginRight: 8,
marginLeft: 8,
shadowColor: theme.centerChannelColor,
shadowOffset: {
width: 0,
height: 6,
},
shadowOpacity: 0.12,
shadowRadius: 4,
elevation: 4,
},
innerContainer: {
flexDirection: 'row',
backgroundColor: '#339970', // intentionally not themed
width: '100%',
borderTopLeftRadius: 12,
borderTopRightRadius: 12,
paddingTop: 5,
paddingBottom: 5,
paddingLeft: 12,
paddingRight: 12,
justifyContent: 'center',
height: '100%',
borderRadius: 8,
paddingTop: 4,
paddingRight: 4,
paddingBottom: 4,
paddingLeft: 8,
alignItems: 'center',
height: JOIN_CALL_BAR_HEIGHT,
backgroundColor: '#339970', // intentionally not themed
},
innerContainerRestricted: {
backgroundColor: changeOpacity(theme.centerChannelColor, 0.48),
},
joinCallIcon: {
color: theme.buttonColor,
marginRight: 7,
iconContainer: {
top: 1,
width: 32,
},
joinCall: {
icon: {
fontSize: 18,
color: theme.buttonColor,
alignSelf: 'center',
},
textContainer: {
flexDirection: 'row',
flex: 1,
marginLeft: 8,
},
joinCallText: {
color: theme.buttonColor,
...typography('Body', 100, 'SemiBold'),
},
started: {
startedText: {
flex: 1,
color: changeOpacity(theme.buttonColor, 0.84),
color: changeOpacity(theme.buttonColor, 0.80),
...typography(),
marginLeft: 10,
marginLeft: 6,
},
limitReached: {
flex: 1,
@ -70,18 +92,47 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
color: changeOpacity(theme.sidebarText, 0.84),
fontWeight: '400',
},
headerText: {
color: changeOpacity(theme.centerChannelColor, 0.56),
fontSize: 12,
fontWeight: '600',
paddingHorizontal: 16,
paddingVertical: 0,
top: 16,
avatarStyle: {
borderColor: '#339970',
backgroundColor: '#339970',
},
overflowContainer: {
justifyContent: 'center',
alignItems: 'center',
borderWidth: (24 / 2),
borderColor: '#339970',
backgroundColor: '#339970',
borderRadius: 24 / 2,
marginTop: 1,
},
overflowItem: {
width: 26,
height: 26,
borderRadius: 26 / 2,
borderWidth: 1,
borderColor: '#339970',
backgroundColor: changeOpacity(theme.buttonColor, 0.24),
},
overflowText: {
fontSize: 10,
fontWeight: 'bold',
color: changeOpacity(theme.buttonColor, 0.80),
textAlign: 'center',
},
dismissContainer: {
alignItems: 'center',
width: 32,
height: '100%',
justifyContent: 'center',
},
closeIcon: {
color: changeOpacity(theme.buttonColor, 0.56),
},
}));
const JoinCallBanner = ({
channelId,
callId,
serverUrl,
participants,
channelCallStartTime,
@ -100,42 +151,61 @@ const JoinCallBanner = ({
leaveAndJoinWithAlert(intl, serverUrl, channelId);
};
const onDismissPress = () => {
removeIncomingCall(serverUrl, callId, channelId);
dismissIncomingCall(serverUrl, channelId);
};
return (
<View style={style.outerContainer}>
<Pressable
style={[style.innerContainer, isLimitRestricted && style.innerContainerRestricted]}
onPress={joinHandler}
>
<CompassIcon
name='phone-in-talk'
size={18}
style={style.joinCallIcon}
/>
<FormattedText
id={'mobile.calls_join_call'}
defaultMessage={'Join call'}
style={style.joinCall}
/>
{isLimitRestricted ? (
<View style={style.iconContainer}>
<CompassIcon
name='phone-in-talk'
style={style.icon}
/>
</View>
<View style={style.textContainer}>
<FormattedText
id={'mobile.calls_limit_reached'}
defaultMessage={'Participant limit reached'}
style={style.limitReached}
id={'mobile.calls_join_call'}
defaultMessage={'Join call'}
style={style.joinCallText}
/>
) : (
<FormattedRelativeTime
value={channelCallStartTime}
updateIntervalInSeconds={1}
style={style.started}
/>
)}
{isLimitRestricted ? (
<FormattedText
id={'mobile.calls_limit_reached'}
defaultMessage={'Participant limit reached'}
style={style.limitReached}
/>
) : (
<FormattedRelativeTime
value={channelCallStartTime}
updateIntervalInSeconds={1}
style={style.startedText}
/>
)}
</View>
<UserAvatarsStack
channelId={channelId}
location={Screens.CHANNEL}
users={participants}
breakAt={1}
noBorder={true}
breakAt={3}
avatarStyle={style.avatarStyle}
overflowContainerStyle={style.overflowContainer}
overflowItemStyle={style.overflowItem}
overflowTextStyle={style.overflowText}
/>
<Pressable onPress={onDismissPress}>
<View style={style.dismissContainer}>
<CompassIcon
name='close'
style={[style.icon, style.closeIcon]}
/>
</View>
</Pressable>
</Pressable>
</View>
);

View file

@ -1,45 +0,0 @@
// 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>
);
};

View file

@ -11,7 +11,7 @@ import CompassIcon from '@components/compass_icon';
import {Calls} from '@constants';
import {CALL_ERROR_BAR_HEIGHT} from '@constants/view';
import {useTheme} from '@context/theme';
import {makeStyleSheetFromTheme} from '@utils/theme';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {typography} from '@utils/typography';
import type {CallsTheme} from '@calls/types/calls';
@ -19,70 +19,80 @@ import type {MessageBarType} from '@constants/calls';
type Props = {
type: MessageBarType;
onPress: () => void;
onDismiss: () => void;
}
const getStyleSheet = makeStyleSheetFromTheme((theme: CallsTheme) => (
{
pressable: {
zIndex: 10,
const getStyleSheet = makeStyleSheetFromTheme((theme: CallsTheme) => ({
outerContainer: {
borderRadius: 8,
height: CALL_ERROR_BAR_HEIGHT,
marginLeft: 8,
marginRight: 8,
shadowColor: theme.centerChannelColor,
shadowOffset: {
width: 0,
height: 6,
},
errorWrapper: {
padding: 8,
paddingTop: 0,
},
errorBar: {
flexDirection: 'row',
backgroundColor: theme.dndIndicator,
minHeight: CALL_ERROR_BAR_HEIGHT,
width: '100%',
borderRadius: 5,
padding: 10,
alignItems: 'center',
},
warningBar: {
backgroundColor: theme.awayIndicator,
},
errorText: {
flex: 1,
...typography('Body', 100, 'SemiBold'),
color: theme.buttonColor,
},
warningText: {
color: theme.callsBg,
},
iconContainer: {
width: 42,
height: 42,
justifyContent: 'center',
alignItems: 'center',
borderRadius: 4,
margin: 0,
padding: 9,
},
pressedIconContainer: {
backgroundColor: theme.buttonColor,
},
errorIcon: {
color: theme.buttonColor,
fontSize: 18,
},
warningIcon: {
color: theme.callsBg,
},
pressedErrorIcon: {
color: theme.dndIndicator,
},
pressedWarningIcon: {
color: theme.awayIndicator,
},
paddingRight: {
paddingRight: 9,
},
}
));
shadowOpacity: 0.12,
shadowRadius: 4,
elevation: 4,
},
outerContainerWarning: {
backgroundColor: theme.awayIndicator,
},
innerContainer: {
flexDirection: 'row',
height: '100%',
width: '100%',
borderRadius: 8,
paddingTop: 4,
paddingRight: 4,
paddingBottom: 4,
paddingLeft: 8,
alignItems: 'center',
backgroundColor: theme.dndIndicator,
},
innerContainerWarning: {
backgroundColor: theme.awayIndicator,
},
iconContainer: {
top: 1,
width: 32,
},
icon: {
fontSize: 18,
color: theme.buttonColor,
alignSelf: 'center',
},
warningIcon: {
color: theme.callsBg,
},
textContainer: {
flex: 1,
marginLeft: 8,
},
errorText: {
...typography('Body', 100, 'SemiBold'),
color: theme.buttonColor,
},
warningText: {
color: theme.callsBg,
},
dismissContainer: {
alignItems: 'center',
width: 32,
height: '100%',
justifyContent: 'center',
},
closeIcon: {
color: changeOpacity(theme.buttonColor, 0.56),
},
closeIconWarning: {
color: changeOpacity(theme.callsBg, 0.56),
},
}));
const MessageBar = ({type, onPress}: Props) => {
const MessageBar = ({type, onDismiss}: Props) => {
const intl = useIntl();
const theme = useTheme();
const callsTheme = useMemo(() => makeCallsTheme(theme), [theme]);
@ -100,7 +110,7 @@ const MessageBar = ({type, onPress}: Props) => {
icon = (
<CompassIcon
name='microphone-off'
style={[style.errorIcon, style.paddingRight]}
style={[style.icon]}
/>);
break;
case Calls.MessageBarType.CallQuality:
@ -111,38 +121,30 @@ const MessageBar = ({type, onPress}: Props) => {
icon = (
<CompassIcon
name='alert-outline'
style={[style.errorIcon, style.warningIcon, style.paddingRight]}
style={[style.icon, style.warningIcon]}
/>);
break;
}
return (
<View style={style.errorWrapper}>
<View style={[style.outerContainer, warning && style.outerContainerWarning]}>
<Pressable
onPress={Permissions.openSettings}
style={[style.errorBar, warning && style.warningBar]}
style={[style.innerContainer, warning && style.innerContainerWarning]}
>
{icon}
<Text style={[style.errorText, warning && style.warningText]}>{message}</Text>
<Pressable
onPress={onPress}
hitSlop={5}
style={({pressed}) => [
style.pressable,
style.iconContainer,
pressed && style.pressedIconContainer,
]}
>
{({pressed}) => (
<View style={style.iconContainer}>
{icon}
</View>
<View style={style.textContainer}>
<Text style={[style.errorText, warning && style.warningText]}>{message}</Text>
</View>
<Pressable onPress={onDismiss}>
<View style={style.dismissContainer}>
<CompassIcon
name='close'
style={[style.errorIcon,
warning && style.warningIcon,
pressed && style.pressedErrorIcon,
pressed && warning && style.pressedWarningIcon,
]}
style={[style.icon, style.closeIcon, warning && style.closeIconWarning]}
/>
)}
</View>
</Pressable>
</Pressable>
</View>

View file

@ -8,8 +8,9 @@ import {useIntl} from 'react-intl';
import {Alert, Platform} from 'react-native';
import Permissions from 'react-native-permissions';
import {CALL_ERROR_BAR_HEIGHT, CALL_NOTIFICATION_BAR_HEIGHT, CURRENT_CALL_BAR_HEIGHT, JOIN_CALL_BAR_HEIGHT} from '@app/constants/view';
import {initializeVoiceTrack} from '@calls/actions/calls';
import {setMicPermissionsGranted} from '@calls/state';
import {setMicPermissionsGranted, useCallsState, useChannelsWithCalls, useCurrentCall, useGlobalCallsState, useIncomingCalls} from '@calls/state';
import {errorAlert} from '@calls/utils';
import {useServerUrl} from '@context/server';
import {useAppState} from '@hooks/device';
@ -108,3 +109,27 @@ export const usePermissionsChecker = (micPermissionsGranted: boolean) => {
}
}, [appState]);
};
export const useCallsAdjustment = (serverUrl: string, channelId: string) => {
const incomingCalls = useIncomingCalls().incomingCalls;
const channelsWithCalls = useChannelsWithCalls(serverUrl);
const callsState = useCallsState(serverUrl);
const globalCallsState = useGlobalCallsState();
const currentCall = useCurrentCall();
const dismissed = Boolean(callsState.calls[channelId]?.dismissed[callsState.myUserId]);
const inCurrentCall = currentCall?.id === channelId;
const joinCallBannerVisible = Boolean(channelsWithCalls[channelId]) && !dismissed && !inCurrentCall;
// Do we have calls banners?
const currentCallBarVisible = Boolean(currentCall);
const micPermissionsError = !globalCallsState.micPermissionsGranted && (currentCall && !currentCall.micPermissionsErrorDismissed);
const callQualityAlert = Boolean(currentCall?.callQualityAlert);
const incomingCallsShowing = incomingCalls.filter((ic) => ic.channelID !== channelId);
const callsIncomingAdjustment = (incomingCallsShowing.length * CALL_NOTIFICATION_BAR_HEIGHT) + (incomingCallsShowing.length * 8);
const callsAdjustment = (currentCallBarVisible ? CURRENT_CALL_BAR_HEIGHT + 8 : 0) +
(micPermissionsError ? CALL_ERROR_BAR_HEIGHT + 8 : 0) +
(callQualityAlert ? CALL_ERROR_BAR_HEIGHT + 8 : 0) +
(joinCallBannerVisible ? JOIN_CALL_BAR_HEIGHT + 8 : 0) +
callsIncomingAdjustment;
return callsAdjustment;
};

View file

@ -105,6 +105,12 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: CallsTheme) => ({
height: '100%',
alignItems: 'center',
},
floatingBarsContainer: {
flexDirection: 'column',
width: '100%',
gap: 8,
marginBottom: 8,
},
header: {
flexDirection: 'row',
alignItems: 'center',
@ -172,8 +178,10 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: CallsTheme) => ({
marginTop: 5,
marginBottom: 0,
},
profileScreenOn: {
marginBottom: 2,
},
username: {
marginTop: 10,
width: usernameL,
textAlign: 'center',
color: theme.buttonColor,
@ -183,11 +191,6 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: CallsTheme) => ({
marginTop: 0,
width: usernameM,
},
incomingCallContainer: {
width: '100%',
marginBottom: 8,
gap: 8,
},
buttonsContainer: {
alignItems: 'center',
},
@ -604,16 +607,18 @@ const CallScreen = ({
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}
/>
<View style={[screenShareOn && style.profileScreenOn]}>
<CallAvatar
userModel={user.userModel}
speaking={currentCall.voiceOn[user.id]}
muted={user.muted}
sharingScreen={user.id === currentCall.screenOn}
raisedHand={Boolean(user.raisedHand)}
reaction={user.reaction?.emoji}
size={avatarSize}
serverUrl={currentCall.serverUrl}
/>
</View>
<Text
style={[style.username, smallerAvatar && style.usernameShort]}
numberOfLines={1}
@ -691,29 +696,29 @@ const CallScreen = ({
{!isLandscape && currentCall.reactionStream.length > 0 &&
<EmojiList reactionStream={currentCall.reactionStream}/>
}
{showIncomingCalls &&
<View style={style.incomingCallContainer}>
{incomingCalls.incomingCalls.map((ic) => (
<View style={style.floatingBarsContainer}>
{showIncomingCalls &&
incomingCalls.incomingCalls.map((ic) => (
<CallNotification
key={ic.callID}
incomingCall={ic}
onCallsScreen={true}
/>
))}
</View>
}
{micPermissionsError &&
<MessageBar
type={Calls.MessageBarType.Microphone}
onPress={setMicPermissionsErrorDismissed}
/>
}
{currentCall.callQualityAlert &&
<MessageBar
type={Calls.MessageBarType.CallQuality}
onPress={setCallQualityAlertDismissed}
/>
}
))
}
{micPermissionsError &&
<MessageBar
type={Calls.MessageBarType.Microphone}
onDismiss={setMicPermissionsErrorDismissed}
/>
}
{currentCall.callQualityAlert &&
<MessageBar
type={Calls.MessageBarType.CallQuality}
onDismiss={setCallQualityAlertDismissed}
/>
}
</View>
<View style={[style.buttonsContainer]}>
<View
style={[

View file

@ -7,8 +7,6 @@ import {type Edge, SafeAreaView, useSafeAreaInsets} from 'react-native-safe-area
import {storeLastViewedChannelIdAndServer, removeLastViewedChannelIdAndServer} from '@actions/app/global';
import FloatingCallContainer from '@calls/components/floating_call_container';
import {IncomingCallsContainer} from '@calls/components/incoming_calls_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 {Screens} from '@constants';
@ -110,7 +108,7 @@ const Channel = ({
setContainerHeight(e.nativeEvent.layout.height);
}, []);
const showFloatingCallContainer = showJoinCallBanner || isInACall;
const showFloatingCallContainer = showJoinCallBanner || isInACall || showIncomingCalls;
return (
<FreezeScreen>
@ -127,15 +125,12 @@ const Channel = ({
callsEnabledInChannel={isCallsEnabledInChannel}
isTabletView={isTabletView}
/>
{showJoinCallBanner && <RoundedHeaderCalls/>}
{shouldRender &&
<>
<View style={[styles.flex, {marginTop}]}>
<ChannelPostList
channelId={channelId}
nativeID={channelId}
currentCallBarVisible={isInACall}
joinCallBannerVisible={showJoinCallBanner}
/>
</View>
<PostDraft
@ -154,16 +149,10 @@ const Channel = ({
<FloatingCallContainer
channelId={channelId}
showJoinCallBanner={showJoinCallBanner}
showIncomingCalls={showIncomingCalls}
isInACall={isInACall}
/>
}
{showIncomingCalls &&
<IncomingCallsContainer
channelId={channelId}
showingJoinCallBanner={showJoinCallBanner}
showingCurrentCallBanner={isInACall}
/>
}
</SafeAreaView>
</FreezeScreen>
);

View file

@ -29,8 +29,6 @@ type Props = {
nativeID: string;
posts: PostModel[];
shouldShowJoinLeaveMessages: boolean;
currentCallBarVisible: boolean;
joinCallBannerVisible: boolean;
}
const edges: Edge[] = ['bottom'];
@ -42,7 +40,6 @@ const styles = StyleSheet.create({
const ChannelPostList = ({
channelId, contentContainerStyle, isCRTEnabled,
lastViewedAt, nativeID, posts, shouldShowJoinLeaveMessages,
currentCallBarVisible, joinCallBannerVisible,
}: Props) => {
const appState = useAppState();
const isTablet = useIsTablet();
@ -110,8 +107,6 @@ const ChannelPostList = ({
shouldShowJoinLeaveMessages={shouldShowJoinLeaveMessages}
showMoreMessages={true}
testID='channel.post_list'
currentCallBarVisible={currentCallBarVisible}
joinCallBannerVisible={joinCallBannerVisible}
/>
);

View file

@ -8,8 +8,6 @@ import {type Edge, SafeAreaView} from 'react-native-safe-area-context';
import {storeLastViewedThreadIdAndServer, removeLastViewedThreadIdAndServer} from '@actions/app/global';
import FloatingCallContainer from '@calls/components/floating_call_container';
import {IncomingCallsContainer} from '@calls/components/incoming_calls_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';
@ -111,7 +109,7 @@ const Thread = ({
setContainerHeight(e.nativeEvent.layout.height);
}, []);
const showFloatingCallContainer = showJoinCallBanner || isInACall;
const showFloatingCallContainer = showJoinCallBanner || isInACall || showIncomingCalls;
return (
<FreezeScreen>
@ -123,7 +121,6 @@ const Thread = ({
onLayout={onLayout}
>
<RoundedHeaderContext/>
{showJoinCallBanner && <RoundedHeaderCalls threadScreen={true}/>}
{Boolean(rootPost) &&
<>
<View style={styles.flex}>
@ -148,18 +145,11 @@ const Thread = ({
<FloatingCallContainer
channelId={rootPost!.channelId}
showJoinCallBanner={showJoinCallBanner}
showIncomingCalls={showIncomingCalls}
isInACall={isInACall}
threadScreen={true}
/>
}
{showIncomingCalls &&
<IncomingCallsContainer
channelId={rootPost!.channelId}
showingJoinCallBanner={showJoinCallBanner}
showingCurrentCallBanner={isInACall}
threadScreen={true}
/>
}
</SafeAreaView>
</FreezeScreen>
);

View file

@ -90,12 +90,27 @@ export const emitNotificationError = (type: 'Team' | 'Channel' | 'Post' | 'Conne
export const scheduleExpiredNotification = (serverUrl: string, session: Session, serverName: string, locale = DEFAULT_LOCALE) => {
const expiresAt = session?.expires_at || 0;
const expiresInDays = Math.ceil(Math.abs(moment.duration(moment().diff(moment(expiresAt))).asDays()));
const expiresInHours = Math.ceil(Math.abs(moment.duration(moment().diff(moment(expiresAt))).asHours()));
const expiresInDays = Math.floor(expiresInHours / 24); // Calculate expiresInDays
const remainingHours = expiresInHours % 24; // Calculate remaining hours
const intl = createIntl({locale, messages: getTranslations(locale)});
const body = intl.formatMessage({
id: 'mobile.session_expired',
defaultMessage: 'Please log in to continue receiving notifications. Sessions for {siteName} are configured to expire every {daysCount, number} {daysCount, plural, one {day} other {days}}.',
}, {siteName: serverName, daysCount: expiresInDays});
let body = '';
if (expiresInDays === 0) {
body = intl.formatMessage({
id: 'mobile.session_expired_hrs',
defaultMessage: 'Please log in to continue receiving notifications. Sessions for {siteName} are configured to expire every {hoursCount, number} {hoursCount, plural, one {hour} other {hours}}.',
}, {siteName: serverName, hoursCount: remainingHours});
} else if (expiresInHours === 0) {
body = intl.formatMessage({
id: 'mobile.session_expired_days',
defaultMessage: 'Please log in to continue receiving notifications. Sessions for {siteName} are configured to expire every {daysCount, number} {daysCount, plural, one {day} other {days}}.',
}, {siteName: serverName, daysCount: expiresInDays});
} else {
body = intl.formatMessage({
id: 'mobile.session_expired_days_hrs',
defaultMessage: 'Please log in to continue receiving notifications. Sessions for {siteName} are configured to expire every {daysCount, number} {daysCount, plural, one {day} other {days}} and {hoursCount, number} {hoursCount, plural, one {hour} other {hours}}.',
}, {siteName: serverName, daysCount: expiresInDays, hoursCount: remainingHours});
}
const title = intl.formatMessage({id: 'mobile.session_expired.title', defaultMessage: 'Session Expired'});
if (expiresAt) {

View file

@ -451,7 +451,6 @@
"mobile.calls_host_rec_title": "You are recording",
"mobile.calls_incoming_dm": "<b>{name}</b> is inviting you to a call",
"mobile.calls_incoming_gm": "<b>{name}</b> is inviting you to a call with <b>{num, plural, one {# other} other {# others}}</b>",
"mobile.calls_join_button": "Join",
"mobile.calls_join_call": "Join call",
"mobile.calls_lasted": "Lasted {duration}",
"mobile.calls_leave": "Leave",
@ -710,7 +709,9 @@
"mobile.server_url.deeplink.emm.denied": "This app is controlled by an EMM and the DeepLink server url does not match the EMM allowed server",
"mobile.server_url.empty": "Please enter a valid server URL",
"mobile.server_url.invalid_format": "URL must start with http:// or https://",
"mobile.session_expired": "Please log in to continue receiving notifications. Sessions for {siteName} are configured to expire every {daysCount, number} {daysCount, plural, one {day} other {days}}.",
"mobile.session_expired_days": "Please log in to continue receiving notifications. Sessions for {siteName} are configured to expire every {daysCount, number} {daysCount, plural, one {day} other {days}}.",
"mobile.session_expired_days_hrs": "Please log in to continue receiving notifications. Sessions for {siteName} are configured to expire every {daysCount, number} {daysCount, plural, one {day} other {days}} and {hoursCount, number} {hoursCount, plural, one {hour} other {hours}}.",
"mobile.session_expired_hrs": "Please log in to continue receiving notifications. Sessions for {siteName} are configured to expire every {hoursCount, number} {hoursCount, plural, one {hour} other {hours}}.",
"mobile.session_expired.title": "Session Expired",
"mobile.set_status.dnd": "Do Not Disturb",
"mobile.storage_permission_denied_description": "Upload files to your server. Open Settings to grant {applicationName} Read and Write access to files on this device.",

View file

@ -1929,7 +1929,7 @@
CODE_SIGN_ENTITLEMENTS = Mattermost/Mattermost.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CURRENT_PROJECT_VERSION = 490;
CURRENT_PROJECT_VERSION = 492;
DEVELOPMENT_TEAM = UQ8HT4Q2XM;
ENABLE_BITCODE = NO;
HEADER_SEARCH_PATHS = (
@ -1973,7 +1973,7 @@
CODE_SIGN_ENTITLEMENTS = Mattermost/Mattermost.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CURRENT_PROJECT_VERSION = 490;
CURRENT_PROJECT_VERSION = 492;
DEVELOPMENT_TEAM = UQ8HT4Q2XM;
ENABLE_BITCODE = NO;
HEADER_SEARCH_PATHS = (
@ -2116,7 +2116,7 @@
CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 490;
CURRENT_PROJECT_VERSION = 492;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = UQ8HT4Q2XM;
GCC_C_LANGUAGE_STANDARD = gnu11;
@ -2165,7 +2165,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 490;
CURRENT_PROJECT_VERSION = 492;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = UQ8HT4Q2XM;
GCC_C_LANGUAGE_STANDARD = gnu11;

View file

@ -37,7 +37,7 @@
</dict>
</array>
<key>CFBundleVersion</key>
<string>490</string>
<string>492</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSRequiresIPhoneOS</key>

View file

@ -21,7 +21,7 @@
<key>CFBundleShortVersionString</key>
<string>2.10.0</string>
<key>CFBundleVersion</key>
<string>490</string>
<string>492</string>
<key>UIAppFonts</key>
<array>
<string>OpenSans-Bold.ttf</string>

View file

@ -21,7 +21,7 @@
<key>CFBundleShortVersionString</key>
<string>2.10.0</string>
<key>CFBundleVersion</key>
<string>490</string>
<string>492</string>
<key>NSExtension</key>
<dict>
<key>NSExtensionPointIdentifier</key>