MM-48753 - Calls: GA requirements (#6814)

* calls GA requirements

* clarifying comments; simplify ephemeral message (move it to the server)
This commit is contained in:
Christopher Poile
2022-12-02 11:34:37 -05:00
committed by GitHub
parent dc4d61972f
commit eae0a15b16
13 changed files with 159 additions and 95 deletions

View File

@@ -9,6 +9,7 @@ import {DeviceEventEmitter} from 'react-native';
import {addChannelToDefaultCategory, storeCategories} from '@actions/local/category';
import {removeCurrentUserFromChannel, setChannelDeleteAt, storeMyChannelsForTeam, switchToChannel} from '@actions/local/channel';
import {switchToGlobalThreads} from '@actions/local/thread';
import {loadCallForChannel} from '@calls/actions/calls';
import {Events, General, Preferences, Screens} from '@constants';
import DatabaseManager from '@database/manager';
import {privateChannelJoinPrompt} from '@helpers/api/channel';
@@ -598,6 +599,7 @@ export async function joinChannel(serverUrl: string, teamId: string, channelId?:
}
if (channelId || channel?.id) {
loadCallForChannel(serverUrl, channelId || channel!.id);
EphemeralStore.removeJoiningChannel(channelId || channel!.id);
}
return {channel, member};

View File

@@ -104,7 +104,10 @@ export const loadCalls = async (serverUrl: string, userId: string) => {
if (channel.call) {
callsResults[channel.channel_id] = createCallAndAddToIds(channel.channel_id, channel.call, ids);
}
enabledChannels[channel.channel_id] = channel.enabled;
if (typeof channel.enabled !== 'undefined') {
enabledChannels[channel.channel_id] = channel.enabled;
}
}
// Batch load user models async because we'll need them later

View File

@@ -5,7 +5,7 @@ import {Alert} from 'react-native';
import {Navigation} from 'react-native-navigation';
import {hasMicrophonePermission, joinCall, leaveCall, unmuteMyself} from '@calls/actions';
import {setMicPermissionsGranted} from '@calls/state';
import {getCallsConfig, getCallsState, setMicPermissionsGranted} from '@calls/state';
import {errorAlert} from '@calls/utils';
import {Screens} from '@constants';
import DatabaseManager from '@database/manager';
@@ -13,6 +13,7 @@ import {getCurrentUser} from '@queries/servers/user';
import {dismissAllModals, dismissAllModalsAndPopToScreen} from '@screens/navigation';
import NavigationStore from '@store/navigation_store';
import {logError} from '@utils/log';
import {isSystemAdmin} from '@utils/user';
import type {IntlShape} from 'react-intl';
@@ -91,17 +92,17 @@ export const leaveAndJoinWithAlert = (
id: 'mobile.leave_and_join_confirmation',
defaultMessage: 'Leave & Join',
}),
onPress: () => doJoinCall(serverUrl, channelId, isDMorGM, intl),
onPress: () => doJoinCall(serverUrl, channelId, isDMorGM, newCall, intl),
style: 'cancel',
},
],
);
} else {
doJoinCall(serverUrl, channelId, isDMorGM, intl);
doJoinCall(serverUrl, channelId, isDMorGM, newCall, intl);
}
};
const doJoinCall = async (serverUrl: string, channelId: string, isDMorGM: boolean, intl: IntlShape) => {
const doJoinCall = async (serverUrl: string, channelId: string, isDMorGM: boolean, newCall: boolean, intl: IntlShape) => {
const {formatMessage} = intl;
let user;
@@ -113,6 +114,30 @@ const doJoinCall = async (serverUrl: string, channelId: string, isDMorGM: boolea
// This shouldn't happen, so don't bother localizing and displaying an alert.
return;
}
if (newCall) {
const enabled = getCallsState(serverUrl).enabled[channelId];
const {DefaultEnabled} = getCallsConfig(serverUrl);
const isAdmin = isSystemAdmin(user.roles);
// if explicitly disabled, we wouldn't get to this point.
// if pre-GA calls:
// if enabled is false, then this channel was returned as enabled=false from the server (it was either
// explicitly disabled, or DefaultEnabled=false), and the StartCall button would not be shown
// if enabled is true, then this channel was return as enabled=true from the server (it was either
// explicitly enabled, or DefaultEnabled=true), everyone can start
// if GA calls:
// if explicitly enabled, everyone can start a call
// if !explicitly enabled and defaultEnabled, everyone can start
// if !explicitly enabled and !defaultEnabled, system admins can start, regular users get alert
// Note: the below is a 'badly' coded if. But it's clear, which trumps.
if (enabled || (!enabled && DefaultEnabled) || (!enabled && !DefaultEnabled && isAdmin)) {
// continue through and start the call
} else {
contactAdminAlert(intl);
return;
}
}
} catch (error) {
logError('failed to getServerDatabaseAndOperator in doJoinCall', error);
return;
@@ -139,6 +164,25 @@ const doJoinCall = async (serverUrl: string, channelId: string, isDMorGM: boolea
}
};
const contactAdminAlert = ({formatMessage}: IntlShape) => {
Alert.alert(
formatMessage({
id: 'mobile.calls_request_title',
defaultMessage: 'Calls is not currently enabled',
}),
formatMessage({
id: 'mobile.calls_request_message',
defaultMessage: 'Calls are currently running in test mode and only system admins can start them. Reach out directly to your system admin for assistance',
}),
[{
text: formatMessage({
id: 'mobile.calls_okay',
defaultMessage: 'Okay',
}),
}],
);
};
export const recordingAlert = (isHost: boolean, intl: IntlShape) => {
if (recordingAlertLock) {
return;

View File

@@ -1,38 +1,41 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Database} from '@nozbe/watermelondb';
import {distinctUntilChanged, switchMap, combineLatest, Observable, of as of$} from 'rxjs';
import {observeCallsConfig, observeCallsState} from '@calls/state';
import {General, License} from '@constants';
import {observeChannel} from '@queries/servers/channel';
import {observeLicense} from '@queries/servers/system';
import {observeConfigValue} from '@queries/servers/system';
import {isMinimumServerVersion} from '@utils/helpers';
export const observeIsCallsFeatureRestricted = (database: Database, serverUrl: string, channelId: string) => {
const isCloud = observeLicense(database).pipe(
switchMap((l) => of$(l?.Cloud === 'true')),
distinctUntilChanged(),
);
const skuShortName = observeCallsConfig(serverUrl).pipe(
switchMap((c) => of$(c.sku_short_name)),
distinctUntilChanged(),
);
const isDMChannel = observeChannel(database, channelId).pipe(
switchMap((c) => of$(c?.type === General.DM_CHANNEL)),
distinctUntilChanged(),
);
return combineLatest([isCloud, skuShortName, isDMChannel]).pipe(
switchMap(([cloud, sku, dm]) => of$(cloud && sku === License.SKU_SHORT_NAME.Starter && !dm)), // are you restricted from making a call because of your subscription?
distinctUntilChanged(),
) as Observable<boolean>;
};
import type {Database} from '@nozbe/watermelondb';
export type LimitRestrictedInfo = {
limitRestricted: boolean;
maxParticipants: number;
}
export const observeIsCallsEnabledInChannel = (database: Database, serverUrl: string, channelId: Observable<string>) => {
const callsDefaultEnabled = observeCallsConfig(serverUrl).pipe(
switchMap((config) => of$(config.DefaultEnabled)),
distinctUntilChanged(),
);
const callsStateEnabledDict = observeCallsState(serverUrl).pipe(
switchMap((state) => of$(state.enabled)),
distinctUntilChanged(), // Did the enabled object ref change? If so, a channel's enabled state has changed.
);
const callsGAServer = observeConfigValue(database, 'Version').pipe(
switchMap((v) => of$(isMinimumServerVersion(v || '', 7, 6))),
);
return combineLatest([channelId, callsStateEnabledDict, callsDefaultEnabled, callsGAServer]).pipe(
switchMap(([id, enabled, defaultEnabled, gaServer]) => {
const explicitlyEnabled = enabled.hasOwnProperty(id as string) && enabled[id];
const explicitlyDisabled = enabled.hasOwnProperty(id as string) && !enabled[id];
return of$(explicitlyEnabled || (!explicitlyDisabled && defaultEnabled) || (!explicitlyDisabled && gaServer));
}),
distinctUntilChanged(),
) as Observable<boolean>;
};
export const observeIsCallLimitRestricted = (serverUrl: string, channelId: string) => {
const maxParticipants = observeCallsConfig(serverUrl).pipe(
switchMap((c) => of$(c.MaxCallParticipants)),

View File

@@ -812,6 +812,7 @@ describe('useCallsState', () => {
sku_short_name: License.SKU_SHORT_NAME.Professional,
MaxCallParticipants: 8,
EnableRecordings: true,
bot_user_id: '',
};
// setup

View File

@@ -52,9 +52,12 @@ export const setCalls = (serverUrl: string, myUserId: string, calls: Dictionary<
setCurrentCall(nextCall);
};
export const setCallForChannel = (serverUrl: string, channelId: string, enabled: boolean, call?: Call) => {
export const setCallForChannel = (serverUrl: string, channelId: string, enabled?: boolean, call?: Call) => {
const callsState = getCallsState(serverUrl);
const nextEnabled = {...callsState.enabled, [channelId]: enabled};
let nextEnabled = callsState.enabled;
if (typeof enabled !== 'undefined') {
nextEnabled = {...callsState.enabled, [channelId]: enabled};
}
const nextCalls = {...callsState.calls};
if (call) {

View File

@@ -84,7 +84,7 @@ export type ChannelsWithCalls = Dictionary<boolean>;
export type ServerChannelState = {
channel_id: string;
enabled: boolean;
enabled?: boolean;
call?: ServerCallState;
}

View File

@@ -142,7 +142,6 @@ const Channel = ({
onLayout={onLayout}
>
<ChannelHeader
serverUrl={serverUrl}
channelId={channelId}
componentId={componentId}
callsEnabledInChannel={isCallsEnabledInChannel}

View File

@@ -39,7 +39,6 @@ type ChannelProps = {
searchTerm: string;
teamId: string;
callsEnabledInChannel: boolean;
callsFeatureRestricted: boolean;
};
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
@@ -67,14 +66,17 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
const ChannelHeader = ({
channelId, channelType, componentId, customStatus, displayName,
isCustomStatusEnabled, isCustomStatusExpired, isOwnDirectMessage, memberCount,
searchTerm, teamId, callsEnabledInChannel, callsFeatureRestricted,
searchTerm, teamId, callsEnabledInChannel,
}: ChannelProps) => {
const intl = useIntl();
const isTablet = useIsTablet();
const theme = useTheme();
const styles = getStyleSheet(theme);
const defaultHeight = useDefaultHeaderHeight();
const callsAvailable = callsEnabledInChannel && !callsFeatureRestricted;
// NOTE: callsEnabledInChannel will be true/false (not undefined) based on explicit state + the DefaultEnabled system setting
// which ultimately comes from channel/index.tsx, and observeIsCallsEnabledInChannel
const callsAvailable = callsEnabledInChannel;
const isDMorGM = isTypeDMorGM(channelType);
const contextStyle = useMemo(() => ({

View File

@@ -6,7 +6,6 @@ import withObservables from '@nozbe/with-observables';
import {of as of$} from 'rxjs';
import {combineLatestWith, switchMap} from 'rxjs/operators';
import {observeIsCallsFeatureRestricted} from '@calls/observers';
import {General} from '@constants';
import {observeChannel, observeChannelInfo} from '@queries/servers/channel';
import {observeConfigBooleanValue, observeCurrentTeamId, observeCurrentUserId} from '@queries/servers/system';
@@ -22,11 +21,10 @@ import ChannelHeader from './header';
import type {WithDatabaseArgs} from '@typings/database/database';
type OwnProps = {
serverUrl: string;
channelId: string;
};
const enhanced = withObservables(['channelId'], ({serverUrl, channelId, database}: OwnProps & WithDatabaseArgs) => {
const enhanced = withObservables(['channelId'], ({channelId, database}: OwnProps & WithDatabaseArgs) => {
const currentUserId = observeCurrentUserId(database);
const teamId = observeCurrentTeamId(database);
@@ -90,7 +88,6 @@ const enhanced = withObservables(['channelId'], ({serverUrl, channelId, database
memberCount,
searchTerm,
teamId,
callsFeatureRestricted: observeIsCallsFeatureRestricted(database, serverUrl, channelId),
};
});

View File

@@ -6,7 +6,8 @@ import withObservables from '@nozbe/with-observables';
import {combineLatest, of as of$} from 'rxjs';
import {distinctUntilChanged, switchMap} from 'rxjs/operators';
import {observeCallsConfig, observeCallsState, observeChannelsWithCalls, observeCurrentCall} from '@calls/state';
import {observeIsCallsEnabledInChannel} from '@calls/observers';
import {observeChannelsWithCalls, observeCurrentCall} from '@calls/state';
import {withServerUrl} from '@context/server';
import {observeCurrentChannelId} from '@queries/servers/system';
@@ -37,22 +38,7 @@ const enhanced = withObservables([], ({database, serverUrl}: EnhanceProps) => {
switchMap(([id, ccId]) => of$(id === ccId)),
distinctUntilChanged(),
);
const callsStateEnabledDict = observeCallsState(serverUrl).pipe(
switchMap((state) => of$(state.enabled)),
distinctUntilChanged(), // Did the enabled object ref change? If so, a channel's enabled state has changed.
);
const callsDefaultEnabled = observeCallsConfig(serverUrl).pipe(
switchMap((config) => of$(config.DefaultEnabled)),
distinctUntilChanged(),
);
const isCallsEnabledInChannel = combineLatest([channelId, callsStateEnabledDict, callsDefaultEnabled]).pipe(
switchMap(([id, enabled, defaultEnabled]) => {
const explicitlyEnabled = enabled.hasOwnProperty(id as string) && enabled[id];
const explicitlyDisabled = enabled.hasOwnProperty(id as string) && !enabled[id];
return of$(explicitlyEnabled || (!explicitlyDisabled && defaultEnabled));
}),
distinctUntilChanged(),
);
const isCallsEnabledInChannel = observeIsCallsEnabledInChannel(database, serverUrl, channelId);
return {
channelId,

View File

@@ -26,7 +26,6 @@ type Props = {
type?: ChannelType;
canEnableDisableCalls: boolean;
isCallsEnabledInChannel: boolean;
isCallsFeatureRestricted: boolean;
}
const edges: Edge[] = ['bottom', 'left', 'right'];
@@ -53,12 +52,14 @@ const ChannelInfo = ({
type,
canEnableDisableCalls,
isCallsEnabledInChannel,
isCallsFeatureRestricted,
}: Props) => {
const theme = useTheme();
const serverUrl = useServerUrl();
const styles = getStyleSheet(theme);
const callsAvailable = isCallsEnabledInChannel && !isCallsFeatureRestricted;
// NOTE: isCallsEnabledInChannel will be true/false (not undefined) based on explicit state + the DefaultEnabled system setting
// which comes from observeIsCallsEnabledInChannel
const callsAvailable = isCallsEnabledInChannel;
const onPressed = useCallback(() => {
return dismissModal({componentId});
@@ -97,7 +98,7 @@ const ChannelInfo = ({
callsEnabled={callsAvailable}
/>
<View style={styles.separator}/>
{canEnableDisableCalls && !isCallsFeatureRestricted &&
{canEnableDisableCalls &&
<>
<ChannelInfoEnableCalls
channelId={channelId}

View File

@@ -6,13 +6,19 @@ import withObservables from '@nozbe/with-observables';
import {combineLatest, of as of$} from 'rxjs';
import {distinctUntilChanged, switchMap} from 'rxjs/operators';
import {observeIsCallsFeatureRestricted} from '@calls/observers';
import {observeCallsConfig, observeCallsState} from '@calls/state';
import {General} from '@constants';
import {observeIsCallsEnabledInChannel} from '@calls/observers';
import {observeCallsConfig} from '@calls/state';
import {withServerUrl} from '@context/server';
import {observeCurrentChannel} from '@queries/servers/channel';
import {observeCurrentChannelId, observeCurrentUserId} from '@queries/servers/system';
import {observeCurrentUser, observeUserIsChannelAdmin} from '@queries/servers/user';
import {
observeConfigValue,
observeCurrentChannelId,
observeCurrentTeamId,
observeCurrentUserId,
} from '@queries/servers/system';
import {observeCurrentUser, observeUserIsChannelAdmin, observeUserIsTeamAdmin} from '@queries/servers/user';
import {isTypeDMorGM} from '@utils/channel';
import {isMinimumServerVersion} from '@utils/helpers';
import {isSystemAdmin} from '@utils/user';
import ChannelInfo from './channel_info';
@@ -27,7 +33,17 @@ const enhanced = withObservables([], ({serverUrl, database}: Props) => {
const channel = observeCurrentChannel(database);
const type = channel.pipe(switchMap((c) => of$(c?.type)));
const channelId = channel.pipe(switchMap((c) => of$(c?.id || '')));
const teamId = channel.pipe(switchMap((c) => (c?.teamId ? of$(c.teamId) : observeCurrentTeamId(database))));
const userId = observeCurrentUserId(database);
const isTeamAdmin = combineLatest([teamId, userId]).pipe(
switchMap(([tId, uId]) => observeUserIsTeamAdmin(database, uId, tId)),
);
// callsDefaultEnabled means "live mode" post 7.6
const callsDefaultEnabled = observeCallsConfig(serverUrl).pipe(
switchMap((config) => of$(config.DefaultEnabled)),
distinctUntilChanged(),
);
const allowEnableCalls = observeCallsConfig(serverUrl).pipe(
switchMap((config) => of$(config.AllowEnableCalls)),
distinctUntilChanged(),
@@ -37,48 +53,55 @@ const enhanced = withObservables([], ({serverUrl, database}: Props) => {
switchMap((roles) => of$(isSystemAdmin(roles || ''))),
distinctUntilChanged(),
);
const channelAdmin = combineLatest([observeCurrentUserId(database), channelId]).pipe(
switchMap(([userId, chId]) => observeUserIsChannelAdmin(database, userId, chId)),
const channelAdmin = combineLatest([userId, channelId]).pipe(
switchMap(([uId, chId]) => observeUserIsChannelAdmin(database, uId, chId)),
distinctUntilChanged(),
);
const canEnableDisableCalls = combineLatest([type, allowEnableCalls, systemAdmin, channelAdmin]).pipe(
switchMap(([t, allow, sysAdmin, chAdmin]) => {
const isDirectMessage = t === General.DM_CHANNEL;
const isGroupMessage = t === General.GM_CHANNEL;
const callsGAServer = observeConfigValue(database, 'Version').pipe(
switchMap((v) => of$(isMinimumServerVersion(v || '', 7, 6))),
);
const dmOrGM = type.pipe(switchMap((t) => of$(isTypeDMorGM(t))));
const canEnableDisableCalls = combineLatest([callsDefaultEnabled, allowEnableCalls, systemAdmin, channelAdmin, callsGAServer, dmOrGM, isTeamAdmin]).pipe(
switchMap(([liveMode, allow, sysAdmin, chAdmin, gaServer, dmGM, tAdmin]) => {
// if GA 7.6:
// allow (will always be true) and !liveMode = system admins can enable/disable
// allow (will always be true) and liveMode = channel, team, system admins, DM/GM participants can enable/disable
// if pre GA 7.6:
// allow and !liveMode = channel, system admins, DM/GM participants can enable/disable
// allow and liveMode = channel, system admins, DM/GM participants can enable/disable
// !allow and !liveMode = system admins can enable/disable -- can combine with below
// !allow and liveMode = system admins can enable/disable -- can combine with above
// Note: There are ways to 'simplify' the conditions below. Here we're preferring clarity.
const isAdmin = sysAdmin || chAdmin;
let temp = Boolean(sysAdmin);
if (allow) {
temp = Boolean(isDirectMessage || isGroupMessage || isAdmin);
if (gaServer) {
if (allow && !liveMode) {
return of$(Boolean(sysAdmin));
}
if (allow && liveMode) {
return of$(Boolean(chAdmin || tAdmin || sysAdmin || dmGM));
}
return of$(false);
}
return of$(temp);
// now we're pre GA 7.6
if (allow && liveMode) {
return of$(Boolean(chAdmin || sysAdmin || dmGM));
}
if (allow && !liveMode) {
return of$(Boolean(sysAdmin || chAdmin || dmGM));
}
if (!allow) {
return of$(Boolean(sysAdmin));
}
return of$(false);
}),
);
const callsDefaultEnabled = observeCallsConfig(serverUrl).pipe(
switchMap((config) => of$(config.DefaultEnabled)),
distinctUntilChanged(),
);
const callsStateEnabledDict = observeCallsState(serverUrl).pipe(
switchMap((state) => of$(state.enabled)),
distinctUntilChanged(), // Did the enabled object ref change? If so, a channel's enabled state has changed.
);
const isCallsEnabledInChannel = combineLatest([observeCurrentChannelId(database), callsStateEnabledDict, callsDefaultEnabled]).pipe(
switchMap(([id, enabled, defaultEnabled]) => {
const explicitlyEnabled = enabled.hasOwnProperty(id as string) && enabled[id];
const explicitlyDisabled = enabled.hasOwnProperty(id as string) && !enabled[id];
return of$(explicitlyEnabled || (!explicitlyDisabled && defaultEnabled));
}),
distinctUntilChanged(),
);
const isCallsFeatureRestricted = channelId.pipe(
switchMap((id) => observeIsCallsFeatureRestricted(database, serverUrl, id)),
);
const isCallsEnabledInChannel = observeIsCallsEnabledInChannel(database, serverUrl, observeCurrentChannelId(database));
return {
type,
canEnableDisableCalls,
isCallsEnabledInChannel,
isCallsFeatureRestricted,
};
});