[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": { "devDependencies": {
"@deanwhillier/jest-matchmedia-mock": "1.2.0", "@deanwhillier/jest-matchmedia-mock": "1.2.0",
"@hot-loader/react-dom": "17.0.2", "@hot-loader/react-dom": "17.0.2",
"@mattermost/calls-common": "0.27.0",
"@mattermost/eslint-plugin": "*", "@mattermost/eslint-plugin": "*",
"@redux-devtools/extension": "3.2.3", "@redux-devtools/extension": "3.2.3",
"@stylistic/stylelint-plugin": "2.1.0", "@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 {renderWithContext} from 'tests/react_testing_utils';
import {TestHelper} from 'utils/test_helper'; import {TestHelper} from 'utils/test_helper';
import {getDirectChannelName} from 'utils/utils';
import type {GlobalState} from 'types/store'; import type {GlobalState} from 'types/store';
@@ -52,6 +53,10 @@ function getBasePropsAndState(): [Props, DeepPartial<GlobalState>] {
const currentUser = TestHelper.getUserMock({id: 'currentUser', roles: 'role'}); const currentUser = TestHelper.getUserMock({id: 'currentUser', roles: 'role'});
const currentTeam = TestHelper.getTeamMock({id: 'currentTeam'}); const currentTeam = TestHelper.getTeamMock({id: 'currentTeam'});
const channel = TestHelper.getChannelMock({id: 'channelId', team_id: currentTeam.id, type: General.OPEN_CHANNEL}); 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> = { const state: DeepPartial<GlobalState> = {
entities: { entities: {
@@ -84,9 +89,11 @@ function getBasePropsAndState(): [Props, DeepPartial<GlobalState>] {
channels: { channels: {
channels: { channels: {
[channel.id]: channel, [channel.id]: channel,
[dmChannel.id]: dmChannel,
}, },
myMembers: { myMembers: {
[channel.id]: {}, [channel.id]: {},
[dmChannel.id]: {},
}, },
}, },
general: { general: {
@@ -140,7 +147,7 @@ function getBasePropsAndState(): [Props, DeepPartial<GlobalState>] {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-ignore
'plugins-com.mattermost.calls': { 'plugins-com.mattermost.calls': {
profiles: {}, sessions: {},
}, },
}; };
const props: Props = { const props: Props = {
@@ -302,16 +309,16 @@ describe('components/ProfilePopover', () => {
expect(screen.queryByLabelText('Start Call')).not.toBeInTheDocument(); 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(); 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); 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'); 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})); (Client4.getCallsChannelState as jest.Mock).mockImplementationOnce(async () => ({enabled: false}));
const [props, initialState] = getBasePropsAndState(); const [props, initialState] = getBasePropsAndState();

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

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

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

@@ -1,53 +1,53 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import {checkUserInCall} from './call_button'; import {isUserInCall} from './call_button';
describe('checkUserInCall', () => { describe('isUserInCall', () => {
test('missing state', () => { test('missing state', () => {
expect(checkUserInCall({ expect(isUserInCall({
'plugins-com.mattermost.calls': {}, 'plugins-com.mattermost.calls': {},
} as any, 'userA')).toBe(false); } as any, 'userA', 'channelID')).toBe(false);
}); });
test('call state missing', () => { test('call state missing', () => {
expect(checkUserInCall({ expect(isUserInCall({
'plugins-com.mattermost.calls': { 'plugins-com.mattermost.calls': {
profiles: { sessions: {
channelID: null, channelID: null,
}, },
}, },
} as any, 'userA')).toBe(false); } as any, 'userA', 'channelID')).toBe(false);
}); });
test('user not in call', () => { test('user not in call', () => {
expect(checkUserInCall({ expect(isUserInCall({
'plugins-com.mattermost.calls': { 'plugins-com.mattermost.calls': {
profiles: { sessions: {
channelID: { channelID: {
sessionB: { sessionB: {
id: 'userB', user_id: 'userB',
}, },
}, },
}, },
}, },
} as any, 'userA')).toBe(false); } as any, 'userA', 'channelID')).toBe(false);
}); });
test('user in call', () => { test('user in call', () => {
expect(checkUserInCall({ expect(isUserInCall({
'plugins-com.mattermost.calls': { 'plugins-com.mattermost.calls': {
profiles: { sessions: {
channelID: { channelID: {
sessionB: { sessionB: {
id: 'userB', user_id: 'userB',
}, },
sessionA: { 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 {Client4} from 'mattermost-redux/client';
import {getChannelByName} from 'mattermost-redux/selectors/entities/channels'; 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 OverlayTrigger from 'components/overlay_trigger';
import ProfilePopoverCallButton from 'components/profile_popover_call_button'; import ProfilePopoverCallButton from 'components/profile_popover_call_button';
@@ -25,7 +24,6 @@ import type {GlobalState} from 'types/store';
type Props = { type Props = {
userId: string; userId: string;
currentUserId: string; currentUserId: string;
channelId?: string;
fullname: string; fullname: string;
username: string; username: string;
} }
@@ -35,14 +33,14 @@ type ChannelCallsState = {
id: string; id: string;
}; };
export function checkUserInCall(state: GlobalState, userId: string) { export function isUserInCall(state: GlobalState, userId: string, channelId: string) {
for (const profilesMap of Object.values(getProfilesInCalls(state))) { const sessionsInCall = getSessionsInCalls(state)[channelId] || {};
for (const profile of Object.values(profilesMap || {})) {
if (profile?.id === userId) { for (const session of Object.values(sessionsInCall)) {
if (session.user_id === userId) {
return true; return true;
} }
} }
}
return false; return false;
} }
@@ -50,22 +48,22 @@ export function checkUserInCall(state: GlobalState, userId: string) {
const CallButton = ({ const CallButton = ({
userId, userId,
currentUserId, currentUserId,
channelId,
fullname, fullname,
username, username,
}: Props) => { }: Props) => {
const {formatMessage} = useIntl(); const {formatMessage} = useIntl();
const isCallsEnabled = useSelector((state: GlobalState) => getIsCallsEnabled(state)); 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 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 [callsDMChannelState, setCallsDMChannelState] = useState<ChannelCallsState>();
const [callsChannelState, setCallsChannelState] = useState<ChannelCallsState>();
const getCallsChannelState = useCallback((channelId: string): Promise<ChannelCallsState> => { const getCallsChannelState = useCallback((channelId: string): Promise<ChannelCallsState> => {
let data: Promise<ChannelCallsState>; let data: Promise<ChannelCallsState>;
@@ -84,26 +82,17 @@ const CallButton = ({
setCallsDMChannelState(data); setCallsDMChannelState(data);
}); });
} }
if (isCallsEnabled && channelId) {
getCallsChannelState(channelId).then((data) => {
setCallsChannelState(data);
});
}
}, []); }, []);
if ( if (!isCallsEnabled || callsDMChannelState?.enabled === false) {
!isCallsEnabled ||
callsDMChannelState?.enabled === false ||
(!isCallsDefaultEnabledOnAllChannels && !isCallsCanBeDisabledOnSpecificChannels && callsChannelState?.enabled === false)
) {
return null; return null;
} }
const disabled = isUserInCall || isCurrentUserInCall; // We disable the button if there's already a call ongoing with the user.
const startCallMessage = isUserInCall ? formatMessage({ const disabled = hasDMCall;
id: 'user_profile.call.userBusy', const startCallMessage = hasDMCall ? formatMessage({
defaultMessage: '{user} is in another call', id: 'user_profile.call.ongoing',
defaultMessage: 'Call with {user} is ongoing',
}, {user: fullname || username}, }, {user: fullname || username},
) : formatMessage({ ) : formatMessage({
id: 'webapp.mattermost.feature.start_call', id: 'webapp.mattermost.feature.start_call',

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

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

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

@@ -5258,7 +5258,7 @@
"user_profile.account.post_was_created": "This post was created by an integration from @{username}", "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": "Add to a Channel",
"user_profile.add_user_to_channel.icon": "Add User to Channel Icon", "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": "Status",
"user_profile.custom_status.set_status": "Set a status", "user_profile.custom_status.set_status": "Set a status",
"user_profile.send.dm": "Message", "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'; 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 // Channels
export function getCurrentChannelId(state: GlobalState): string { export function getCurrentChannelId(state: GlobalState): string {
@@ -69,20 +56,6 @@ export function getUsers(state: GlobalState): IDMappedObjects<UserProfile> {
return state.entities.users.profiles; 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 // Config
export const getIsUserStatusesConfigEnabled: (a: GlobalState) => boolean = createSelector( export const getIsUserStatusesConfigEnabled: (a: GlobalState) => boolean = createSelector(
'getIsUserStatusesConfigEnabled', 'getIsUserStatusesConfigEnabled',

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

@@ -3,10 +3,14 @@
import semver from 'semver'; import semver from 'semver';
import type {CallsConfig, UserSessionState} from '@mattermost/calls-common/lib/types';
import {suitePluginIds} from 'utils/constants'; import {suitePluginIds} from 'utils/constants';
import type {GlobalState} from 'types/store'; import type {GlobalState} from 'types/store';
const CALLS_PLUGIN = 'plugins-com.mattermost.calls';
export function isCallsEnabled(state: GlobalState, minVersion = '0.4.2') { export function isCallsEnabled(state: GlobalState, minVersion = '0.4.2') {
return Boolean(state.plugins.plugins[suitePluginIds.calls] && return Boolean(state.plugins.plugins[suitePluginIds.calls] &&
semver.gte(String(semver.clean(state.plugins.plugins[suitePluginIds.calls].version || '0.0.0')), minVersion)); 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 // @ts-ignore
return Boolean(state[`plugins-${suitePluginIds.calls}`]?.callsConfig?.EnableRinging); 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": { "devDependencies": {
"@deanwhillier/jest-matchmedia-mock": "1.2.0", "@deanwhillier/jest-matchmedia-mock": "1.2.0",
"@hot-loader/react-dom": "17.0.2", "@hot-loader/react-dom": "17.0.2",
"@mattermost/calls-common": "0.27.0",
"@mattermost/eslint-plugin": "*", "@mattermost/eslint-plugin": "*",
"@redux-devtools/extension": "3.2.3", "@redux-devtools/extension": "3.2.3",
"@stylistic/stylelint-plugin": "2.1.0", "@stylistic/stylelint-plugin": "2.1.0",
@@ -4244,6 +4245,12 @@
"node": ">=10" "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": { "node_modules/@mattermost/client": {
"resolved": "platform/client", "resolved": "platform/client",
"link": true "link": true