Skip to content

Commit

Permalink
fix: show only relevant userInfoActions for mentioned non-members (#3…
Browse files Browse the repository at this point in the history
  • Loading branch information
abhinavkrin authored Aug 23, 2024
1 parent 58c0efc commit b764c41
Show file tree
Hide file tree
Showing 14 changed files with 684 additions and 15 deletions.
7 changes: 7 additions & 0 deletions .changeset/ninety-hounds-exist.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@rocket.chat/rest-typings': patch
'@rocket.chat/meteor': patch
'@rocket.chat/i18n': patch
---

Fix: Show correct user info actions for non-members in channels.
40 changes: 38 additions & 2 deletions apps/meteor/app/api/server/v1/rooms.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import { Media } from '@rocket.chat/core-services';
import type { IRoom, IUpload } from '@rocket.chat/core-typings';
import { Messages, Rooms, Users, Uploads } from '@rocket.chat/models';
import { Messages, Rooms, Users, Uploads, Subscriptions } from '@rocket.chat/models';
import type { Notifications } from '@rocket.chat/rest-typings';
import { isGETRoomsNameExists, isRoomsImagesProps, isRoomsMuteUnmuteUserProps, isRoomsExportProps } from '@rocket.chat/rest-typings';
import {
isGETRoomsNameExists,
isRoomsImagesProps,
isRoomsMuteUnmuteUserProps,
isRoomsExportProps,
isRoomsIsMemberProps,
} from '@rocket.chat/rest-typings';
import { Meteor } from 'meteor/meteor';

import { isTruthy } from '../../../../lib/isTruthy';
Expand Down Expand Up @@ -783,6 +789,36 @@ API.v1.addRoute(
},
);

API.v1.addRoute(
'rooms.isMember',
{
authRequired: true,
validateParams: isRoomsIsMemberProps,
},
{
async get() {
const { roomId, userId, username } = this.queryParams;
const [room, user] = await Promise.all([
findRoomByIdOrName({
params: { roomId },
}) as Promise<IRoom>,
Users.findOneByIdOrUsername(userId || username),
]);

if (!user?._id) {
return API.v1.failure('error-user-not-found');
}

if (await canAccessRoomAsync(room, { _id: this.user._id })) {
return API.v1.success({
isMember: (await Subscriptions.countByRoomIdAndUserId(room._id, user._id)) > 0,
});
}
return API.v1.unauthorized();
},
},
);

