MM-53486 - Incoming calls notification banner (#7477)
* upgrade calls-common * incoming call notifications * tests * i18n * PR comments * PR comments * PR comment
This commit is contained in:
parent
469ca75d49
commit
83f81985cd
29 changed files with 890 additions and 63 deletions
|
|
@ -28,6 +28,7 @@ import {
|
|||
handleCallUserUnraiseHand,
|
||||
handleCallUserVoiceOff,
|
||||
handleCallUserVoiceOn,
|
||||
handleUserDismissedNotification,
|
||||
} from '@calls/connection/websocket_event_handlers';
|
||||
import {isSupportedServerCalls} from '@calls/utils';
|
||||
import {Screens, WebsocketEvents} from '@constants';
|
||||
|
|
@ -392,6 +393,9 @@ export async function handleEvent(serverUrl: string, msg: WebSocketMessage) {
|
|||
case WebsocketEvents.CALLS_HOST_CHANGED:
|
||||
handleCallHostChanged(serverUrl, msg);
|
||||
break;
|
||||
case WebsocketEvents.CALLS_USER_DISMISSED_NOTIFICATION:
|
||||
handleUserDismissedNotification(serverUrl, msg);
|
||||
break;
|
||||
|
||||
case WebsocketEvents.GROUP_RECEIVED:
|
||||
handleGroupReceivedEvent(serverUrl, msg);
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ 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 ANNOUNCEMENT_BAR_HEIGHT = 40;
|
||||
|
||||
|
|
|
|||
|
|
@ -76,6 +76,7 @@ const WebsocketEvents = {
|
|||
CALLS_USER_REACTED: `custom_${Calls.PluginId}_user_reacted`,
|
||||
CALLS_RECORDING_STATE: `custom_${Calls.PluginId}_call_recording_state`,
|
||||
CALLS_HOST_CHANGED: `custom_${Calls.PluginId}_call_host_changed`,
|
||||
CALLS_USER_DISMISSED_NOTIFICATION: `custom_${Calls.PluginId}_user_dismissed_notification`,
|
||||
GROUP_RECEIVED: 'received_group',
|
||||
GROUP_MEMBER_ADD: 'group_member_add',
|
||||
GROUP_MEMBER_DELETE: 'group_member_delete',
|
||||
|
|
|
|||
|
|
@ -67,6 +67,7 @@ const mockClient = {
|
|||
enableChannelCalls: jest.fn(),
|
||||
startCallRecording: jest.fn(),
|
||||
stopCallRecording: jest.fn(),
|
||||
dismissCall: jest.fn(),
|
||||
};
|
||||
|
||||
jest.mock('@calls/connection/connection', () => ({
|
||||
|
|
@ -98,7 +99,8 @@ jest.mock('react-native-navigation', () => ({
|
|||
}));
|
||||
|
||||
const addFakeCall = (serverUrl: string, channelId: string) => {
|
||||
const call = {
|
||||
const call: Call = {
|
||||
id: 'call',
|
||||
participants: {
|
||||
xohi8cki9787fgiryne716u84o: {id: 'xohi8cki9787fgiryne716u84o', muted: false, raisedHand: 0},
|
||||
xohi8cki9787fgiryne716u841: {id: 'xohi8cki9787fgiryne716u84o', muted: true, raisedHand: 0},
|
||||
|
|
@ -113,7 +115,8 @@ const addFakeCall = (serverUrl: string, channelId: string) => {
|
|||
threadId: 'abcd1234567',
|
||||
ownerId: 'xohi8cki9787fgiryne716u84o',
|
||||
hostId: 'xohi8cki9787fgiryne716u84o',
|
||||
} as Call;
|
||||
dismissed: {},
|
||||
};
|
||||
act(() => {
|
||||
State.setCallsState(serverUrl, {myUserId: 'myUserId', calls: {}, enabled: {}});
|
||||
State.callStarted(serverUrl, call);
|
||||
|
|
@ -158,7 +161,7 @@ describe('Actions.Calls', () => {
|
|||
setCallsState('server1', DefaultCallsState);
|
||||
setChannelsWithCalls('server1', {});
|
||||
setCurrentCall(null);
|
||||
setCallsConfig('server1', DefaultCallsConfig);
|
||||
setCallsConfig('server1', {...DefaultCallsConfig, EnableRinging: true});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -385,4 +388,12 @@ describe('Actions.Calls', () => {
|
|||
expect(needsRecordingErrorAlert).toBeCalled();
|
||||
expect(needsRecordingWillBePostedAlert).toBeCalled();
|
||||
});
|
||||
|
||||
it('dismissIncomingCall', async () => {
|
||||
await act(async () => {
|
||||
await CallsActions.dismissIncomingCall('server1', 'channel-id');
|
||||
});
|
||||
|
||||
expect(mockClient.dismissCall).toBeCalledWith('channel-id');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -7,21 +7,21 @@ import InCallManager from 'react-native-incall-manager';
|
|||
import {forceLogoutIfNecessary} from '@actions/remote/session';
|
||||
import {updateThreadFollowing} from '@actions/remote/thread';
|
||||
import {fetchUsersByIds} from '@actions/remote/user';
|
||||
import {leaveAndJoinWithAlert, needsRecordingWillBePostedAlert, needsRecordingErrorAlert} from '@calls/alerts';
|
||||
import {leaveAndJoinWithAlert, needsRecordingErrorAlert, needsRecordingWillBePostedAlert} from '@calls/alerts';
|
||||
import {
|
||||
getCallsConfig,
|
||||
getCallsState,
|
||||
getChannelsWithCalls,
|
||||
getCurrentCall,
|
||||
myselfLeftCall,
|
||||
newCurrentCall,
|
||||
setCallForChannel,
|
||||
setCalls,
|
||||
setChannelEnabled,
|
||||
setConfig,
|
||||
setPluginEnabled,
|
||||
setScreenShareURL,
|
||||
setSpeakerPhone,
|
||||
setCallForChannel,
|
||||
newCurrentCall,
|
||||
myselfLeftCall,
|
||||
getCurrentCall,
|
||||
getChannelsWithCalls,
|
||||
} from '@calls/state';
|
||||
import {General, Preferences} from '@constants';
|
||||
import Calls from '@constants/calls';
|
||||
|
|
@ -39,12 +39,7 @@ import {displayUsername, getUserIdFromChannelName, isSystemAdmin} from '@utils/u
|
|||
|
||||
import {newConnection} from '../connection/connection';
|
||||
|
||||
import type {
|
||||
AudioDevice,
|
||||
Call,
|
||||
CallParticipant,
|
||||
CallsConnection,
|
||||
} from '@calls/types/calls';
|
||||
import type {AudioDevice, Call, CallParticipant, CallsConnection} from '@calls/types/calls';
|
||||
import type {CallChannelState, CallState, EmojiData} from '@mattermost/calls/lib/types';
|
||||
import type {IntlShape} from 'react-intl';
|
||||
|
||||
|
|
@ -150,12 +145,14 @@ const createCallAndAddToIds = (channelId: string, call: CallState, ids: Set<stri
|
|||
return accum;
|
||||
}, {} as Dictionary<CallParticipant>),
|
||||
channelId,
|
||||
id: call.id,
|
||||
startTime: call.start_at,
|
||||
screenOn: call.screen_sharing_id,
|
||||
threadId: call.thread_id,
|
||||
ownerId: call.owner_id,
|
||||
hostId: call.host_id,
|
||||
recState: call.recording,
|
||||
dismissed: call.dismissed_notification || {},
|
||||
} as Call;
|
||||
};
|
||||
|
||||
|
|
@ -417,6 +414,21 @@ export const stopCallRecording = async (serverUrl: string, callId: string) => {
|
|||
}
|
||||
};
|
||||
|
||||
export const dismissIncomingCall = async (serverUrl: string, channelId: string) => {
|
||||
if (!getCallsConfig(serverUrl).EnableRinging) {
|
||||
return {};
|
||||
}
|
||||
|
||||
try {
|
||||
const client = NetworkManager.getClient(serverUrl);
|
||||
return await client.dismissCall(channelId);
|
||||
} catch (error) {
|
||||
logDebug('error on dismissIncomingCall', getFullErrorMessage(error));
|
||||
await forceLogoutIfNecessary(serverUrl, error);
|
||||
return error;
|
||||
}
|
||||
};
|
||||
|
||||
// handleCallsSlashCommand will return true if the slash command was handled
|
||||
export const handleCallsSlashCommand = async (value: string, serverUrl: string, channelId: string, rootId: string, currentUserId: string, intl: IntlShape):
|
||||
Promise<{ handled?: boolean; error?: string }> => {
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ export {
|
|||
handleCallsSlashCommand,
|
||||
startCallRecording,
|
||||
stopCallRecording,
|
||||
dismissIncomingCall,
|
||||
} from './calls';
|
||||
|
||||
export {hasMicrophonePermission} from './permissions';
|
||||
|
|
|
|||
|
|
@ -4,12 +4,14 @@
|
|||
import {Alert} from 'react-native';
|
||||
|
||||
import {hasMicrophonePermission, joinCall, leaveCall, unmuteMyself} from '@calls/actions';
|
||||
import {dismissIncomingCall} from '@calls/actions/calls';
|
||||
import {hasBluetoothPermission} from '@calls/actions/permissions';
|
||||
import {
|
||||
getCallsConfig,
|
||||
getCallsState,
|
||||
getChannelsWithCalls,
|
||||
getCurrentCall,
|
||||
removeIncomingCall,
|
||||
setMicPermissionsGranted,
|
||||
} from '@calls/state';
|
||||
import {errorAlert} from '@calls/utils';
|
||||
|
|
@ -199,6 +201,13 @@ const doJoinCall = async (
|
|||
const hasPermission = await hasMicrophonePermission();
|
||||
setMicPermissionsGranted(hasPermission);
|
||||
|
||||
if (!newCall && joinChannelIsDMorGM) {
|
||||
// we're joining an existing call, so dismiss any notifications (for all clients, too)
|
||||
const callId = getCallsState(serverUrl).calls[channelId].id;
|
||||
dismissIncomingCall(serverUrl, channelId);
|
||||
removeIncomingCall(serverUrl, callId, channelId);
|
||||
}
|
||||
|
||||
const res = await joinCall(serverUrl, channelId, user.id, hasPermission, title, rootId);
|
||||
if (res.error) {
|
||||
const seeLogs = formatMessage({id: 'mobile.calls_see_logs', defaultMessage: 'See server logs'});
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ export interface ClientCallsMix {
|
|||
genTURNCredentials: () => Promise<RTCIceServer[]>;
|
||||
startCallRecording: (callId: string) => Promise<ApiResp | CallRecordingState>;
|
||||
stopCallRecording: (callId: string) => Promise<ApiResp | CallRecordingState>;
|
||||
dismissCall: (channelId: string) => Promise<ApiResp>;
|
||||
}
|
||||
|
||||
const ClientCalls = (superclass: any) => class extends superclass {
|
||||
|
|
@ -85,6 +86,13 @@ const ClientCalls = (superclass: any) => class extends superclass {
|
|||
{method: 'post'},
|
||||
);
|
||||
};
|
||||
|
||||
dismissCall = async (channelID: string) => {
|
||||
return this.doFetch(
|
||||
`${this.getCallsRoute()}/calls/${channelID}/dismiss-notification`,
|
||||
{method: 'post'},
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
export default ClientCalls;
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ import type UserModel from '@typings/database/models/servers/user';
|
|||
|
||||
type Props = {
|
||||
userModel?: UserModel;
|
||||
volume: number;
|
||||
volume?: number;
|
||||
serverUrl: string;
|
||||
size: number;
|
||||
muted?: boolean;
|
||||
|
|
@ -149,7 +149,7 @@ const getStyleSheet = ({
|
|||
});
|
||||
};
|
||||
|
||||
const CallAvatar = ({userModel, volume, serverUrl, sharingScreen, size, muted, raisedHand, reaction}: Props) => {
|
||||
const CallAvatar = ({userModel, volume = 0, 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]);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,227 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useEffect} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {Pressable, StyleSheet, 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 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 {typography} from '@utils/typography';
|
||||
import {displayUsername} from '@utils/user';
|
||||
|
||||
const style = StyleSheet.create({
|
||||
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),
|
||||
},
|
||||
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,
|
||||
alignItems: 'center',
|
||||
backgroundColor: changeOpacity('#000', 0.16),
|
||||
},
|
||||
innerOnCallsScreen: {
|
||||
borderColor: changeOpacity(Preferences.THEMES.denim.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),
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
dismissOnCallsScreen: {
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
dismissIcon: {
|
||||
color: Preferences.THEMES.denim.buttonColor,
|
||||
},
|
||||
});
|
||||
|
||||
type Props = {
|
||||
incomingCall: IncomingCallNotification;
|
||||
currentUserId: string;
|
||||
teammateNameDisplay: string;
|
||||
members?: ChannelMembershipModel[];
|
||||
onCallsScreen?: boolean;
|
||||
}
|
||||
|
||||
export const CallNotification = ({
|
||||
incomingCall,
|
||||
currentUserId,
|
||||
teammateNameDisplay,
|
||||
members,
|
||||
onCallsScreen,
|
||||
}: Props) => {
|
||||
const intl = useIntl();
|
||||
const serverUrl = useServerUrl();
|
||||
|
||||
useEffect(() => {
|
||||
const channelMembers = members?.filter((m) => m.userId !== currentUserId);
|
||||
if (!channelMembers?.length) {
|
||||
fetchProfilesInChannel(serverUrl, incomingCall.channelID, currentUserId, undefined, false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const onJoinPress = useCallback(() => {
|
||||
leaveAndJoinWithAlert(intl, incomingCall.serverUrl, incomingCall.channelID);
|
||||
}, [intl, incomingCall]);
|
||||
|
||||
const onContainerPress = useCallback(async () => {
|
||||
if (serverUrl !== incomingCall.serverUrl) {
|
||||
await DatabaseManager.setActiveServerDatabase(incomingCall.serverUrl);
|
||||
await WebsocketManager.initializeClient(incomingCall.serverUrl);
|
||||
}
|
||||
switchToChannelById(incomingCall.serverUrl, incomingCall.channelID);
|
||||
}, [serverUrl, incomingCall]);
|
||||
|
||||
const onDismissPress = useCallback(() => {
|
||||
removeIncomingCall(serverUrl, incomingCall.callID, incomingCall.channelID);
|
||||
dismissIncomingCall(incomingCall.serverUrl, incomingCall.channelID);
|
||||
}, [incomingCall]);
|
||||
|
||||
let message: React.ReactElement;
|
||||
if (incomingCall.type === ChannelType.DM) {
|
||||
message = (
|
||||
<FormattedText
|
||||
id={'mobile.calls_incoming_dm'}
|
||||
defaultMessage={'<b>{name}</b> is inviting you to a call'}
|
||||
values={{
|
||||
b: (text: string) => <Text style={style.boldText}>{text}</Text>,
|
||||
name: displayUsername(incomingCall.callerModel, intl.locale, teammateNameDisplay),
|
||||
}}
|
||||
style={style.text}
|
||||
numberOfLines={2}
|
||||
ellipsizeMode={'tail'}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
message = (
|
||||
<FormattedText
|
||||
id={'mobile.calls_incoming_gm'}
|
||||
defaultMessage={'<b>{name}</b> is inviting you to a call with <b>{num, plural, one {# other} other {# others}}</b>'}
|
||||
values={{
|
||||
b: (text: string) => <Text style={style.boldText}>{text}</Text>,
|
||||
name: displayUsername(incomingCall.callerModel, intl.locale, teammateNameDisplay),
|
||||
num: (members?.length || 2) - 1,
|
||||
}}
|
||||
style={style.text}
|
||||
numberOfLines={2}
|
||||
ellipsizeMode={'tail'}
|
||||
/>
|
||||
);
|
||||
}
|
||||
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]}>
|
||||
<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'}
|
||||
size={24}
|
||||
style={style.dismissIcon}
|
||||
/>
|
||||
</Pressable>
|
||||
</Pressable>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
42
app/products/calls/components/call_notification/index.ts
Normal file
42
app/products/calls/components/call_notification/index.ts
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import withObservables from '@nozbe/with-observables';
|
||||
import {of as of$} from 'rxjs';
|
||||
import {distinctUntilChanged, switchMap} from 'rxjs/operators';
|
||||
|
||||
import {CallNotification} from '@calls/components/call_notification/call_notification';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {observeChannelMembers} from '@queries/servers/channel';
|
||||
import {observeCurrentUserId} from '@queries/servers/system';
|
||||
import {observeTeammateNameDisplay} from '@queries/servers/user';
|
||||
|
||||
import type {IncomingCallNotification} from '@calls/types/calls';
|
||||
|
||||
type OwnProps = {
|
||||
incomingCall: IncomingCallNotification;
|
||||
}
|
||||
|
||||
const enhanced = withObservables(['incomingCall'], ({incomingCall}: OwnProps) => {
|
||||
const database = of$(DatabaseManager.serverDatabases[incomingCall.serverUrl]?.database);
|
||||
const currentUserId = database.pipe(
|
||||
switchMap((db) => (db ? observeCurrentUserId(db) : of$(''))),
|
||||
distinctUntilChanged(),
|
||||
);
|
||||
const teammateNameDisplay = database.pipe(
|
||||
switchMap((db) => (db ? observeTeammateNameDisplay(db) : of$(''))),
|
||||
distinctUntilChanged(),
|
||||
);
|
||||
const members = database.pipe(
|
||||
switchMap((db) => (db ? observeChannelMembers(db, incomingCall.channelID) : of$([]))),
|
||||
distinctUntilChanged(),
|
||||
);
|
||||
|
||||
return {
|
||||
currentUserId,
|
||||
teammateNameDisplay,
|
||||
members,
|
||||
};
|
||||
});
|
||||
|
||||
export default enhanced(CallNotification);
|
||||
|
|
@ -39,10 +39,7 @@ type Props = {
|
|||
const getStyleSheet = makeStyleSheetFromTheme((theme: CallsTheme) => {
|
||||
return {
|
||||
wrapper: {
|
||||
marginTop: 8,
|
||||
marginRight: 6,
|
||||
marginBottom: 8,
|
||||
marginLeft: 6,
|
||||
margin: 8,
|
||||
backgroundColor: theme.callsBg,
|
||||
borderRadius: 8,
|
||||
},
|
||||
|
|
|
|||
71
app/products/calls/components/incoming_calls_container.tsx
Normal file
71
app/products/calls/components/incoming_calls_container.tsx
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
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;
|
||||
|
||||
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);
|
||||
if (calls.length === 0) {
|
||||
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]}>
|
||||
{calls.map((ic) => (
|
||||
<CallNotification
|
||||
key={ic.callID}
|
||||
incomingCall={ic}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
|
@ -28,7 +28,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: CallsTheme) => (
|
|||
zIndex: 10,
|
||||
},
|
||||
errorWrapper: {
|
||||
padding: 10,
|
||||
padding: 8,
|
||||
paddingTop: 0,
|
||||
},
|
||||
errorBar: {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import {fetchUsersByIds} from '@actions/remote/user';
|
|||
import {
|
||||
callEnded,
|
||||
callStarted,
|
||||
removeIncomingCall,
|
||||
setCallScreenOff,
|
||||
setCallScreenOn,
|
||||
setChannelEnabled,
|
||||
|
|
@ -21,14 +22,21 @@ import {
|
|||
} from '@calls/state';
|
||||
import {WebsocketEvents} from '@constants';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {getCurrentUserId} from '@queries/servers/system';
|
||||
|
||||
import type {
|
||||
CallHostChangedData, CallRecordingStateData,
|
||||
CallHostChangedData,
|
||||
CallRecordingStateData,
|
||||
CallStartData,
|
||||
EmptyData,
|
||||
UserConnectedData,
|
||||
UserDisconnectedData, UserMutedUnmutedData, UserRaiseUnraiseHandData,
|
||||
UserReactionData, UserScreenOnOffData, UserVoiceOnOffData,
|
||||
UserDisconnectedData,
|
||||
UserDismissedNotification,
|
||||
UserMutedUnmutedData,
|
||||
UserRaiseUnraiseHandData,
|
||||
UserReactionData,
|
||||
UserScreenOnOffData,
|
||||
UserVoiceOnOffData,
|
||||
} from '@mattermost/calls/lib/types';
|
||||
|
||||
export const handleCallUserConnected = (serverUrl: string, msg: WebSocketMessage<UserConnectedData>) => {
|
||||
|
|
@ -59,12 +67,8 @@ export const handleCallUserVoiceOff = (msg: WebSocketMessage<UserVoiceOnOffData>
|
|||
};
|
||||
|
||||
export const handleCallStarted = (serverUrl: string, msg: WebSocketMessage<CallStartData>) => {
|
||||
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
|
||||
if (!database) {
|
||||
return;
|
||||
}
|
||||
|
||||
callStarted(serverUrl, {
|
||||
id: msg.data.id,
|
||||
channelId: msg.data.channelID,
|
||||
startTime: msg.data.start_at,
|
||||
threadId: msg.data.thread_id,
|
||||
|
|
@ -72,6 +76,7 @@ export const handleCallStarted = (serverUrl: string, msg: WebSocketMessage<CallS
|
|||
participants: {},
|
||||
ownerId: msg.data.owner_id,
|
||||
hostId: msg.data.host_id,
|
||||
dismissed: {},
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -118,3 +123,18 @@ export const handleCallRecordingState = (serverUrl: string, msg: WebSocketMessag
|
|||
export const handleCallHostChanged = (serverUrl: string, msg: WebSocketMessage<CallHostChangedData>) => {
|
||||
setHost(serverUrl, msg.broadcast.channel_id, msg.data.hostID);
|
||||
};
|
||||
|
||||
export const handleUserDismissedNotification = async (serverUrl: string, msg: WebSocketMessage<UserDismissedNotification>) => {
|
||||
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
|
||||
if (!database) {
|
||||
return;
|
||||
}
|
||||
|
||||
// For now we are only handling our own dismissed.
|
||||
const myUserId = await getCurrentUserId(database);
|
||||
if (myUserId !== msg.data.userID) {
|
||||
return;
|
||||
}
|
||||
|
||||
removeIncomingCall(serverUrl, msg.data.callID);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import {recordingAlert, recordingWillBePostedAlert, recordingErrorAlert} from '@
|
|||
import {AudioDeviceButton} from '@calls/components/audio_device_button';
|
||||
import CallAvatar from '@calls/components/call_avatar';
|
||||
import CallDuration from '@calls/components/call_duration';
|
||||
import CallNotification from '@calls/components/call_notification';
|
||||
import CallsBadge, {CallsBadgeType} from '@calls/components/calls_badge';
|
||||
import EmojiList from '@calls/components/emoji_list';
|
||||
import MessageBar from '@calls/components/message_bar';
|
||||
|
|
@ -34,7 +35,12 @@ 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 {setCallQualityAlertDismissed, setMicPermissionsErrorDismissed, useCallsConfig} from '@calls/state';
|
||||
import {
|
||||
setCallQualityAlertDismissed,
|
||||
setMicPermissionsErrorDismissed,
|
||||
useCallsConfig,
|
||||
useIncomingCalls,
|
||||
} from '@calls/state';
|
||||
import {getHandsRaised, makeCallsTheme, sortParticipants} from '@calls/utils';
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import FormattedText from '@components/formatted_text';
|
||||
|
|
@ -177,6 +183,11 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: CallsTheme) => ({
|
|||
marginTop: 0,
|
||||
width: usernameM,
|
||||
},
|
||||
incomingCallContainer: {
|
||||
width: '100%',
|
||||
marginBottom: 8,
|
||||
gap: 8,
|
||||
},
|
||||
buttonsContainer: {
|
||||
alignItems: 'center',
|
||||
},
|
||||
|
|
@ -316,6 +327,8 @@ const CallScreen = ({
|
|||
const serverUrl = useServerUrl();
|
||||
const {EnableRecordings} = useCallsConfig(serverUrl);
|
||||
usePermissionsChecker(micPermissionsGranted);
|
||||
const incomingCalls = useIncomingCalls();
|
||||
|
||||
const [showControlsInLandscape, setShowControlsInLandscape] = useState(false);
|
||||
const [showReactions, setShowReactions] = useState(false);
|
||||
const callsTheme = useMemo(() => makeCallsTheme(theme), [theme]);
|
||||
|
|
@ -330,6 +343,7 @@ const CallScreen = ({
|
|||
const smallerAvatar = isLandscape || screenShareOn;
|
||||
const avatarSize = smallerAvatar ? avatarM : avatarL;
|
||||
const numParticipants = Object.keys(participantsDict).length;
|
||||
const showIncomingCalls = incomingCalls.incomingCalls.length > 0;
|
||||
|
||||
const callThreadOptionTitle = intl.formatMessage({id: 'mobile.calls_call_thread', defaultMessage: 'Call Thread'});
|
||||
const recordOptionTitle = intl.formatMessage({id: 'mobile.calls_record', defaultMessage: 'Record'});
|
||||
|
|
@ -675,6 +689,17 @@ const CallScreen = ({
|
|||
{!isLandscape && currentCall.reactionStream.length > 0 &&
|
||||
<EmojiList reactionStream={currentCall.reactionStream}/>
|
||||
}
|
||||
{showIncomingCalls &&
|
||||
<View style={style.incomingCallContainer}>
|
||||
{incomingCalls.incomingCalls.map((ic) => (
|
||||
<CallNotification
|
||||
key={ic.callID}
|
||||
incomingCall={ic}
|
||||
onCallsScreen={true}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
}
|
||||
{micPermissionsError &&
|
||||
<MessageBar
|
||||
type={Calls.MessageBarType.Microphone}
|
||||
|
|
|
|||
|
|
@ -8,9 +8,12 @@ import {act, renderHook} from '@testing-library/react-hooks';
|
|||
import {needsRecordingAlert} from '@calls/alerts';
|
||||
import {
|
||||
newCurrentCall,
|
||||
processIncomingCalls,
|
||||
processMeanOpinionScore,
|
||||
removeIncomingCall,
|
||||
setAudioDeviceInfo,
|
||||
setCallQualityAlertDismissed,
|
||||
setCallsConfig,
|
||||
setCallsState,
|
||||
setChannelsWithCalls,
|
||||
setCurrentCall,
|
||||
|
|
@ -23,6 +26,7 @@ import {
|
|||
useChannelsWithCalls,
|
||||
useCurrentCall,
|
||||
useGlobalCallsState,
|
||||
useIncomingCalls,
|
||||
userReacted,
|
||||
} from '@calls/state';
|
||||
import {
|
||||
|
|
@ -52,6 +56,7 @@ import {
|
|||
DefaultCallsState,
|
||||
DefaultCurrentCall,
|
||||
DefaultGlobalCallsState,
|
||||
DefaultIncomingCalls,
|
||||
type GlobalCallsState,
|
||||
} from '@calls/types/calls';
|
||||
import {License} from '@constants';
|
||||
|
|
@ -75,6 +80,18 @@ jest.mock('@queries/servers/thread', () => ({
|
|||
})),
|
||||
}));
|
||||
|
||||
jest.mock('@queries/servers/channel', () => ({
|
||||
getChannelById: jest.fn(() => Promise.resolve({
|
||||
type: 'D',
|
||||
})),
|
||||
}));
|
||||
|
||||
jest.mock('@queries/servers/user', () => ({
|
||||
getUserById: jest.fn(() => Promise.resolve({
|
||||
username: 'user-5',
|
||||
})),
|
||||
}));
|
||||
|
||||
jest.mock('react-native-navigation', () => ({
|
||||
Navigation: {
|
||||
pop: jest.fn(() => Promise.resolve({
|
||||
|
|
@ -84,6 +101,7 @@ jest.mock('react-native-navigation', () => ({
|
|||
}));
|
||||
|
||||
const call1: Call = {
|
||||
id: 'call1',
|
||||
participants: {
|
||||
'user-1': {id: 'user-1', muted: false, raisedHand: 0},
|
||||
'user-2': {id: 'user-2', muted: true, raisedHand: 0},
|
||||
|
|
@ -94,8 +112,10 @@ const call1: Call = {
|
|||
threadId: 'thread-1',
|
||||
ownerId: 'user-1',
|
||||
hostId: 'user-1',
|
||||
dismissed: {},
|
||||
};
|
||||
const call2: Call = {
|
||||
id: 'call2',
|
||||
participants: {
|
||||
'user-3': {id: 'user-3', muted: false, raisedHand: 0},
|
||||
'user-4': {id: 'user-4', muted: true, raisedHand: 0},
|
||||
|
|
@ -106,8 +126,10 @@ const call2: Call = {
|
|||
threadId: 'thread-2',
|
||||
ownerId: 'user-3',
|
||||
hostId: 'user-3',
|
||||
dismissed: {},
|
||||
};
|
||||
const call3: Call = {
|
||||
id: 'call3',
|
||||
participants: {
|
||||
'user-5': {id: 'user-5', muted: false, raisedHand: 0},
|
||||
'user-6': {id: 'user-6', muted: true, raisedHand: 0},
|
||||
|
|
@ -118,6 +140,20 @@ const call3: Call = {
|
|||
threadId: 'thread-3',
|
||||
ownerId: 'user-5',
|
||||
hostId: 'user-5',
|
||||
dismissed: {},
|
||||
};
|
||||
const callDM: Call = {
|
||||
id: 'callDM',
|
||||
participants: {
|
||||
'user-5': {id: 'user-5', muted: false, raisedHand: 0},
|
||||
},
|
||||
channelId: 'channel-private',
|
||||
startTime: 123,
|
||||
screenOn: '',
|
||||
threadId: 'thread-4',
|
||||
ownerId: 'user-5',
|
||||
hostId: 'user-5',
|
||||
dismissed: {},
|
||||
};
|
||||
|
||||
describe('useCallsState', () => {
|
||||
|
|
@ -153,7 +189,7 @@ describe('useCallsState', () => {
|
|||
assert.deepEqual(result.current[1], {});
|
||||
});
|
||||
|
||||
it('setCalls, two callsState hooks, channelsWithCalls hook, ', () => {
|
||||
it('setCalls, two callsState hooks, channelsWithCalls hook, ', async () => {
|
||||
const initialCallsState = {
|
||||
...DefaultCallsState,
|
||||
calls: {'channel-1': call1},
|
||||
|
|
@ -217,7 +253,7 @@ describe('useCallsState', () => {
|
|||
assert.deepEqual(result.current[3], initialCurrentCallState);
|
||||
|
||||
// test
|
||||
act(() => setCalls('server1', 'myId', test.calls, test.enabled));
|
||||
await act(async () => setCalls('server1', 'myId', test.calls, test.enabled));
|
||||
assert.deepEqual(result.current[0], expectedCallsState);
|
||||
assert.deepEqual(result.current[1], expectedCallsState);
|
||||
assert.deepEqual(result.current[2], expectedChannelsWithCallsState);
|
||||
|
|
@ -242,6 +278,7 @@ describe('useCallsState', () => {
|
|||
};
|
||||
const expectedCallsState = {
|
||||
'channel-1': {
|
||||
id: 'call1',
|
||||
participants: {
|
||||
'user-1': {id: 'user-1', muted: false, raisedHand: 0},
|
||||
'user-2': {id: 'user-2', muted: true, raisedHand: 0},
|
||||
|
|
@ -253,6 +290,7 @@ describe('useCallsState', () => {
|
|||
threadId: 'thread-1',
|
||||
ownerId: 'user-1',
|
||||
hostId: 'user-1',
|
||||
dismissed: {},
|
||||
},
|
||||
};
|
||||
const expectedChannelsWithCallsState = initialChannelsWithCallsState;
|
||||
|
|
@ -302,6 +340,7 @@ describe('useCallsState', () => {
|
|||
};
|
||||
const expectedCallsState = {
|
||||
'channel-1': {
|
||||
id: 'call1',
|
||||
participants: {
|
||||
'user-2': {id: 'user-2', muted: true, raisedHand: 0},
|
||||
},
|
||||
|
|
@ -311,6 +350,7 @@ describe('useCallsState', () => {
|
|||
threadId: 'thread-1',
|
||||
ownerId: 'user-1',
|
||||
hostId: 'user-1',
|
||||
dismissed: {},
|
||||
},
|
||||
};
|
||||
const expectedChannelsWithCallsState = initialChannelsWithCallsState;
|
||||
|
|
@ -366,6 +406,7 @@ describe('useCallsState', () => {
|
|||
};
|
||||
const expectedCallsState = {
|
||||
'channel-1': {
|
||||
id: 'call1',
|
||||
participants: {
|
||||
'user-2': {id: 'user-2', muted: true, raisedHand: 0},
|
||||
},
|
||||
|
|
@ -375,6 +416,7 @@ describe('useCallsState', () => {
|
|||
ownerId: 'user-1',
|
||||
hostId: 'user-1',
|
||||
screenOn: '',
|
||||
dismissed: {},
|
||||
},
|
||||
};
|
||||
const expectedChannelsWithCallsState = initialChannelsWithCallsState;
|
||||
|
|
@ -563,6 +605,7 @@ describe('useCallsState', () => {
|
|||
};
|
||||
const expectedCalls = {
|
||||
'channel-1': {
|
||||
id: 'call1',
|
||||
participants: {
|
||||
'user-1': {id: 'user-1', muted: false, raisedHand: 0},
|
||||
'user-2': {id: 'user-2', muted: true, raisedHand: 345},
|
||||
|
|
@ -573,6 +616,7 @@ describe('useCallsState', () => {
|
|||
threadId: 'thread-1',
|
||||
ownerId: 'user-1',
|
||||
hostId: 'user-1',
|
||||
dismissed: {},
|
||||
},
|
||||
};
|
||||
const initialCurrentCallState: CurrentCall = {
|
||||
|
|
@ -989,7 +1033,7 @@ describe('useCallsState', () => {
|
|||
assert.deepEqual((result.current[1] as CurrentCall).callQualityAlert, true);
|
||||
});
|
||||
|
||||
it('voiceOn and Off', () => {
|
||||
it('voiceOn and Off', async () => {
|
||||
const initialCallsState = {
|
||||
...DefaultCallsState,
|
||||
myUserId: 'myUserId',
|
||||
|
|
@ -1025,7 +1069,7 @@ describe('useCallsState', () => {
|
|||
assert.deepEqual(result.current[0], initialCallsState);
|
||||
|
||||
// test that voice state is cleared on reconnect
|
||||
act(() => setCalls('server1', 'myUserId', initialCallsState.calls, {}));
|
||||
await act(() => setCalls('server1', 'myUserId', initialCallsState.calls, {}));
|
||||
assert.deepEqual(result.current[1], initialCurrentCallState);
|
||||
assert.deepEqual(result.current[0], initialCallsState);
|
||||
});
|
||||
|
|
@ -1248,4 +1292,111 @@ describe('useCallsState', () => {
|
|||
act(() => setHost('server1', 'channel-1', 'myUserId'));
|
||||
expect(needsRecordingAlert).toBeCalled();
|
||||
});
|
||||
|
||||
it('incoming calls', async () => {
|
||||
const calls = {'channel-dm': callDM};
|
||||
const afterLoadCallsState: CallsState = {
|
||||
myUserId: 'myId',
|
||||
calls,
|
||||
enabled: {},
|
||||
};
|
||||
const initialCurrentCallState: CurrentCall | null = null;
|
||||
const initialIncomingCalls = DefaultIncomingCalls;
|
||||
const expectedIncomingCalls = {
|
||||
incomingCalls: [{
|
||||
callID: 'callDM',
|
||||
callerID: 'user-5',
|
||||
callerModel: {username: 'user-5'},
|
||||
channelID: 'channel-private',
|
||||
myUserId: 'myId',
|
||||
serverUrl: 'server1',
|
||||
startAt: 123,
|
||||
type: 0,
|
||||
}],
|
||||
};
|
||||
const dismissedCalls = {
|
||||
'channel-dm': {...callDM, dismissed: {myId: true}},
|
||||
};
|
||||
const callIStarted: Call = {
|
||||
id: 'callIStartedid',
|
||||
participants: {
|
||||
'user-5': {id: 'user-5', muted: false, raisedHand: 0},
|
||||
},
|
||||
channelId: 'channel-private2',
|
||||
startTime: 123,
|
||||
screenOn: '',
|
||||
threadId: 'thread-4',
|
||||
ownerId: 'myId',
|
||||
hostId: 'user-5',
|
||||
dismissed: {},
|
||||
};
|
||||
const callImIn: Call = {
|
||||
id: 'callImInId',
|
||||
participants: {},
|
||||
channelId: 'channel-private2',
|
||||
startTime: 123,
|
||||
screenOn: '',
|
||||
threadId: 'thread-4',
|
||||
ownerId: 'user-5',
|
||||
hostId: 'user-5',
|
||||
dismissed: {},
|
||||
};
|
||||
const currentCallStateImIn: CurrentCall = {
|
||||
...DefaultCurrentCall,
|
||||
serverUrl: 'server1',
|
||||
myUserId: 'myId',
|
||||
...callImIn,
|
||||
};
|
||||
|
||||
// setup
|
||||
await DatabaseManager.init(['server1']);
|
||||
const {result} = renderHook(() => {
|
||||
return [
|
||||
useCallsState('server1'),
|
||||
useCurrentCall(),
|
||||
useIncomingCalls(),
|
||||
];
|
||||
});
|
||||
act(() => {
|
||||
setCallsConfig('server1', {...DefaultCallsConfig, EnableRinging: true});
|
||||
});
|
||||
assert.deepEqual(result.current[0], DefaultCallsState);
|
||||
assert.deepEqual(result.current[1], initialCurrentCallState);
|
||||
assert.deepEqual(result.current[2], initialIncomingCalls);
|
||||
|
||||
// test incoming call on load
|
||||
await act(async () => setCalls('server1', 'myId', afterLoadCallsState.calls, {}));
|
||||
assert.deepEqual(result.current[0], afterLoadCallsState);
|
||||
assert.deepEqual(result.current[1], initialCurrentCallState);
|
||||
assert.deepEqual(result.current[2], expectedIncomingCalls);
|
||||
|
||||
// test dismissing (same path for manually dismissing, joining that call,
|
||||
// or receiving ws event from dismissing/joining from another client)
|
||||
act(() => removeIncomingCall('server1', 'callDM'));
|
||||
assert.deepEqual(result.current[0], afterLoadCallsState);
|
||||
assert.deepEqual(result.current[1], initialCurrentCallState);
|
||||
assert.deepEqual(result.current[2], DefaultIncomingCalls);
|
||||
|
||||
// test load call, but call has been dismissed
|
||||
await act(async () => setCalls('server1', 'myId', dismissedCalls, {}));
|
||||
assert.deepEqual(result.current[2], DefaultIncomingCalls);
|
||||
|
||||
// test load call, then load same call again (eg from ws event): should only have one notification
|
||||
await act(async () => setCalls('server1', 'myId', afterLoadCallsState.calls, {}));
|
||||
assert.deepEqual(result.current[2], expectedIncomingCalls);
|
||||
await act(async () => processIncomingCalls('server1', [calls['channel-dm']]));
|
||||
assert.deepEqual(result.current[2], expectedIncomingCalls);
|
||||
|
||||
// test received ws event for a call I started
|
||||
await act(async () => processIncomingCalls('server1', [callIStarted]));
|
||||
assert.deepEqual(result.current[2], expectedIncomingCalls);
|
||||
|
||||
// test received ws event for a call I am in
|
||||
await act(async () => {
|
||||
setCurrentCall(currentCallStateImIn);
|
||||
await processIncomingCalls('server1', [callImIn]);
|
||||
});
|
||||
assert.deepEqual(result.current[1], currentCallStateImIn);
|
||||
assert.deepEqual(result.current[2], expectedIncomingCalls);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -12,30 +12,37 @@ import {
|
|||
getChannelsWithCalls,
|
||||
getCurrentCall,
|
||||
getGlobalCallsState,
|
||||
getIncomingCalls,
|
||||
setCallsConfig,
|
||||
setCallsState,
|
||||
setChannelsWithCalls,
|
||||
setCurrentCall,
|
||||
setGlobalCallsState,
|
||||
setIncomingCalls,
|
||||
} from '@calls/state';
|
||||
import {
|
||||
type AudioDeviceInfo,
|
||||
type Call,
|
||||
type CallsConfigState,
|
||||
type ChannelsWithCalls,
|
||||
ChannelType,
|
||||
type CurrentCall,
|
||||
DefaultCall,
|
||||
DefaultCurrentCall,
|
||||
type IncomingCallNotification,
|
||||
type ReactionStreamEmoji,
|
||||
} from '@calls/types/calls';
|
||||
import {Calls, Screens} from '@constants';
|
||||
import {Calls, General, Screens} from '@constants';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {getChannelById} from '@queries/servers/channel';
|
||||
import {getThreadById} from '@queries/servers/thread';
|
||||
import {getUserById} from '@queries/servers/user';
|
||||
import {isDMorGM} from '@utils/channel';
|
||||
import {logDebug} from '@utils/log';
|
||||
|
||||
import type {CallRecordingState, UserReactionData} from '@mattermost/calls/lib/types';
|
||||
|
||||
export const setCalls = (serverUrl: string, myUserId: string, calls: Dictionary<Call>, enabled: Dictionary<boolean>) => {
|
||||
export const setCalls = async (serverUrl: string, myUserId: string, calls: Dictionary<Call>, enabled: Dictionary<boolean>) => {
|
||||
const channelsWithCalls = Object.keys(calls).reduce(
|
||||
(accum, next) => {
|
||||
accum[next] = true;
|
||||
|
|
@ -45,6 +52,8 @@ export const setCalls = (serverUrl: string, myUserId: string, calls: Dictionary<
|
|||
|
||||
setCallsState(serverUrl, {myUserId, calls, enabled});
|
||||
|
||||
await processIncomingCalls(serverUrl, Object.values(calls), false);
|
||||
|
||||
// Does the current call need to be updated?
|
||||
const currentCall = getCurrentCall();
|
||||
if (!currentCall || !calls[currentCall.channelId]) {
|
||||
|
|
@ -61,6 +70,118 @@ export const setCalls = (serverUrl: string, myUserId: string, calls: Dictionary<
|
|||
setCurrentCall(nextCall);
|
||||
};
|
||||
|
||||
export const processIncomingCalls = async (serverUrl: string, calls: Call[], keepExisting = true) => {
|
||||
if (!getCallsConfig(serverUrl).EnableRinging) {
|
||||
return;
|
||||
}
|
||||
|
||||
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
|
||||
if (!database) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Do we have incoming calls we should notify about?
|
||||
const incomingCalls = getIncomingCalls().incomingCalls;
|
||||
const existingCalls = getCallsState(serverUrl).calls;
|
||||
const myUserId = getCallsState(serverUrl).myUserId;
|
||||
const newIncoming: IncomingCallNotification[] = [];
|
||||
|
||||
for await (const call of calls) {
|
||||
// dismissed already?
|
||||
if (call.dismissed[myUserId] || existingCalls[call.channelId]?.dismissed[myUserId]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// already in our incomingCalls notifications?
|
||||
if (incomingCalls.findIndex((c) => c.callID === call.id) >= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Never send a notification for a call you started, or a call you are currently in.
|
||||
if (myUserId === call.ownerId || getCurrentCall()?.id === call.id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const channel = await getChannelById(database, call.channelId);
|
||||
if (!channel) {
|
||||
logDebug('calls: processIncomingCalls could not find channel by id', call.channelId, 'for serverUrl', serverUrl);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isDMorGM(channel)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const callerModel = await getUserById(database, call.ownerId);
|
||||
|
||||
newIncoming.push({
|
||||
serverUrl,
|
||||
myUserId,
|
||||
callID: call.id,
|
||||
callerID: call.ownerId,
|
||||
callerModel,
|
||||
channelID: call.channelId,
|
||||
startAt: call.startTime,
|
||||
type: channel.type === General.DM_CHANNEL ? ChannelType.DM : ChannelType.GM,
|
||||
});
|
||||
}
|
||||
|
||||
if (newIncoming.length === 0 && keepExisting) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (keepExisting) {
|
||||
newIncoming.push(...incomingCalls);
|
||||
} else {
|
||||
const removedThisServer = incomingCalls.filter((ic) => ic.serverUrl !== serverUrl);
|
||||
newIncoming.push(...removedThisServer);
|
||||
}
|
||||
|
||||
if (newIncoming.length === 0 && incomingCalls.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
newIncoming.sort((a, b) => a.startAt - b.startAt);
|
||||
setIncomingCalls({incomingCalls: newIncoming});
|
||||
};
|
||||
|
||||
const getChannelIdFromCallId = (serverUrl: string, callId: string) => {
|
||||
const callsState = getCallsState(serverUrl);
|
||||
for (const call of Object.values(callsState.calls)) {
|
||||
if (call.id === callId) {
|
||||
return call.channelId;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const removeIncomingCall = (serverUrl: string, callId: string, channelId?: string) => {
|
||||
if (!getCallsConfig(serverUrl).EnableRinging) {
|
||||
return;
|
||||
}
|
||||
|
||||
const incomingCalls = getIncomingCalls();
|
||||
const newIncomingCalls = incomingCalls.incomingCalls.filter((ic) => ic.callID !== callId);
|
||||
if (incomingCalls.incomingCalls.length !== newIncomingCalls.length) {
|
||||
setIncomingCalls({incomingCalls: newIncomingCalls});
|
||||
}
|
||||
|
||||
let chId = channelId;
|
||||
if (!chId) {
|
||||
chId = getChannelIdFromCallId(serverUrl, callId);
|
||||
if (!chId) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const callsState = getCallsState(serverUrl);
|
||||
const nextCalls = {...callsState.calls};
|
||||
if (nextCalls[chId]) {
|
||||
nextCalls[chId].dismissed[callsState.myUserId] = true;
|
||||
}
|
||||
setCallsState(serverUrl, {...callsState, calls: nextCalls});
|
||||
};
|
||||
|
||||
export const setCallForChannel = (serverUrl: string, channelId: string, enabled?: boolean, call?: Call) => {
|
||||
const callsState = getCallsState(serverUrl);
|
||||
let nextEnabled = callsState.enabled;
|
||||
|
|
@ -136,6 +257,10 @@ export const userJoinedCall = (serverUrl: string, channelId: string, userId: str
|
|||
|
||||
setCurrentCall(nextCurrentCall);
|
||||
}
|
||||
|
||||
if (userId === callsState.myUserId) {
|
||||
removeIncomingCall(serverUrl, callsState.calls[channelId].id, channelId);
|
||||
}
|
||||
};
|
||||
|
||||
export const userLeftCall = (serverUrl: string, channelId: string, userId: string) => {
|
||||
|
|
@ -159,6 +284,9 @@ export const userLeftCall = (serverUrl: string, channelId: string, userId: strin
|
|||
if (Object.keys(nextCall.participants).length === 0) {
|
||||
delete nextCalls[channelId];
|
||||
|
||||
const callId = callsState.calls[channelId].id;
|
||||
removeIncomingCall(serverUrl, callId, channelId);
|
||||
|
||||
const channelsWithCalls = getChannelsWithCalls(serverUrl);
|
||||
const nextChannelsWithCalls = {...channelsWithCalls};
|
||||
delete nextChannelsWithCalls[channelId];
|
||||
|
|
@ -230,6 +358,8 @@ export const callStarted = async (serverUrl: string, call: Call) => {
|
|||
nextCalls[call.channelId] = call;
|
||||
setCallsState(serverUrl, {...callsState, calls: nextCalls});
|
||||
|
||||
await processIncomingCalls(serverUrl, [call]);
|
||||
|
||||
const nextChannelsWithCalls = {...getChannelsWithCalls(serverUrl), [call.channelId]: true};
|
||||
setChannelsWithCalls(serverUrl, nextChannelsWithCalls);
|
||||
|
||||
|
|
@ -263,6 +393,7 @@ export const callStarted = async (serverUrl: string, call: Call) => {
|
|||
export const callEnded = (serverUrl: string, channelId: string) => {
|
||||
const callsState = getCallsState(serverUrl);
|
||||
const nextCalls = {...callsState.calls};
|
||||
const callId = nextCalls[channelId]?.id || '';
|
||||
delete nextCalls[channelId];
|
||||
setCallsState(serverUrl, {...callsState, calls: nextCalls});
|
||||
|
||||
|
|
@ -271,6 +402,8 @@ export const callEnded = (serverUrl: string, channelId: string) => {
|
|||
delete nextChannelsWithCalls[channelId];
|
||||
setChannelsWithCalls(serverUrl, nextChannelsWithCalls);
|
||||
|
||||
removeIncomingCall(serverUrl, callId, channelId);
|
||||
|
||||
// currentCall is set to null by the disconnect.
|
||||
};
|
||||
|
||||
|
|
|
|||
37
app/products/calls/state/incoming_calls.ts
Normal file
37
app/products/calls/state/incoming_calls.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {useEffect, useState} from 'react';
|
||||
import {BehaviorSubject} from 'rxjs';
|
||||
|
||||
import {DefaultIncomingCalls, type IncomingCalls} from '@calls/types/calls';
|
||||
|
||||
const incomingCallsSubject = new BehaviorSubject(DefaultIncomingCalls);
|
||||
|
||||
export const getIncomingCalls = () => {
|
||||
return incomingCallsSubject.value;
|
||||
};
|
||||
|
||||
export const setIncomingCalls = (state: IncomingCalls) => {
|
||||
incomingCallsSubject.next(state);
|
||||
};
|
||||
|
||||
export const observeIncomingCalls = () => {
|
||||
return incomingCallsSubject.asObservable();
|
||||
};
|
||||
|
||||
export const useIncomingCalls = () => {
|
||||
const [state, setState] = useState(DefaultIncomingCalls);
|
||||
|
||||
useEffect(() => {
|
||||
const subscription = incomingCallsSubject.subscribe((incomingCalls) => {
|
||||
setState(incomingCalls);
|
||||
});
|
||||
|
||||
return () => {
|
||||
subscription?.unsubscribe();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return state;
|
||||
};
|
||||
|
|
@ -7,3 +7,4 @@ export * from './calls_config';
|
|||
export * from './current_call';
|
||||
export * from './channels_with_calls';
|
||||
export * from './global_calls_state';
|
||||
export * from './incoming_calls';
|
||||
|
|
|
|||
|
|
@ -24,7 +24,32 @@ export const DefaultCallsState: CallsState = {
|
|||
enabled: {},
|
||||
};
|
||||
|
||||
export enum ChannelType {
|
||||
DM,
|
||||
GM
|
||||
}
|
||||
|
||||
export type IncomingCallNotification = {
|
||||
serverUrl: string;
|
||||
myUserId: string;
|
||||
callID: string;
|
||||
channelID: string;
|
||||
callerID: string;
|
||||
callerModel?: UserModel;
|
||||
startAt: number;
|
||||
type: ChannelType;
|
||||
}
|
||||
|
||||
export type IncomingCalls = {
|
||||
incomingCalls: IncomingCallNotification[];
|
||||
}
|
||||
|
||||
export const DefaultIncomingCalls: IncomingCalls = {
|
||||
incomingCalls: [],
|
||||
};
|
||||
|
||||
export type Call = {
|
||||
id: string;
|
||||
participants: Dictionary<CallParticipant>;
|
||||
channelId: string;
|
||||
startTime: number;
|
||||
|
|
@ -33,9 +58,11 @@ export type Call = {
|
|||
ownerId: string;
|
||||
recState?: CallRecordingState;
|
||||
hostId: string;
|
||||
dismissed: Dictionary<boolean>;
|
||||
}
|
||||
|
||||
export const DefaultCall: Call = {
|
||||
id: '',
|
||||
participants: {},
|
||||
channelId: '',
|
||||
startTime: 0,
|
||||
|
|
@ -43,6 +70,7 @@ export const DefaultCall: Call = {
|
|||
threadId: '',
|
||||
ownerId: '',
|
||||
hostId: '',
|
||||
dismissed: {},
|
||||
};
|
||||
|
||||
export enum AudioDevice {
|
||||
|
|
@ -123,6 +151,7 @@ export const DefaultCallsConfig: CallsConfigState = {
|
|||
MaxRecordingDuration: 60,
|
||||
AllowScreenSharing: true,
|
||||
EnableSimulcast: false,
|
||||
EnableRinging: false,
|
||||
};
|
||||
|
||||
export type ApiResp = {
|
||||
|
|
|
|||
|
|
@ -1,7 +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 {makeCallsBaseAndBadgeRGB, rgbToCSS} from '@mattermost/calls';
|
||||
import {Alert} from 'react-native';
|
||||
|
||||
import {Calls, Post} from '@constants';
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ 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';
|
||||
|
|
@ -30,10 +31,10 @@ import type {KeyboardTrackingViewRef} from 'react-native-keyboard-tracking-view'
|
|||
type ChannelProps = {
|
||||
channelId: string;
|
||||
componentId?: AvailableScreens;
|
||||
isCallInCurrentChannel: boolean;
|
||||
showJoinCallBanner: boolean;
|
||||
isInACall: boolean;
|
||||
isInCurrentChannelCall: boolean;
|
||||
isCallsEnabledInChannel: boolean;
|
||||
showIncomingCalls: boolean;
|
||||
isTabletView?: boolean;
|
||||
};
|
||||
|
||||
|
|
@ -49,10 +50,10 @@ const styles = StyleSheet.create({
|
|||
const Channel = ({
|
||||
channelId,
|
||||
componentId,
|
||||
isCallInCurrentChannel,
|
||||
showJoinCallBanner,
|
||||
isInACall,
|
||||
isInCurrentChannelCall,
|
||||
isCallsEnabledInChannel,
|
||||
showIncomingCalls,
|
||||
isTabletView,
|
||||
}: ChannelProps) => {
|
||||
const isTablet = useIsTablet();
|
||||
|
|
@ -98,8 +99,7 @@ const Channel = ({
|
|||
setContainerHeight(e.nativeEvent.layout.height);
|
||||
}, []);
|
||||
|
||||
const showJoinCallBanner = isCallInCurrentChannel && !isInCurrentChannelCall;
|
||||
const renderCallsComponents = showJoinCallBanner || isInACall;
|
||||
const showFloatingCallContainer = showJoinCallBanner || isInACall;
|
||||
|
||||
return (
|
||||
<FreezeScreen>
|
||||
|
|
@ -139,13 +139,20 @@ const Channel = ({
|
|||
/>
|
||||
</>
|
||||
}
|
||||
{renderCallsComponents &&
|
||||
{showFloatingCallContainer &&
|
||||
<FloatingCallContainer
|
||||
channelId={channelId}
|
||||
showJoinCallBanner={showJoinCallBanner}
|
||||
isInACall={isInACall}
|
||||
/>
|
||||
}
|
||||
{showIncomingCalls &&
|
||||
<IncomingCallsContainer
|
||||
channelId={channelId}
|
||||
showingJoinCallBanner={showJoinCallBanner}
|
||||
showingCurrentCallBanner={isInACall}
|
||||
/>
|
||||
}
|
||||
</SafeAreaView>
|
||||
</FreezeScreen>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -6,7 +6,12 @@ import withObservables from '@nozbe/with-observables';
|
|||
import {combineLatest, distinctUntilChanged, of as of$, switchMap} from 'rxjs';
|
||||
|
||||
import {observeIsCallsEnabledInChannel} from '@calls/observers';
|
||||
import {observeChannelsWithCalls, observeCurrentCall} from '@calls/state';
|
||||
import {
|
||||
observeCallsState,
|
||||
observeChannelsWithCalls,
|
||||
observeCurrentCall,
|
||||
observeIncomingCalls,
|
||||
} from '@calls/state';
|
||||
import {withServerUrl} from '@context/server';
|
||||
import {observeCurrentChannelId} from '@queries/servers/system';
|
||||
|
||||
|
|
@ -34,16 +39,28 @@ const enhanced = withObservables([], ({database, serverUrl}: EnhanceProps) => {
|
|||
switchMap((call) => of$(Boolean(call?.connected))),
|
||||
distinctUntilChanged(),
|
||||
);
|
||||
const dismissed = combineLatest([channelId, observeCallsState(serverUrl)]).pipe(
|
||||
switchMap(([id, state]) => of$(Boolean(state.calls[id]?.dismissed[state.myUserId]))),
|
||||
distinctUntilChanged(),
|
||||
);
|
||||
const isInCurrentChannelCall = combineLatest([channelId, ccChannelId]).pipe(
|
||||
switchMap(([id, ccId]) => of$(id === ccId)),
|
||||
distinctUntilChanged(),
|
||||
);
|
||||
const showJoinCallBanner = combineLatest([isCallInCurrentChannel, dismissed, isInCurrentChannelCall]).pipe(
|
||||
switchMap(([isCall, dism, inCurrCall]) => of$(Boolean(isCall && !dism && !inCurrCall))),
|
||||
distinctUntilChanged(),
|
||||
);
|
||||
const showIncomingCalls = observeIncomingCalls().pipe(
|
||||
switchMap((ics) => of$(ics.incomingCalls.length > 0)),
|
||||
distinctUntilChanged(),
|
||||
);
|
||||
|
||||
return {
|
||||
channelId,
|
||||
isCallInCurrentChannel,
|
||||
showJoinCallBanner,
|
||||
isInACall,
|
||||
isInCurrentChannelCall,
|
||||
showIncomingCalls,
|
||||
isCallsEnabledInChannel: observeIsCallsEnabledInChannel(database, serverUrl, channelId),
|
||||
};
|
||||
});
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
|
|||
import withObservables from '@nozbe/with-observables';
|
||||
import {distinctUntilChanged, switchMap, combineLatest, of as of$} from 'rxjs';
|
||||
|
||||
import {observeChannelsWithCalls, observeCurrentCall} from '@calls/state';
|
||||
import {observeCallsState, observeChannelsWithCalls, observeCurrentCall, observeIncomingCalls} from '@calls/state';
|
||||
import {withServerUrl} from '@context/server';
|
||||
import {observePost} from '@queries/servers/post';
|
||||
import {observeIsCRTEnabled} from '@queries/servers/thread';
|
||||
|
|
@ -41,16 +41,28 @@ const enhanced = withObservables(['rootId'], ({database, serverUrl, rootId}: Enh
|
|||
switchMap((call) => of$(Boolean(call?.connected))),
|
||||
distinctUntilChanged(),
|
||||
);
|
||||
const dismissed = combineLatest([channelId, observeCallsState(serverUrl)]).pipe(
|
||||
switchMap(([id, state]) => of$(Boolean(state.calls[id]?.dismissed[state.myUserId]))),
|
||||
distinctUntilChanged(),
|
||||
);
|
||||
const isInCurrentChannelCall = combineLatest([channelId, ccChannelId]).pipe(
|
||||
switchMap(([id, ccId]) => of$(id === ccId)),
|
||||
distinctUntilChanged(),
|
||||
);
|
||||
const showJoinCallBanner = combineLatest([isCallInCurrentChannel, dismissed, isInCurrentChannelCall]).pipe(
|
||||
switchMap(([isCall, dism, inCurrCall]) => of$(Boolean(isCall && !dism && !inCurrCall))),
|
||||
distinctUntilChanged(),
|
||||
);
|
||||
const showIncomingCalls = observeIncomingCalls().pipe(
|
||||
switchMap((ics) => of$(ics.incomingCalls.length > 0)),
|
||||
distinctUntilChanged(),
|
||||
);
|
||||
|
||||
return {
|
||||
isCRTEnabled: observeIsCRTEnabled(database),
|
||||
isCallInCurrentChannel,
|
||||
showJoinCallBanner,
|
||||
isInACall,
|
||||
isInCurrentChannelCall,
|
||||
showIncomingCalls,
|
||||
rootId: of$(rId),
|
||||
rootPost,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ 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';
|
||||
|
|
@ -30,9 +31,9 @@ import type {KeyboardTrackingViewRef} from 'react-native-keyboard-tracking-view'
|
|||
type ThreadProps = {
|
||||
componentId: AvailableScreens;
|
||||
isCRTEnabled: boolean;
|
||||
isCallInCurrentChannel: boolean;
|
||||
showJoinCallBanner: boolean;
|
||||
isInACall: boolean;
|
||||
isInCurrentChannelCall: boolean;
|
||||
showIncomingCalls: boolean;
|
||||
rootId: string;
|
||||
rootPost?: PostModel;
|
||||
};
|
||||
|
|
@ -49,9 +50,9 @@ const Thread = ({
|
|||
isCRTEnabled,
|
||||
rootId,
|
||||
rootPost,
|
||||
isCallInCurrentChannel,
|
||||
showJoinCallBanner,
|
||||
isInACall,
|
||||
isInCurrentChannelCall,
|
||||
showIncomingCalls,
|
||||
}: ThreadProps) => {
|
||||
const postDraftRef = useRef<KeyboardTrackingViewRef>(null);
|
||||
const [containerHeight, setContainerHeight] = useState(0);
|
||||
|
|
@ -110,8 +111,7 @@ const Thread = ({
|
|||
setContainerHeight(e.nativeEvent.layout.height);
|
||||
}, []);
|
||||
|
||||
const showJoinCallBanner = isCallInCurrentChannel && !isInCurrentChannelCall;
|
||||
const renderCallsComponents = showJoinCallBanner || isInACall;
|
||||
const showFloatingCallContainer = showJoinCallBanner || isInACall;
|
||||
|
||||
return (
|
||||
<FreezeScreen>
|
||||
|
|
@ -144,7 +144,7 @@ const Thread = ({
|
|||
/>
|
||||
</>
|
||||
}
|
||||
{renderCallsComponents &&
|
||||
{showFloatingCallContainer &&
|
||||
<FloatingCallContainer
|
||||
channelId={rootPost!.channelId}
|
||||
showJoinCallBanner={showJoinCallBanner}
|
||||
|
|
@ -152,6 +152,14 @@ const Thread = ({
|
|||
threadScreen={true}
|
||||
/>
|
||||
}
|
||||
{showIncomingCalls &&
|
||||
<IncomingCallsContainer
|
||||
channelId={rootPost!.channelId}
|
||||
showingJoinCallBanner={showJoinCallBanner}
|
||||
showingCurrentCallBanner={isInACall}
|
||||
threadScreen={true}
|
||||
/>
|
||||
}
|
||||
</SafeAreaView>
|
||||
</FreezeScreen>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -441,6 +441,9 @@
|
|||
"mobile.calls_host_rec_stopped": "You can find the recording in this call's chat thread once it's finished processing.",
|
||||
"mobile.calls_host_rec_stopped_title": "Recording has stopped. Processing...",
|
||||
"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",
|
||||
|
|
|
|||
8
package-lock.json
generated
8
package-lock.json
generated
|
|
@ -17,7 +17,7 @@
|
|||
"@formatjs/intl-pluralrules": "5.2.4",
|
||||
"@formatjs/intl-relativetimeformat": "11.2.4",
|
||||
"@gorhom/bottom-sheet": "4.4.7",
|
||||
"@mattermost/calls": "github:mattermost/calls-common#v0.14.0",
|
||||
"@mattermost/calls": "github:mattermost/calls-common#v0.17.0",
|
||||
"@mattermost/compass-icons": "0.1.37",
|
||||
"@mattermost/react-native-emm": "1.3.5",
|
||||
"@mattermost/react-native-network-client": "1.3.8",
|
||||
|
|
@ -3402,7 +3402,7 @@
|
|||
"node_modules/@mattermost/calls": {
|
||||
"name": "@calls/common",
|
||||
"version": "0.14.0",
|
||||
"resolved": "git+ssh://git@github.com/mattermost/calls-common.git#d539eb36f9549075b0a0a6578174db31297c45ae"
|
||||
"resolved": "git+ssh://git@github.com/mattermost/calls-common.git#43011d8e9c7985b0542628329be098715a43dc03"
|
||||
},
|
||||
"node_modules/@mattermost/commonmark": {
|
||||
"version": "0.30.1-0",
|
||||
|
|
@ -25390,8 +25390,8 @@
|
|||
}
|
||||
},
|
||||
"@mattermost/calls": {
|
||||
"version": "git+ssh://git@github.com/mattermost/calls-common.git#d539eb36f9549075b0a0a6578174db31297c45ae",
|
||||
"from": "@mattermost/calls@github:mattermost/calls-common#v0.14.0"
|
||||
"version": "git+ssh://git@github.com/mattermost/calls-common.git#43011d8e9c7985b0542628329be098715a43dc03",
|
||||
"from": "@mattermost/calls@github:mattermost/calls-common#v0.17.0"
|
||||
},
|
||||
"@mattermost/commonmark": {
|
||||
"version": "0.30.1-0",
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
"@formatjs/intl-pluralrules": "5.2.4",
|
||||
"@formatjs/intl-relativetimeformat": "11.2.4",
|
||||
"@gorhom/bottom-sheet": "4.4.7",
|
||||
"@mattermost/calls": "github:mattermost/calls-common#v0.14.0",
|
||||
"@mattermost/calls": "github:mattermost/calls-common#v0.17.0",
|
||||
"@mattermost/compass-icons": "0.1.37",
|
||||
"@mattermost/react-native-emm": "1.3.5",
|
||||
"@mattermost/react-native-network-client": "1.3.8",
|
||||
|
|
|
|||
Loading…
Reference in a new issue