mattermost-mobile/app/products/calls/state/actions.ts
Christopher Poile eae0a15b16
MM-48753 - Calls: GA requirements (#6814)
* calls GA requirements

* clarifying comments; simplify ephemeral message (move it to the server)
2022-12-02 11:34:37 -05:00

544 lines
18 KiB
TypeScript

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {
getCallsConfig,
getCallsState,
getChannelsWithCalls,
getCurrentCall,
getGlobalCallsState,
setCallsConfig,
setCallsState,
setChannelsWithCalls,
setCurrentCall,
setGlobalCallsState,
} from '@calls/state';
import {
Call,
CallReaction,
CallsConfig,
ChannelsWithCalls,
CurrentCall,
DefaultCall,
DefaultCurrentCall,
ReactionStreamEmoji,
RecordingState,
} from '@calls/types/calls';
import {REACTION_LIMIT, REACTION_TIMEOUT} from '@constants/calls';
export const setCalls = (serverUrl: string, myUserId: string, calls: Dictionary<Call>, enabled: Dictionary<boolean>) => {
const channelsWithCalls = Object.keys(calls).reduce(
(accum, next) => {
accum[next] = true;
return accum;
}, {} as ChannelsWithCalls);
setChannelsWithCalls(serverUrl, channelsWithCalls);
setCallsState(serverUrl, {serverUrl, myUserId, calls, enabled});
// Does the current call need to be updated?
const currentCall = getCurrentCall();
if (!currentCall || !calls[currentCall.channelId]) {
return;
}
// Edge case: if the app went into the background and lost the main ws connection, we don't know who is currently
// talking. Instead of guessing, erase voiceOn state (same state as when joining an ongoing call).
const nextCall = {
...currentCall,
...calls[currentCall.channelId],
voiceOn: {},
};
setCurrentCall(nextCall);
};
export const setCallForChannel = (serverUrl: string, channelId: string, enabled?: boolean, call?: Call) => {
const callsState = getCallsState(serverUrl);
let nextEnabled = callsState.enabled;
if (typeof enabled !== 'undefined') {
nextEnabled = {...callsState.enabled, [channelId]: enabled};
}
const nextCalls = {...callsState.calls};
if (call) {
nextCalls[channelId] = call;
// In case we got a complete update on the currentCall
const currentCall = getCurrentCall();
if (currentCall?.channelId === channelId) {
setCurrentCall({
...currentCall,
...call,
});
}
} else {
delete nextCalls[channelId];
}
setCallsState(serverUrl, {...callsState, calls: nextCalls, enabled: nextEnabled});
const channelsWithCalls = getChannelsWithCalls(serverUrl);
if (call && !channelsWithCalls[channelId]) {
const nextChannelsWithCalls = {...channelsWithCalls};
nextChannelsWithCalls[channelId] = true;
setChannelsWithCalls(serverUrl, nextChannelsWithCalls);
} else if (!call && channelsWithCalls[channelId]) {
const nextChannelsWithCalls = {...channelsWithCalls};
delete nextChannelsWithCalls[channelId];
setChannelsWithCalls(serverUrl, nextChannelsWithCalls);
}
};
export const userJoinedCall = (serverUrl: string, channelId: string, userId: string) => {
const callsState = getCallsState(serverUrl);
if (!callsState.calls[channelId]) {
return;
}
const nextCall = {
...callsState.calls[channelId],
participants: {...callsState.calls[channelId].participants},
};
nextCall.participants[userId] = {
id: userId,
muted: true,
raisedHand: 0,
};
const nextCalls = {...callsState.calls, [channelId]: nextCall};
setCallsState(serverUrl, {...callsState, calls: nextCalls});
// Did the user join the current call? If so, update that too.
const currentCall = getCurrentCall();
if (currentCall && currentCall.channelId === channelId) {
const voiceOn = {...currentCall.voiceOn};
delete voiceOn[userId];
const nextCurrentCall = {
...currentCall,
participants: {...currentCall.participants, [userId]: nextCall.participants[userId]},
voiceOn,
};
// If this is the currentUser, that means we've connected to the call we created.
if (userId === nextCurrentCall.myUserId) {
nextCurrentCall.connected = true;
}
setCurrentCall(nextCurrentCall);
}
};
export const userLeftCall = (serverUrl: string, channelId: string, userId: string) => {
const callsState = getCallsState(serverUrl);
if (!callsState.calls[channelId]?.participants[userId]) {
return;
}
const nextCall = {
...callsState.calls[channelId],
participants: {...callsState.calls[channelId].participants},
};
delete nextCall.participants[userId];
const nextCalls = {...callsState.calls};
if (Object.keys(nextCall.participants).length === 0) {
delete nextCalls[channelId];
const channelsWithCalls = getChannelsWithCalls(serverUrl);
const nextChannelsWithCalls = {...channelsWithCalls};
delete nextChannelsWithCalls[channelId];
setChannelsWithCalls(serverUrl, nextChannelsWithCalls);
} else {
nextCalls[channelId] = nextCall;
}
setCallsState(serverUrl, {...callsState, calls: nextCalls});
// Did the user leave the current call? If so, update that too.
const currentCall = getCurrentCall();
if (!currentCall || currentCall.channelId !== channelId) {
return;
}
// Was the user me?
if (userId === callsState.myUserId) {
myselfLeftCall();
return;
}
// Clear them from the voice list
const voiceOn = {...currentCall.voiceOn};
delete voiceOn[userId];
const nextCurrentCall = {
...currentCall,
participants: {...currentCall.participants},
voiceOn,
};
delete nextCurrentCall.participants[userId];
setCurrentCall(nextCurrentCall);
};
export const newCurrentCall = (serverUrl: string, channelId: string, myUserId: string) => {
let existingCall: Call = DefaultCall;
const callsState = getCallsState(serverUrl);
if (callsState.calls[channelId]) {
existingCall = callsState.calls[channelId];
}
setCurrentCall({
...DefaultCurrentCall,
...existingCall,
serverUrl,
channelId,
myUserId,
});
};
export const myselfLeftCall = () => {
setCurrentCall(null);
};
export const callStarted = (serverUrl: string, call: Call) => {
const callsState = getCallsState(serverUrl);
const nextCalls = {...callsState.calls};
nextCalls[call.channelId] = call;
setCallsState(serverUrl, {...callsState, calls: nextCalls});
const nextChannelsWithCalls = {...getChannelsWithCalls(serverUrl), [call.channelId]: true};
setChannelsWithCalls(serverUrl, nextChannelsWithCalls);
// If we started a call, we will get a callStarted event with the 'official' data from the server.
// Save that in our currentCall.
const currentCall = getCurrentCall();
if (!currentCall || currentCall.channelId !== call.channelId) {
return;
}
const nextCurrentCall: CurrentCall = {
...currentCall,
...call,
};
setCurrentCall(nextCurrentCall);
};
export const callEnded = (serverUrl: string, channelId: string) => {
const callsState = getCallsState(serverUrl);
const nextCalls = {...callsState.calls};
delete nextCalls[channelId];
setCallsState(serverUrl, {...callsState, calls: nextCalls});
const channelsWithCalls = getChannelsWithCalls(serverUrl);
const nextChannelsWithCalls = {...channelsWithCalls};
delete nextChannelsWithCalls[channelId];
setChannelsWithCalls(serverUrl, nextChannelsWithCalls);
// currentCall is set to null by the disconnect.
};
export const setUserMuted = (serverUrl: string, channelId: string, userId: string, muted: boolean) => {
const callsState = getCallsState(serverUrl);
if (!callsState.calls[channelId] || !callsState.calls[channelId].participants[userId]) {
return;
}
const nextUser = {...callsState.calls[channelId].participants[userId], muted};
const nextCall = {
...callsState.calls[channelId],
participants: {...callsState.calls[channelId].participants},
};
nextCall.participants[userId] = nextUser;
const nextCalls = {...callsState.calls};
nextCalls[channelId] = nextCall;
setCallsState(serverUrl, {...callsState, calls: nextCalls});
// Was it the current call? If so, update that too.
const currentCall = getCurrentCall();
if (!currentCall || currentCall.channelId !== channelId) {
return;
}
const nextCurrentCall = {
...currentCall,
participants: {
...currentCall.participants,
[userId]: {...currentCall.participants[userId], muted},
},
};
setCurrentCall(nextCurrentCall);
};
export const setUserVoiceOn = (channelId: string, userId: string, voiceOn: boolean) => {
const currentCall = getCurrentCall();
if (!currentCall || currentCall.channelId !== channelId) {
return;
}
const nextVoiceOn = {...currentCall.voiceOn};
if (voiceOn) {
nextVoiceOn[userId] = true;
} else {
delete nextVoiceOn[userId];
}
const nextCurrentCall = {
...currentCall,
voiceOn: nextVoiceOn,
};
setCurrentCall(nextCurrentCall);
};
export const setRaisedHand = (serverUrl: string, channelId: string, userId: string, timestamp: number) => {
const callsState = getCallsState(serverUrl);
if (!callsState.calls[channelId] || !callsState.calls[channelId].participants[userId]) {
return;
}
const nextUser = {...callsState.calls[channelId].participants[userId], raisedHand: timestamp};
const nextCall = {
...callsState.calls[channelId],
participants: {...callsState.calls[channelId].participants},
};
nextCall.participants[userId] = nextUser;
const nextCalls = {...callsState.calls};
nextCalls[channelId] = nextCall;
setCallsState(serverUrl, {...callsState, calls: nextCalls});
// Was it the current call? If so, update that too.
const currentCall = getCurrentCall();
if (!currentCall || currentCall.channelId !== channelId) {
return;
}
const nextCurrentCall = {
...currentCall,
participants: {
...currentCall.participants,
[userId]: {...currentCall.participants[userId], raisedHand: timestamp},
},
};
setCurrentCall(nextCurrentCall);
};
export const setCallScreenOn = (serverUrl: string, channelId: string, userId: string) => {
const callsState = getCallsState(serverUrl);
if (!callsState.calls[channelId] || !callsState.calls[channelId].participants[userId]) {
return;
}
const nextCall = {...callsState.calls[channelId], screenOn: userId};
const nextCalls = {...callsState.calls};
nextCalls[channelId] = nextCall;
setCallsState(serverUrl, {...callsState, calls: nextCalls});
// Was it the current call? If so, update that too.
const currentCall = getCurrentCall();
if (!currentCall || currentCall.channelId !== channelId) {
return;
}
const nextCurrentCall = {
...currentCall,
screenOn: userId,
};
setCurrentCall(nextCurrentCall);
};
export const setCallScreenOff = (serverUrl: string, channelId: string) => {
const callsState = getCallsState(serverUrl);
if (!callsState.calls[channelId]) {
return;
}
const nextCall = {...callsState.calls[channelId], screenOn: ''};
const nextCalls = {...callsState.calls};
nextCalls[channelId] = nextCall;
setCallsState(serverUrl, {...callsState, calls: nextCalls});
// Was it the current call? If so, update that too.
const currentCall = getCurrentCall();
if (!currentCall || currentCall.channelId !== channelId) {
return;
}
const nextCurrentCall = {
...currentCall,
screenOn: '',
};
setCurrentCall(nextCurrentCall);
};
export const setChannelEnabled = (serverUrl: string, channelId: string, enabled: boolean) => {
const callsState = getCallsState(serverUrl);
const nextEnabled = {...callsState.enabled};
nextEnabled[channelId] = enabled;
setCallsState(serverUrl, {...callsState, enabled: nextEnabled});
};
export const setScreenShareURL = (url: string) => {
const call = getCurrentCall();
if (call) {
setCurrentCall({...call, screenShareURL: url});
}
};
export const setSpeakerPhone = (speakerphoneOn: boolean) => {
const call = getCurrentCall();
if (call) {
setCurrentCall({...call, speakerphoneOn});
}
};
export const setConfig = (serverUrl: string, config: Partial<CallsConfig>) => {
const callsConfig = getCallsConfig(serverUrl);
setCallsConfig(serverUrl, {...callsConfig, ...config});
};
export const setPluginEnabled = (serverUrl: string, pluginEnabled: boolean) => {
const callsConfig = getCallsConfig(serverUrl);
setCallsConfig(serverUrl, {...callsConfig, pluginEnabled});
};
export const setMicPermissionsGranted = (granted: boolean) => {
const globalState = getGlobalCallsState();
const nextGlobalState = {
...globalState,
micPermissionsGranted: granted,
};
setGlobalCallsState(nextGlobalState);
};
export const setMicPermissionsErrorDismissed = () => {
const currentCall = getCurrentCall();
if (!currentCall) {
return;
}
const nextCurrentCall = {
...currentCall,
micPermissionsErrorDismissed: true,
};
setCurrentCall(nextCurrentCall);
};
export const userReacted = (serverUrl: string, channelId: string, reaction: CallReaction) => {
// Note: Simplification for performance:
// If you are not in the call with the reaction, ignore it. There could be many calls ongoing in your
// servers, do we want to be tracking reactions and setting timeouts for all those calls? No.
// The downside of this approach: when you join/rejoin a call, you will not see the current reactions.
// When you leave a call, you will lose the reactions you were tracking.
// We can revisit this if it causes UX issues.
const currentCall = getCurrentCall();
if (currentCall?.channelId !== channelId) {
return;
}
// Update the reaction stream.
const newReactionStream = [...currentCall.reactionStream];
const idx = newReactionStream.findIndex((e) => e.name === reaction.emoji.name);
if (idx > -1) {
const [newReaction] = newReactionStream.splice(idx, 1);
newReaction.count += 1;
newReaction.latestTimestamp = reaction.timestamp;
newReactionStream.splice(0, 0, newReaction);
} else {
const newReaction: ReactionStreamEmoji = {
name: reaction.emoji.name,
count: 1,
latestTimestamp: reaction.timestamp,
};
newReactionStream.splice(0, 0, newReaction);
}
if (newReactionStream.length > REACTION_LIMIT) {
newReactionStream.pop();
}
// Update the participant.
const nextParticipants = {...currentCall.participants};
if (nextParticipants[reaction.user_id]) {
const nextUser = {...nextParticipants[reaction.user_id], reaction};
nextParticipants[reaction.user_id] = nextUser;
}
const nextCurrentCall: CurrentCall = {
...currentCall,
reactionStream: newReactionStream,
participants: nextParticipants,
};
setCurrentCall(nextCurrentCall);
setTimeout(() => {
userReactionTimeout(serverUrl, channelId, reaction);
}, REACTION_TIMEOUT);
};
const userReactionTimeout = (serverUrl: string, channelId: string, reaction: CallReaction) => {
const currentCall = getCurrentCall();
if (currentCall?.channelId !== channelId) {
return;
}
// Remove the reaction only if it was the last time that emoji was used.
const newReactions = currentCall.reactionStream.filter((e) => e.latestTimestamp !== reaction.timestamp);
const nextParticipants = {...currentCall.participants};
if (nextParticipants[reaction.user_id] && nextParticipants[reaction.user_id].reaction?.timestamp === reaction.timestamp) {
const nextUser = {...nextParticipants[reaction.user_id]};
delete nextUser.reaction;
nextParticipants[reaction.user_id] = nextUser;
}
const nextCurrentCall: CurrentCall = {
...currentCall,
reactionStream: newReactions,
participants: nextParticipants,
};
setCurrentCall(nextCurrentCall);
};
export const setRecordingState = (serverUrl: string, channelId: string, recState: RecordingState) => {
const callsState = getCallsState(serverUrl);
if (!callsState.calls[channelId]) {
return;
}
const nextCall = {...callsState.calls[channelId], recState};
const nextCalls = {...callsState.calls, [channelId]: nextCall};
setCallsState(serverUrl, {...callsState, calls: nextCalls});
// Was it the current call? If so, update that too.
const currentCall = getCurrentCall();
if (!currentCall || currentCall.channelId !== channelId) {
return;
}
const nextCurrentCall = {
...currentCall,
recState,
};
setCurrentCall(nextCurrentCall);
};
export const setHost = (serverUrl: string, channelId: string, hostId: string) => {
const callsState = getCallsState(serverUrl);
if (!callsState.calls[channelId]) {
return;
}
const nextCall = {...callsState.calls[channelId], hostId};
const nextCalls = {...callsState.calls, [channelId]: nextCall};
setCallsState(serverUrl, {...callsState, calls: nextCalls});
// Was it the current call? If so, update that too.
const currentCall = getCurrentCall();
if (!currentCall || currentCall.channelId !== channelId) {
return;
}
const nextCurrentCall = {
...currentCall,
hostId,
};
setCurrentCall(nextCurrentCall);
};