API.v1.addRoute(
'rooms.muteUser',
{ authRequired: true, validateParams: isRoomsMuteUnmuteUserProps },
Expand Down
10 changes: 10 additions & 0 deletions apps/meteor/client/views/hooks/useMemberExists.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { useEndpoint } from '@rocket.chat/ui-contexts';
import { useQuery } from '@tanstack/react-query';

type UseMemberExistsProps = { roomId: string; username: string };

export const useMemberExists = ({ roomId, username }: UseMemberExistsProps) => {
const checkMember = useEndpoint('GET', '/v1/rooms.isMember');

return useQuery(['rooms/isMember', roomId, username], () => checkMember({ roomId, username }));
};
15 changes: 14 additions & 1 deletion apps/meteor/client/views/room/UserCard/UserCardWithData.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import LocalTime from '../../../components/LocalTime';
import { UserCard, UserCardAction, UserCardRole, UserCardSkeleton } from '../../../components/UserCard';
import { ReactiveUserStatus } from '../../../components/UserStatus';
import { useUserInfoQuery } from '../../../hooks/useUserInfoQuery';
import { useMemberExists } from '../../hooks/useMemberExists';
import { useUserInfoActions } from '../hooks/useUserInfoActions';

type UserCardWithDataProps = {
Expand All @@ -24,7 +25,16 @@ const UserCardWithData = ({ username, rid, onOpenUserInfo, onClose }: UserCardWi
const getRoles = useRolesDescription();
const showRealNames = Boolean(useSetting('UI_Use_Real_Name'));

const { data, isLoading } = useUserInfoQuery({ username });
const { data, isLoading: isUserInfoLoading } = useUserInfoQuery({ username });
const {
data: isMemberData,
refetch,
isSuccess: membershipCheckSuccess,
isLoading: isMembershipStatusLoading,
} = useMemberExists({ roomId: rid, username });

const isLoading = isUserInfoLoading || isMembershipStatusLoading;
const isMember = membershipCheckSuccess && isMemberData?.isMember;

const user = useMemo(() => {
const defaultValue = isLoading ? undefined : null;
Expand Down Expand Up @@ -62,6 +72,9 @@ const UserCardWithData = ({ username, rid, onOpenUserInfo, onClose }: UserCardWi
const { actions: actionsDefinition, menuActions: menuOptions } = useUserInfoActions(
{ _id: user._id ?? '', username: user.username, name: user.name },
rid,
refetch,
undefined,
isMember,
);

const menu = useMemo(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ type RoomMembersActionsProps = {

const RoomMembersActions = ({ username, _id, name, rid, reload }: RoomMembersActionsProps): ReactElement | null => {
const t = useTranslation();
const { menuActions: menuOptions } = useUserInfoActions({ _id, username, name }, rid, reload, 0);
const { menuActions: menuOptions } = useUserInfoActions({ _id, username, name }, rid, reload, 0, true);
if (!menuOptions) {
return null;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
/* eslint-disable react/display-name, react/no-multi-comp */
import type { IRoom, IUser } from '@rocket.chat/core-typings';
import { ButtonGroup, IconButton } from '@rocket.chat/fuselage';
import { ButtonGroup, IconButton, Skeleton } from '@rocket.chat/fuselage';
import { useTranslation } from '@rocket.chat/ui-contexts';
import type { ReactElement } from 'react';
import React, { useMemo } from 'react';

import GenericMenu from '../../../../components/GenericMenu/GenericMenu';
import { UserInfoAction } from '../../../../components/UserInfo';
import { useMemberExists } from '../../../hooks/useMemberExists';
import { useUserInfoActions } from '../../hooks/useUserInfoActions';

type UserInfoActionsProps = {
Expand All @@ -17,10 +18,24 @@ type UserInfoActionsProps = {

const UserInfoActions = ({ user, rid, backToList }: UserInfoActionsProps): ReactElement => {
const t = useTranslation();
const {
data: isMemberData,
refetch,
isSuccess: membershipCheckSuccess,
isLoading,
} = useMemberExists({ roomId: rid, username: user.username as string });

const isMember = membershipCheckSuccess && isMemberData?.isMember;

const { actions: actionsDefinition, menuActions: menuOptions } = useUserInfoActions(
{ _id: user._id, username: user.username, name: user.name },
rid,
backToList,
() => {
backToList?.();
refetch();
},
undefined,
isMember,
);

const menu = useMemo(() => {
Expand Down Expand Up @@ -51,6 +66,9 @@ const UserInfoActions = ({ user, rid, backToList }: UserInfoActionsProps): React
return [...actionsDefinition.map(mapAction), menu].filter(Boolean);
}, [actionsDefinition, menu]);

if (isLoading) {
return <Skeleton w='full' />;
}
return <ButtonGroup align='center'>{actions}</ButtonGroup>;
};

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import type { IRoom, IUser } from '@rocket.chat/core-typings';
import { isRoomFederated } from '@rocket.chat/core-typings';
import { useEffectEvent } from '@rocket.chat/fuselage-hooks';
import {
useTranslation,
useUser,
useUserRoom,
useUserSubscription,
useToastMessageDispatch,
useAtLeastOnePermission,
useEndpoint,
} from '@rocket.chat/ui-contexts';
import { useMemo } from 'react';

import * as Federation from '../../../../../lib/federation/Federation';
import { useAddMatrixUsers } from '../../../contextualBar/RoomMembers/AddUsers/AddMatrixUsers/useAddMatrixUsers';
import { getRoomDirectives } from '../../../lib/getRoomDirectives';
import type { UserInfoAction } from '../useUserInfoActions';

const inviteUserEndpoints = {
c: '/v1/channels.invite',
p: '/v1/groups.invite',
} as const;

export const useAddUserAction = (
user: Pick<IUser, '_id' | 'username'>,
rid: IRoom['_id'],
reload?: () => void,
): UserInfoAction | undefined => {
const t = useTranslation();
const room = useUserRoom(rid);
const currentUser = useUser();
const subscription = useUserSubscription(rid);
const dispatchToastMessage = useToastMessageDispatch();

const { username, _id: uid } = user;

if (!room) {
throw Error('Room not provided');
}

const hasPermissionToAddUsers = useAtLeastOnePermission(
useMemo(() => [room?.t === 'p' ? 'add-user-to-any-p-room' : 'add-user-to-any-c-room', 'add-user-to-joined-room'], [room?.t]),
rid,
);

const userCanAdd =
room && user && isRoomFederated(room)
? Federation.isEditableByTheUser(currentUser || undefined, room, subscription)
: hasPermissionToAddUsers;

const { roomCanInvite } = getRoomDirectives({ room, showingUserId: uid, userSubscription: subscription });

const inviteUser = useEndpoint('POST', inviteUserEndpoints[room.t === 'p' ? 'p' : 'c']);

const handleAddUser = useEffectEvent(async ({ users }) => {
const [username] = users;
await inviteUser({ roomId: rid, username });
reload?.();
});

const addClickHandler = useAddMatrixUsers();

const addUserOptionAction = useEffectEvent(async () => {
try {
const users = [username as string];
if (isRoomFederated(room)) {
addClickHandler.mutate({
users,
handleSave: handleAddUser,
});
} else {
await handleAddUser({ users });
}
dispatchToastMessage({ type: 'success', message: t('User_added') });
} catch (error) {
dispatchToastMessage({ type: 'error', message: error as Error });
}
});

const addUserOption = useMemo(
() =>
roomCanInvite && userCanAdd && room.archived !== true
? {
content: t('add-to-room'),
icon: 'user-plus' as const,
onClick: addUserOptionAction,
type: 'management' as const,
}
: undefined,
[roomCanInvite, userCanAdd, room.archived, t, addUserOptionAction],
);

return addUserOption;
};
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { useMemo } from 'react';

import type { GenericMenuItemProps } from '../../../../components/GenericMenu/GenericMenuItem';
import { useEmbeddedLayout } from '../../../../hooks/useEmbeddedLayout';
import { useAddUserAction } from './actions/useAddUserAction';
import { useBlockUserAction } from './actions/useBlockUserAction';
import { useCallAction } from './actions/useCallAction';
import { useChangeLeaderAction } from './actions/useChangeLeaderAction';
Expand Down Expand Up @@ -39,7 +40,9 @@ export const useUserInfoActions = (
rid: IRoom['_id'],
reload?: () => void,
size = 2,
isMember?: boolean,
): { actions: [string, UserInfoAction][]; menuActions: any | undefined } => {
const addUser = useAddUserAction(user, rid, reload);
const blockUser = useBlockUserAction(user, rid);
const changeLeader = useChangeLeaderAction(user, rid);
const changeModerator = useChangeModeratorAction(user, rid);
Expand All @@ -58,15 +61,16 @@ export const useUserInfoActions = (
() => ({
...(openDirectMessage && !isLayoutEmbedded && { openDirectMessage }),
...(call && { call }),
...(changeOwner && { changeOwner }),
...(changeLeader && { changeLeader }),
...(changeModerator && { changeModerator }),
...(openModerationConsole && { openModerationConsole }),
...(ignoreUser && { ignoreUser }),
...(muteUser && { muteUser }),
...(!isMember && addUser && { addUser }),
...(isMember && changeOwner && { changeOwner }),
...(isMember && changeLeader && { changeLeader }),
...(isMember && changeModerator && { changeModerator }),
...(isMember && openModerationConsole && { openModerationConsole }),
...(isMember && ignoreUser && { ignoreUser }),
...(isMember && muteUser && { muteUser }),
...(blockUser && { toggleBlock: blockUser }),
...(reportUserOption && { reportUser: reportUserOption }),
...(removeUser && { removeUser }),
...(isMember && removeUser && { removeUser }),
}),
[
openDirectMessage,
Expand All @@ -81,6 +85,8 @@ export const useUserInfoActions = (
removeUser,
reportUserOption,
openModerationConsole,
addUser,
isMember,
],
);

Expand Down
6 changes: 4 additions & 2 deletions apps/meteor/client/views/room/lib/getRoomDirectives.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ type getRoomDirectiesType = {
roomCanBlock: boolean;
roomCanMute: boolean;
roomCanRemove: boolean;
roomCanInvite: boolean;
};

export const getRoomDirectives = ({
Expand All @@ -24,7 +25,7 @@ export const getRoomDirectives = ({
}): getRoomDirectiesType => {
const roomDirectives = room?.t && roomCoordinator.getRoomDirectives(room.t);

const [roomCanSetOwner, roomCanSetLeader, roomCanSetModerator, roomCanIgnore, roomCanBlock, roomCanMute, roomCanRemove] = [
const [roomCanSetOwner, roomCanSetLeader, roomCanSetModerator, roomCanIgnore, roomCanBlock, roomCanMute, roomCanRemove, roomCanInvite] = [
...((roomDirectives && [
roomDirectives.allowMemberAction(room, RoomMemberActions.SET_AS_OWNER, showingUserId, userSubscription),
roomDirectives.allowMemberAction(room, RoomMemberActions.SET_AS_LEADER, showingUserId, userSubscription),
Expand All @@ -33,9 +34,10 @@ export const getRoomDirectives = ({
roomDirectives.allowMemberAction(room, RoomMemberActions.BLOCK, showingUserId, userSubscription),
roomDirectives.allowMemberAction(room, RoomMemberActions.MUTE, showingUserId, userSubscription),
roomDirectives.allowMemberAction(room, RoomMemberActions.REMOVE_USER, showingUserId, userSubscription),
roomDirectives.allowMemberAction(room, RoomMemberActions.INVITE, showingUserId, userSubscription),
]) ??
[]),
];

return { roomCanSetOwner, roomCanSetLeader, roomCanSetModerator, roomCanIgnore, roomCanBlock, roomCanMute, roomCanRemove };
return { roomCanSetOwner, roomCanSetLeader, roomCanSetModerator, roomCanIgnore, roomCanBlock, roomCanMute, roomCanRemove, roomCanInvite };
};
Loading

0 comments on commit b764c41

Please sign in to comment.