[MM-58002] Review start call button functionality in profile popover (#26867)

* Review start call button functionality in profile popover

* Address feedback

* Use published @mattermost/calls-common package

* Fix import

* Lint fix
Этот коммит содержится в:
Claudio Costa
2024-05-08 13:47:36 -06:00
коммит произвёл GitHub
родитель be2ffbcd0c
Коммит 5d4ad44bfe
10 изменённых файлов: 73 добавлений и 84 удалений

Просмотреть файл

@@ -103,6 +103,7 @@
"devDependencies": {
"@deanwhillier/jest-matchmedia-mock": "1.2.0",
"@hot-loader/react-dom": "17.0.2",
"@mattermost/calls-common": "0.27.0",
"@mattermost/eslint-plugin": "*",
"@redux-devtools/extension": "3.2.3",
"@stylistic/stylelint-plugin": "2.1.0",

Просмотреть файл

@@ -14,6 +14,7 @@ import {General, Permissions} from 'mattermost-redux/constants';
import {renderWithContext} from 'tests/react_testing_utils';
import {TestHelper} from 'utils/test_helper';
import {getDirectChannelName} from 'utils/utils';
import type {GlobalState} from 'types/store';
@@ -52,6 +53,10 @@ function getBasePropsAndState(): [Props, DeepPartial<GlobalState>] {
const currentUser = TestHelper.getUserMock({id: 'currentUser', roles: 'role'});
const currentTeam = TestHelper.getTeamMock({id: 'currentTeam'});
const channel = TestHelper.getChannelMock({id: 'channelId', team_id: currentTeam.id, type: General.OPEN_CHANNEL});
const dmChannel = {
id: 'dmChannelId',
name: getDirectChannelName(user.id, currentUser.id),
};
const state: DeepPartial<GlobalState> = {
entities: {
@@ -84,9 +89,11 @@ function getBasePropsAndState(): [Props, DeepPartial<GlobalState>] {
channels: {
channels: {
[channel.id]: channel,
[dmChannel.id]: dmChannel,
},
myMembers: {
[channel.id]: {},
[dmChannel.id]: {},
},
},
general: {
@@ -140,7 +147,7 @@ function getBasePropsAndState(): [Props, DeepPartial<GlobalState>] {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
'plugins-com.mattermost.calls': {
profiles: {},
sessions: {},
},
};
const props: Props = {
@@ -302,16 +309,16 @@ describe('components/ProfilePopover', () => {
expect(screen.queryByLabelText('Start Call')).not.toBeInTheDocument();
});
test('should disable start call button when user is in another call', async () => {
test('should disable start call button when call is ongoing in the DM', async () => {
const [props, initialState] = getBasePropsAndState();
(initialState as any)['plugins-com.mattermost.calls'].profiles = {fakeChannel: {currentUser: {id: 'currentUser'}}};
(initialState as any)['plugins-com.mattermost.calls'].sessions = {dmChannelId: {currentUser: {user_id: 'currentUser'}}};
renderWithPluginReducers(<ProfilePopover {...props}/>, initialState);
const button = (await screen.findByLabelText('Start Call')).closest('button');
const button = (await screen.findByLabelText('Call with user is ongoing')).closest('button');
expect(button?.getAttribute('aria-disabled')).toBe('true');
});
test('should not show the start call button when isCallsDefaultEnabledOnAllChannels, isCallsCanBeDisabledOnSpecificChannels is false and callsChannelState.enabled is false', async () => {
test('should not show the start call button when callsChannelState.enabled is false', async () => {
(Client4.getCallsChannelState as jest.Mock).mockImplementationOnce(async () => ({enabled: false}));
const [props, initialState] = getBasePropsAndState();

Просмотреть файл

@@ -310,7 +310,6 @@ const ProfilePopover = ({
username={user.username}
/>
<ProfilePopoverActions
channelId={channelId}
currentUserId={currentUserId}
fullname={fullname}
handleCloseModals={handleCloseModals}

Просмотреть файл

@@ -1,53 +1,53 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {checkUserInCall} from './call_button';
import {isUserInCall} from './call_button';
describe('checkUserInCall', () => {
describe('isUserInCall', () => {
test('missing state', () => {
expect(checkUserInCall({
expect(isUserInCall({
'plugins-com.mattermost.calls': {},
} as any, 'userA')).toBe(false);
} as any, 'userA', 'channelID')).toBe(false);
});
test('call state missing', () => {
expect(checkUserInCall({
expect(isUserInCall({
'plugins-com.mattermost.calls': {
profiles: {
sessions: {
channelID: null,
},
},
} as any, 'userA')).toBe(false);
} as any, 'userA', 'channelID')).toBe(false);
});
test('user not in call', () => {
expect(checkUserInCall({
expect(isUserInCall({
'plugins-com.mattermost.calls': {
profiles: {
sessions: {
channelID: {
sessionB: {
id: 'userB',
user_id: 'userB',
},
},
},
},
} as any, 'userA')).toBe(false);
} as any, 'userA', 'channelID')).toBe(false);
});
test('user in call', () => {
expect(checkUserInCall({
expect(isUserInCall({
'plugins-com.mattermost.calls': {
profiles: {
sessions: {
channelID: {
sessionB: {
id: 'userB',
user_id: 'userB',
},
sessionA: {
id: 'userA',
user_id: 'userA',
},
},
},
},
} as any, 'userA')).toBe(true);
} as any, 'userA', 'channelID')).toBe(true);
});
});

Просмотреть файл

@@ -10,9 +10,8 @@ import {PhoneInTalkIcon} from '@mattermost/compass-icons/components';
import {Client4} from 'mattermost-redux/client';
import {getChannelByName} from 'mattermost-redux/selectors/entities/channels';
import {getCallsConfig, getProfilesInCalls} from 'mattermost-redux/selectors/entities/common';
import {isCallsEnabled as getIsCallsEnabled} from 'selectors/calls';
import {isCallsEnabled as getIsCallsEnabled, getSessionsInCalls} from 'selectors/calls';
import OverlayTrigger from 'components/overlay_trigger';
import ProfilePopoverCallButton from 'components/profile_popover_call_button';
@@ -25,7 +24,6 @@ import type {GlobalState} from 'types/store';
type Props = {
userId: string;
currentUserId: string;
channelId?: string;
fullname: string;
username: string;
}
@@ -35,12 +33,12 @@ type ChannelCallsState = {
id: string;
};
export function checkUserInCall(state: GlobalState, userId: string) {
for (const profilesMap of Object.values(getProfilesInCalls(state))) {
for (const profile of Object.values(profilesMap || {})) {
if (profile?.id === userId) {
return true;
}
export function isUserInCall(state: GlobalState, userId: string, channelId: string) {
const sessionsInCall = getSessionsInCalls(state)[channelId] || {};
for (const session of Object.values(sessionsInCall)) {
if (session.user_id === userId) {
return true;
}
}
@@ -50,22 +48,22 @@ export function checkUserInCall(state: GlobalState, userId: string) {
const CallButton = ({
userId,
currentUserId,
channelId,
fullname,
username,
}: Props) => {
const {formatMessage} = useIntl();
const isCallsEnabled = useSelector((state: GlobalState) => getIsCallsEnabled(state));
const isUserInCall = useSelector((state: GlobalState) => (isCallsEnabled ? checkUserInCall(state, userId) : undefined));
const isCurrentUserInCall = useSelector((state: GlobalState) => (isCallsEnabled ? checkUserInCall(state, currentUserId) : undefined));
const callsConfig = useSelector((state: GlobalState) => (isCallsEnabled ? getCallsConfig(state) : undefined));
const isCallsDefaultEnabledOnAllChannels = callsConfig?.DefaultEnabled;
const isCallsCanBeDisabledOnSpecificChannels = callsConfig?.AllowEnableCalls;
const dmChannel = useSelector((state: GlobalState) => getChannelByName(state, getDirectChannelName(currentUserId, userId)));
const hasDMCall = useSelector((state: GlobalState) => {
if (isCallsEnabled && dmChannel) {
return isUserInCall(state, currentUserId, dmChannel.id) || isUserInCall(state, userId, dmChannel.id);
}
return false;
});
const [callsDMChannelState, setCallsDMChannelState] = useState<ChannelCallsState>();
const [callsChannelState, setCallsChannelState] = useState<ChannelCallsState>();
const getCallsChannelState = useCallback((channelId: string): Promise<ChannelCallsState> => {
let data: Promise<ChannelCallsState>;
@@ -84,26 +82,17 @@ const CallButton = ({
setCallsDMChannelState(data);
});
}
if (isCallsEnabled && channelId) {
getCallsChannelState(channelId).then((data) => {
setCallsChannelState(data);
});
}
}, []);
if (
!isCallsEnabled ||
callsDMChannelState?.enabled === false ||
(!isCallsDefaultEnabledOnAllChannels && !isCallsCanBeDisabledOnSpecificChannels && callsChannelState?.enabled === false)
) {
if (!isCallsEnabled || callsDMChannelState?.enabled === false) {
return null;
}
const disabled = isUserInCall || isCurrentUserInCall;
const startCallMessage = isUserInCall ? formatMessage({
id: 'user_profile.call.userBusy',
defaultMessage: '{user} is in another call',
// We disable the button if there's already a call ongoing with the user.
const disabled = hasDMCall;
const startCallMessage = hasDMCall ? formatMessage({
id: 'user_profile.call.ongoing',
defaultMessage: 'Call with {user} is ongoing',
}, {user: fullname || username},
) : formatMessage({
id: 'webapp.mattermost.feature.start_call',

Просмотреть файл

@@ -13,7 +13,6 @@ import CallButton from './call_button';
type Props = {
user: UserProfile;
fullname: string;
channelId?: string;
currentUserId: string;
haveOverrideProp: boolean;
handleShowDirectChannel: (e: React.MouseEvent<HTMLButtonElement>) => void;
@@ -31,7 +30,6 @@ const ProfilePopoverActions = ({
returnFocus,
hide,
fullname,
channelId,
}: Props) => {
const {formatMessage} = useIntl();
@@ -69,7 +67,6 @@ const ProfilePopoverActions = ({
hide={hide}
/>
<CallButton
channelId={channelId}
currentUserId={currentUserId}
fullname={fullname}
userId={user.id}

Просмотреть файл

@@ -5258,7 +5258,7 @@
"user_profile.account.post_was_created": "This post was created by an integration from @{username}",
"user_profile.add_user_to_channel": "Add to a Channel",
"user_profile.add_user_to_channel.icon": "Add User to Channel Icon",
"user_profile.call.userBusy": "{user} is in another call",
"user_profile.call.ongoing": "Call with {user} is ongoing",
"user_profile.custom_status": "Status",
"user_profile.custom_status.set_status": "Set a status",
"user_profile.send.dm": "Message",

Просмотреть файл

@@ -9,19 +9,6 @@ import type {RelationOneToOne, IDMappedObjects} from '@mattermost/types/utilitie
import {createSelector} from 'mattermost-redux/selectors/create_selector';
const CALLS_PLUGIN = 'plugins-com.mattermost.calls';
type CallsConfig = {
ICEServers: string[];
ICEServersConfigs: RTCIceServer[];
AllowEnableCalls: boolean;
DefaultEnabled: boolean;
MaxCallParticipants: number;
NeedsTURNCredentials: boolean;
AllowScreenSharing: boolean;
sku_short_name: string;
}
// Channels
export function getCurrentChannelId(state: GlobalState): string {
@@ -69,20 +56,6 @@ export function getUsers(state: GlobalState): IDMappedObjects<UserProfile> {
return state.entities.users.profiles;
}
// Calls
export function getProfilesInCalls(state: GlobalState): Record<string, Record<string, UserProfile>> {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
return state[CALLS_PLUGIN].profiles || {};
}
export function getCallsConfig(state: GlobalState): CallsConfig {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
return state[CALLS_PLUGIN].callsConfig;
}
// Config
export const getIsUserStatusesConfigEnabled: (a: GlobalState) => boolean = createSelector(
'getIsUserStatusesConfigEnabled',

Просмотреть файл

@@ -3,10 +3,14 @@
import semver from 'semver';
import type {CallsConfig, UserSessionState} from '@mattermost/calls-common/lib/types';
import {suitePluginIds} from 'utils/constants';
import type {GlobalState} from 'types/store';
const CALLS_PLUGIN = 'plugins-com.mattermost.calls';
export function isCallsEnabled(state: GlobalState, minVersion = '0.4.2') {
return Boolean(state.plugins.plugins[suitePluginIds.calls] &&
semver.gte(String(semver.clean(state.plugins.plugins[suitePluginIds.calls].version || '0.0.0')), minVersion));
@@ -18,3 +22,15 @@ export function isCallsRingingEnabledOnServer(state: GlobalState) {
// @ts-ignore
return Boolean(state[`plugins-${suitePluginIds.calls}`]?.callsConfig?.EnableRinging);
}
export function getSessionsInCalls(state: GlobalState): Record<string, Record<string, UserSessionState>> {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
return state[CALLS_PLUGIN].sessions || {};
}
export function getCallsConfig(state: GlobalState): CallsConfig {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
return state[CALLS_PLUGIN].callsConfig;
}

7
webapp/package-lock.json сгенерированный
Просмотреть файл

@@ -152,6 +152,7 @@
"devDependencies": {
"@deanwhillier/jest-matchmedia-mock": "1.2.0",
"@hot-loader/react-dom": "17.0.2",
"@mattermost/calls-common": "0.27.0",
"@mattermost/eslint-plugin": "*",
"@redux-devtools/extension": "3.2.3",
"@stylistic/stylelint-plugin": "2.1.0",
@@ -4244,6 +4245,12 @@
"node": ">=10"
}
},
"node_modules/@mattermost/calls-common": {
"version": "0.27.0",
"resolved": "https://registry.npmjs.org/@mattermost/calls-common/-/calls-common-0.27.0.tgz",
"integrity": "sha512-G/r8dWoloDycHkOtewIkme7af8OEVsnjWT77+GsmRypGkZAWJy+nI7zecYFLBzOROjoqGRhn1QUNsPartUgC/w==",
"dev": true
},
"node_modules/@mattermost/client": {
"resolved": "platform/client",
"link": true