[NEW] Unify members section (#4399)

* create useUserPermissions hook

* create CheckRadioButton component

* fix return

* create MembersSection component

* apply MembersSection and header filter

* fix re-render and testID

* fix detox tests

* rename to RadioButton

* move the component closer to the screen

* remove useUserPermissions

* remove theme prop

* migrate to hooks

* fix team permissions

* remove theme prop from UserItem

* remove options prop

* fix Member

* remove commented test

* fixes

* fix for room not joined

* add room members events

* adds empty option

* add members filter and pagination

* clear RoomMembersView

* remove unused styles

* Update app/views/RoomMembersView/index.tsx

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* wip

* Temp workaround for SearchBox background color

* Rename import

* Fix missing params for 5.0

* Fix e2e tests

Co-authored-by: Diego Mello <diegolmello@gmail.com>
This commit is contained in:
Gleidson Daniel Silva 2022-08-26 10:21:25 -03:00 committed by GitHub
parent ccbc84f9a8
commit cbc6892084
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 752 additions and 721 deletions

View File

@ -0,0 +1,16 @@
import React from 'react';
import { RadioButton as RadioButtonUiLib } from 'react-native-ui-lib';
import { useTheme } from '../../theme';
export const RadioButton = ({ check, testID, size }: { check: boolean; testID?: string; size?: number }): React.ReactElement => {
const { colors } = useTheme();
return (
<RadioButtonUiLib
testID={testID}
selected={check}
size={size || 20}
color={check ? colors.tintActive : colors.auxiliaryTintColor}
/>
);
};

View File

@ -4,9 +4,8 @@ import { Pressable, StyleProp, StyleSheet, Text, View, ViewStyle } from 'react-n
import Avatar from './Avatar'; import Avatar from './Avatar';
import { CustomIcon, TIconsName } from './CustomIcon'; import { CustomIcon, TIconsName } from './CustomIcon';
import sharedStyles from '../views/Styles'; import sharedStyles from '../views/Styles';
import { themes } from '../lib/constants';
import { isIOS } from '../lib/methods/helpers'; import { isIOS } from '../lib/methods/helpers';
import { TSupportedThemes } from '../theme'; import { useTheme } from '../theme';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
button: { button: {
@ -47,34 +46,36 @@ interface IUserItem {
onLongPress?: () => void; onLongPress?: () => void;
style?: StyleProp<ViewStyle>; style?: StyleProp<ViewStyle>;
icon?: TIconsName | null; icon?: TIconsName | null;
theme: TSupportedThemes;
} }
const UserItem = ({ name, username, onPress, testID, onLongPress, style, icon, theme }: IUserItem) => ( const UserItem = ({ name, username, onPress, testID, onLongPress, style, icon }: IUserItem): React.ReactElement => {
const { colors } = useTheme();
return (
<Pressable <Pressable
onPress={onPress} onPress={onPress}
onLongPress={onLongPress} onLongPress={onLongPress}
testID={testID} testID={testID}
android_ripple={{ android_ripple={{
color: themes[theme].bannerBackground color: colors.bannerBackground
}} }}
style={({ pressed }: any) => ({ style={({ pressed }: any) => ({
backgroundColor: isIOS && pressed ? themes[theme].bannerBackground : 'transparent' backgroundColor: isIOS && pressed ? colors.bannerBackground : 'transparent'
})} })}
> >
<View style={[styles.container, styles.button, style]}> <View style={[styles.container, styles.button, style]}>
<Avatar text={username} size={30} style={styles.avatar} /> <Avatar text={username} size={30} style={styles.avatar} />
<View style={styles.textContainer}> <View style={styles.textContainer}>
<Text style={[styles.name, { color: themes[theme].titleText }]} numberOfLines={1}> <Text style={[styles.name, { color: colors.titleText }]} numberOfLines={1}>
{name} {name}
</Text> </Text>
<Text style={[styles.username, { color: themes[theme].auxiliaryText }]} numberOfLines={1}> <Text style={[styles.username, { color: colors.auxiliaryText }]} numberOfLines={1}>
@{username} @{username}
</Text> </Text>
</View> </View>
{icon ? <CustomIcon name={icon} size={22} color={themes[theme].actionTintColor} style={styles.icon} /> : null} {icon ? <CustomIcon name={icon} size={22} color={colors.actionTintColor} style={styles.icon} /> : null}
</View> </View>
</Pressable> </Pressable>
); );
};
export default UserItem; export default UserItem;

View File

@ -356,6 +356,7 @@
"No_mentioned_messages": "No mentioned messages", "No_mentioned_messages": "No mentioned messages",
"No_pinned_messages": "No pinned messages", "No_pinned_messages": "No pinned messages",
"No_results_found": "No results found", "No_results_found": "No results found",
"No_members_found": "No members found",
"No_starred_messages": "No starred messages", "No_starred_messages": "No starred messages",
"No_thread_messages": "No thread messages", "No_thread_messages": "No thread messages",
"No_label_provided": "No {{label}} provided.", "No_label_provided": "No {{label}} provided.",

View File

@ -334,6 +334,7 @@
"No_mentioned_messages": "Não há menções", "No_mentioned_messages": "Não há menções",
"No_pinned_messages": "Não há mensagens fixadas", "No_pinned_messages": "Não há mensagens fixadas",
"No_results_found": "Nenhum resultado encontrado", "No_results_found": "Nenhum resultado encontrado",
"No_members_found": "Nenhum usuário encontrado",
"No_starred_messages": "Não há mensagens favoritas", "No_starred_messages": "Não há mensagens favoritas",
"No_thread_messages": "Não há tópicos", "No_thread_messages": "Não há tópicos",
"No_label_provided": "Sem {{label}}.", "No_label_provided": "Sem {{label}}.",

View File

@ -85,7 +85,7 @@ export function hasRole(role): boolean {
return userRoles.indexOf(role) > -1; return userRoles.indexOf(role) > -1;
} }
export async function hasPermission(permissions, rid?: any): boolean[] { export async function hasPermission(permissions, rid?: any): Promise<boolean[]> {
let roomRoles = []; let roomRoles = [];
if (rid) { if (rid) {
const db = database.active; const db = database.active;

View File

@ -272,6 +272,10 @@ export default {
RA_MOVE_TO_TEAM_F: 'ra_move_to_team_f', RA_MOVE_TO_TEAM_F: 'ra_move_to_team_f',
RA_SEARCH_TEAM: 'ra_search_team', RA_SEARCH_TEAM: 'ra_search_team',
// ROOM MEMBERS ACTIONS VIEW
RM_GO_SELECTEDUSERS: 'rm_go_selected_users',
RM_GO_INVITEUSERS: 'rm_go_invite_users',
// ROOM INFO VIEW // ROOM INFO VIEW
RI_GO_RI_EDIT: 'ri_go_ri_edit', RI_GO_RI_EDIT: 'ri_go_ri_edit',
RI_GO_LIVECHAT_EDIT: 'ri_go_livechat_edit', RI_GO_LIVECHAT_EDIT: 'ri_go_livechat_edit',

View File

@ -17,10 +17,15 @@ function replace(name: string, params: any) {
navigationRef.current?.dispatch(StackActions.replace(name, params)); navigationRef.current?.dispatch(StackActions.replace(name, params));
} }
function popToTop() {
navigationRef.current?.dispatch(StackActions.popToTop());
}
export default { export default {
navigationRef, navigationRef,
routeNameRef, routeNameRef,
navigate, navigate,
back, back,
replace replace,
popToTop
}; };

View File

@ -95,7 +95,7 @@ const ChatsStackNavigator = () => {
<ChatsStack.Screen name='SelectListView' component={SelectListView} options={SelectListView.navigationOptions} /> <ChatsStack.Screen name='SelectListView' component={SelectListView} options={SelectListView.navigationOptions} />
<ChatsStack.Screen name='RoomInfoView' component={RoomInfoView} options={RoomInfoView.navigationOptions} /> <ChatsStack.Screen name='RoomInfoView' component={RoomInfoView} options={RoomInfoView.navigationOptions} />
<ChatsStack.Screen name='RoomInfoEditView' component={RoomInfoEditView} options={RoomInfoEditView.navigationOptions} /> <ChatsStack.Screen name='RoomInfoEditView' component={RoomInfoEditView} options={RoomInfoEditView.navigationOptions} />
<ChatsStack.Screen name='RoomMembersView' component={RoomMembersView} options={RoomMembersView.navigationOptions} /> <ChatsStack.Screen name='RoomMembersView' component={RoomMembersView} />
<ChatsStack.Screen name='DiscussionsView' component={DiscussionsView} /> <ChatsStack.Screen name='DiscussionsView' component={DiscussionsView} />
<ChatsStack.Screen <ChatsStack.Screen
name='SearchMessagesView' name='SearchMessagesView'

View File

@ -127,7 +127,7 @@ const ModalStackNavigator = React.memo(({ navigation }: INavigation) => {
<ModalStack.Screen name='RoomInfoView' component={RoomInfoView} options={RoomInfoView.navigationOptions} /> <ModalStack.Screen name='RoomInfoView' component={RoomInfoView} options={RoomInfoView.navigationOptions} />
<ModalStack.Screen name='SelectListView' component={SelectListView} /> <ModalStack.Screen name='SelectListView' component={SelectListView} />
<ModalStack.Screen name='RoomInfoEditView' component={RoomInfoEditView} options={RoomInfoEditView.navigationOptions} /> <ModalStack.Screen name='RoomInfoEditView' component={RoomInfoEditView} options={RoomInfoEditView.navigationOptions} />
<ModalStack.Screen name='RoomMembersView' component={RoomMembersView} options={RoomMembersView.navigationOptions} /> <ModalStack.Screen name='RoomMembersView' component={RoomMembersView} />
<ModalStack.Screen <ModalStack.Screen
name='SearchMessagesView' name='SearchMessagesView'
component={SearchMessagesView} component={SearchMessagesView}

View File

@ -64,6 +64,7 @@ export type ModalStackParamList = {
RoomMembersView: { RoomMembersView: {
rid: string; rid: string;
room: TSubscriptionModel; room: TSubscriptionModel;
joined?: boolean;
}; };
DiscussionsView: { DiscussionsView: {
rid: string; rid: string;

View File

@ -75,6 +75,7 @@ export type ChatsStackParamList = {
RoomMembersView: { RoomMembersView: {
rid: string; rid: string;
room: ISubscription; room: ISubscription;
joined?: boolean;
}; };
DiscussionsView: { DiscussionsView: {
rid: string; rid: string;

View File

@ -331,20 +331,15 @@ class CreateChannelView extends React.Component<ICreateChannelViewProps, ICreate
}); });
} }
renderItem = ({ item }: { item: IOtherUser }) => { renderItem = ({ item }: { item: IOtherUser }) => (
const { theme } = this.props;
return (
<UserItem <UserItem
name={item.fname} name={item.fname}
username={item.name} username={item.name}
onPress={() => this.removeUser(item)} onPress={() => this.removeUser(item)}
testID={`create-channel-view-item-${item.name}`} testID={`create-channel-view-item-${item.name}`}
icon='check' icon='check'
theme={theme}
/> />
); );
};
renderInvitedList = () => { renderInvitedList = () => {
const { users, theme } = this.props; const { users, theme } = this.props;

View File

@ -290,7 +290,6 @@ class NewMessageView extends React.Component<INewMessageViewProps, INewMessageVi
onPress={() => this.goRoom(itemModel)} onPress={() => this.goRoom(itemModel)}
testID={`new-message-view-item-${item.name}`} testID={`new-message-view-item-${item.name}`}
style={style} style={style}
theme={theme}
/> />
); );
}; };

View File

@ -64,10 +64,6 @@ interface IRoomActionsViewProps extends IActionSheetProvider, IBaseScreen<ChatsS
encryptionEnabled: boolean; encryptionEnabled: boolean;
fontScale: number; fontScale: number;
serverVersion: string | null; serverVersion: string | null;
addUserToJoinedRoomPermission?: string[];
addUserToAnyCRoomPermission?: string[];
addUserToAnyPRoomPermission?: string[];
createInviteLinksPermission?: string[];
editRoomPermission?: string[]; editRoomPermission?: string[];
toggleRoomE2EEncryptionPermission?: string[]; toggleRoomE2EEncryptionPermission?: string[];
viewBroadcastMemberListPermission?: string[]; viewBroadcastMemberListPermission?: string[];
@ -94,8 +90,6 @@ interface IRoomActionsViewState {
joined: boolean; joined: boolean;
canViewMembers: boolean; canViewMembers: boolean;
canAutoTranslate: boolean; canAutoTranslate: boolean;
canAddUser: boolean;
canInviteUser: boolean;
canEdit: boolean; canEdit: boolean;
canToggleEncryption: boolean; canToggleEncryption: boolean;
canCreateTeam: boolean; canCreateTeam: boolean;
@ -146,8 +140,6 @@ class RoomActionsView extends React.Component<IRoomActionsViewProps, IRoomAction
joined: !!room, joined: !!room,
canViewMembers: false, canViewMembers: false,
canAutoTranslate: false, canAutoTranslate: false,
canAddUser: false,
canInviteUser: false,
canEdit: false, canEdit: false,
canToggleEncryption: false, canToggleEncryption: false,
canCreateTeam: false, canCreateTeam: false,
@ -206,8 +198,6 @@ class RoomActionsView extends React.Component<IRoomActionsViewProps, IRoomAction
} }
const canAutoTranslate = canAutoTranslateMethod(); const canAutoTranslate = canAutoTranslateMethod();
const canAddUser = await this.canAddUser();
const canInviteUser = await this.canInviteUser();
const canEdit = await this.canEdit(); const canEdit = await this.canEdit();
const canToggleEncryption = await this.canToggleEncryption(); const canToggleEncryption = await this.canToggleEncryption();
const canViewMembers = await this.canViewMembers(); const canViewMembers = await this.canViewMembers();
@ -217,8 +207,6 @@ class RoomActionsView extends React.Component<IRoomActionsViewProps, IRoomAction
this.setState({ this.setState({
canAutoTranslate, canAutoTranslate,
canAddUser,
canInviteUser,
canEdit, canEdit,
canToggleEncryption, canToggleEncryption,
canViewMembers, canViewMembers,
@ -261,40 +249,6 @@ class RoomActionsView extends React.Component<IRoomActionsViewProps, IRoomAction
} }
}; };
canAddUser = async () => {
const { room, joined } = this.state;
const { addUserToJoinedRoomPermission, addUserToAnyCRoomPermission, addUserToAnyPRoomPermission } = this.props;
const { rid, t } = room;
let canAddUser = false;
const userInRoom = joined;
const permissions = await hasPermission(
[addUserToJoinedRoomPermission, addUserToAnyCRoomPermission, addUserToAnyPRoomPermission],
rid
);
if (userInRoom && permissions[0]) {
canAddUser = true;
}
if (t === 'c' && permissions[1]) {
canAddUser = true;
}
if (t === 'p' && permissions[2]) {
canAddUser = true;
}
return canAddUser;
};
canInviteUser = async () => {
const { room } = this.state;
const { createInviteLinksPermission } = this.props;
const { rid } = room;
const permissions = await hasPermission([createInviteLinksPermission], rid);
const canInviteUser = permissions[0];
return canInviteUser;
};
canEdit = async () => { canEdit = async () => {
const { room } = this.state; const { room } = this.state;
const { editRoomPermission } = this.props; const { editRoomPermission } = this.props;
@ -1135,7 +1089,7 @@ class RoomActionsView extends React.Component<IRoomActionsViewProps, IRoomAction
}; };
render() { render() {
const { room, membersCount, canViewMembers, canAddUser, canInviteUser, joined, canAutoTranslate } = this.state; const { room, membersCount, canViewMembers, joined, canAutoTranslate } = this.state;
const { rid, t, prid } = room; const { rid, t, prid } = room;
const isGroupChatHandler = isGroupChat(room); const isGroupChatHandler = isGroupChat(room);
@ -1154,7 +1108,7 @@ class RoomActionsView extends React.Component<IRoomActionsViewProps, IRoomAction
<List.Item <List.Item
title='Members' title='Members'
subtitle={membersCount > 0 ? `${membersCount} ${I18n.t('members')}` : undefined} subtitle={membersCount > 0 ? `${membersCount} ${I18n.t('members')}` : undefined}
onPress={() => this.onPressTouchable({ route: 'RoomMembersView', params: { rid, room } })} onPress={() => this.onPressTouchable({ route: 'RoomMembersView', params: { rid, room, joined: this.joined } })}
testID='room-actions-members' testID='room-actions-members'
left={() => <List.Icon name='team' />} left={() => <List.Icon name='team' />}
showActionIndicator showActionIndicator
@ -1164,45 +1118,6 @@ class RoomActionsView extends React.Component<IRoomActionsViewProps, IRoomAction
</> </>
) : null} ) : null}
{['c', 'p'].includes(t) && canAddUser ? (
<>
<List.Item
title='Add_users'
onPress={() =>
this.onPressTouchable({
route: 'SelectedUsersView',
params: {
title: I18n.t('Add_users'),
nextAction: this.addUser
}
})
}
testID='room-actions-add-user'
left={() => <List.Icon name='add' />}
showActionIndicator
/>
<List.Separator />
</>
) : null}
{['c', 'p'].includes(t) && canInviteUser ? (
<>
<List.Item
title='Invite_users'
onPress={() =>
this.onPressTouchable({
route: 'InviteUsersView',
params: { rid }
})
}
testID='room-actions-invite-user'
left={() => <List.Icon name='user-add' />}
showActionIndicator
/>
<List.Separator />
</>
) : null}
{['c', 'p', 'd'].includes(t) && !prid ? ( {['c', 'p', 'd'].includes(t) && !prid ? (
<> <>
<List.Item <List.Item
@ -1384,10 +1299,6 @@ const mapStateToProps = (state: IApplicationState) => ({
encryptionEnabled: state.encryption.enabled, encryptionEnabled: state.encryption.enabled,
serverVersion: state.server.version, serverVersion: state.server.version,
isMasterDetail: state.app.isMasterDetail, isMasterDetail: state.app.isMasterDetail,
addUserToJoinedRoomPermission: state.permissions['add-user-to-joined-room'],
addUserToAnyCRoomPermission: state.permissions['add-user-to-any-c-room'],
addUserToAnyPRoomPermission: state.permissions['add-user-to-any-p-room'],
createInviteLinksPermission: state.permissions['create-invite-links'],
editRoomPermission: state.permissions['edit-room'], editRoomPermission: state.permissions['edit-room'],
toggleRoomE2EEncryptionPermission: state.permissions['toggle-room-e2e-encryption'], toggleRoomE2EEncryptionPermission: state.permissions['toggle-room-e2e-encryption'],
viewBroadcastMemberListPermission: state.permissions['view-broadcast-member-list'], viewBroadcastMemberListPermission: state.permissions['view-broadcast-member-list'],

View File

@ -0,0 +1,109 @@
import { CompositeNavigationProp, useNavigation } from '@react-navigation/native';
import { StackNavigationProp } from '@react-navigation/stack';
import React from 'react';
import { View } from 'react-native';
import { useDispatch } from 'react-redux';
import { setLoading } from '../../../actions/selectedUsers';
import * as List from '../../../containers/List';
import { TSubscriptionModel } from '../../../definitions';
import i18n from '../../../i18n';
import { usePermissions } from '../../../lib/hooks';
import log, { events, logEvent } from '../../../lib/methods/helpers/log';
import { Services } from '../../../lib/services';
import { MasterDetailInsideStackParamList } from '../../../stacks/MasterDetailStack/types';
import { ChatsStackParamList } from '../../../stacks/types';
type TNavigation = CompositeNavigationProp<
StackNavigationProp<ChatsStackParamList, 'RoomActionsView'>,
StackNavigationProp<MasterDetailInsideStackParamList>
>;
interface IActionsSection {
rid: TSubscriptionModel['rid'];
t: TSubscriptionModel['t'];
joined: boolean;
}
export default function ActionsSection({ rid, t, joined }: IActionsSection): React.ReactElement {
const { navigate, pop } = useNavigation<TNavigation>();
const dispatch = useDispatch();
const [addUserToJoinedRoomPermission, addUserToAnyCRoomPermission, addUserToAnyPRoomPermission, createInviteLinksPermission] =
usePermissions(['add-user-to-joined-room', 'add-user-to-any-c-room', 'add-user-to-any-p-room', 'create-invite-links'], rid);
const canAddUser =
(joined && addUserToJoinedRoomPermission) ||
(t === 'c' && addUserToAnyCRoomPermission) ||
(t === 'p' && addUserToAnyPRoomPermission) ||
false;
const canInviteUser = createInviteLinksPermission;
const handleOnPress = ({
route,
params
}: {
route: keyof ChatsStackParamList;
params: ChatsStackParamList[keyof ChatsStackParamList];
}) => {
navigate(route, params);
// @ts-ignore
logEvent(events[`RM_GO_${route.replace('View', '').toUpperCase()}`]);
};
const addUser = async () => {
try {
dispatch(setLoading(true));
await Services.addUsersToRoom(rid);
pop();
} catch (e) {
log(e);
} finally {
dispatch(setLoading(false));
}
};
return (
<View style={{ paddingTop: canAddUser || canInviteUser ? 16 : 0, paddingBottom: canAddUser || canInviteUser ? 8 : 0 }}>
{['c', 'p'].includes(t) && canAddUser ? (
<>
<List.Separator />
<List.Item
title='Add_users'
onPress={() =>
handleOnPress({
route: 'SelectedUsersView',
params: {
title: i18n.t('Add_users'),
nextAction: addUser
}
})
}
testID='room-actions-add-user'
left={() => <List.Icon name='add' />}
showActionIndicator
/>
<List.Separator />
</>
) : null}
{['c', 'p'].includes(t) && canInviteUser ? (
<>
<List.Item
title='Invite_users'
onPress={() =>
handleOnPress({
route: 'InviteUsersView',
params: { rid }
})
}
testID='room-actions-invite-user'
left={() => <List.Icon name='user-add' />}
showActionIndicator
/>
<List.Separator />
</>
) : null}
</View>
);
}

View File

@ -0,0 +1,258 @@
import { Q } from '@nozbe/watermelondb';
import { LISTENER } from '../../containers/Toast';
import { IUser, SubscriptionType, TSubscriptionModel, TUserModel } from '../../definitions';
import I18n from '../../i18n';
import { getRoomTitle, showConfirmationAlert, showErrorAlert } from '../../lib/methods/helpers';
import EventEmitter from '../../lib/methods/helpers/events';
import { goRoom, TGoRoomItem } from '../../lib/methods/helpers/goRoom';
import log from '../../lib/methods/helpers/log';
import appNavigation from '../../lib/navigation/appNavigation';
import { Services } from '../../lib/services';
import database from '../../lib/database';
import { RoomTypes } from '../../lib/methods';
export type TRoomType = SubscriptionType.CHANNEL | SubscriptionType.GROUP | SubscriptionType.OMNICHANNEL;
const handleGoRoom = (item: TGoRoomItem, isMasterDetail: boolean): void => {
if (isMasterDetail) {
appNavigation.navigate('DrawerNavigator');
} else {
appNavigation.popToTop();
}
goRoom({ item, isMasterDetail });
};
export const fetchRole = (role: string, selectedUser: TUserModel, roomRoles: any): boolean => {
const userRoleResult = roomRoles.find((r: any) => r.u._id === selectedUser._id);
return userRoleResult?.roles.includes(role);
};
export const fetchRoomMembersRoles = async (roomType: TRoomType, rid: string, updateState: any): Promise<void> => {
try {
const type = roomType;
const result = await Services.getRoomRoles(rid, type);
if (result?.success) {
updateState({ roomRoles: result.roles });
}
} catch (e) {
log(e);
}
};
export const handleMute = async (user: TUserModel, rid: string) => {
try {
await Services.toggleMuteUserInRoom(rid, user?.username, !user?.muted);
EventEmitter.emit(LISTENER, {
message: I18n.t('User_has_been_key', { key: user?.muted ? I18n.t('unmuted') : I18n.t('muted') })
});
} catch (e) {
log(e);
}
};
export const handleModerator = async (
selectedUser: TUserModel,
isModerator: boolean,
room: TSubscriptionModel,
username: string,
callback: () => Promise<void>
): Promise<void> => {
try {
await Services.toggleRoomModerator({
roomId: room.rid,
t: room.t,
userId: selectedUser._id,
isModerator
});
const message = isModerator
? 'User__username__is_now_a_moderator_of__room_name_'
: 'User__username__removed_from__room_name__moderators';
EventEmitter.emit(LISTENER, {
message: I18n.t(message, {
username,
room_name: getRoomTitle(room)
})
});
callback();
} catch (e) {
log(e);
}
};
export const navToDirectMessage = async (item: IUser, isMasterDetail: boolean): Promise<void> => {
try {
const db = database.active;
const subsCollection = db.get('subscriptions');
const query = await subsCollection.query(Q.where('name', item.username)).fetch();
if (query.length) {
const [room] = query;
handleGoRoom(room, isMasterDetail);
} else {
const result = await Services.createDirectMessage(item.username);
if (result.success) {
handleGoRoom({ rid: result.room?._id as string, name: item.username, t: SubscriptionType.DIRECT }, isMasterDetail);
}
}
} catch (e) {
log(e);
}
};
const removeFromTeam = async (
selectedUser: IUser,
updateState: Function,
room: TSubscriptionModel,
members: TUserModel[],
selected?: any
) => {
try {
const userId = selectedUser._id;
const result = await Services.removeTeamMember({
teamId: room.teamId,
userId,
...(selected && { rooms: selected })
});
if (result.success) {
const message = I18n.t('User_has_been_removed_from_s', { s: getRoomTitle(room) });
EventEmitter.emit(LISTENER, { message });
const newMembers = members.filter(member => member._id !== userId);
updateState({
members: newMembers
});
}
} catch (e: any) {
log(e);
showErrorAlert(
e.data.error ? I18n.t(e.data.error) : I18n.t('There_was_an_error_while_action', { action: I18n.t('removing_team') }),
I18n.t('Cannot_remove')
);
}
};
export const handleRemoveFromTeam = async (
selectedUser: TUserModel,
updateState: Function,
room: TSubscriptionModel,
members: TUserModel[]
): Promise<void> => {
try {
const result = await Services.teamListRoomsOfUser({ teamId: room.teamId as string, userId: selectedUser._id });
if (result.success) {
if (result.rooms?.length) {
const teamChannels = result.rooms.map((r: any) => ({
rid: r._id,
name: r.name,
teamId: r.teamId,
alert: r.isLastOwner
}));
appNavigation.navigate('SelectListView', {
title: 'Remove_Member',
infoText: 'Remove_User_Team_Channels',
data: teamChannels,
nextAction: (selected: any) => removeFromTeam(selectedUser, updateState, room, members, selected),
showAlert: () => showErrorAlert(I18n.t('Last_owner_team_room'), I18n.t('Cannot_remove'))
});
} else {
showConfirmationAlert({
message: I18n.t('Removing_user_from_this_team', { user: selectedUser.username }),
confirmationText: I18n.t('Yes_action_it', { action: I18n.t('remove') }),
onPress: () => removeFromTeam(selectedUser, updateState, room, members)
});
}
}
} catch (e) {
showConfirmationAlert({
message: I18n.t('Removing_user_from_this_team', { user: selectedUser.username }),
confirmationText: I18n.t('Yes_action_it', { action: I18n.t('remove') }),
onPress: () => removeFromTeam(selectedUser, updateState, room, members)
});
}
};
export const handleLeader = async (
selectedUser: TUserModel,
isLeader: boolean,
room: TSubscriptionModel,
username: string,
callback: () => Promise<void>
): Promise<void> => {
try {
await Services.toggleRoomLeader({
roomId: room.rid,
t: room.t,
userId: selectedUser._id,
isLeader
});
const message = isLeader
? 'User__username__is_now_a_leader_of__room_name_'
: 'User__username__removed_from__room_name__leaders';
EventEmitter.emit(LISTENER, {
message: I18n.t(message, {
username,
room_name: getRoomTitle(room)
})
});
callback();
} catch (e) {
log(e);
}
};
export const handleRemoveUserFromRoom = async (
selectedUser: TUserModel,
room: TSubscriptionModel,
callback: Function
): Promise<void> => {
try {
const userId = selectedUser._id;
await Services.removeUserFromRoom({ roomId: room.rid, t: room.t as RoomTypes, userId });
const message = I18n.t('User_has_been_removed_from_s', { s: getRoomTitle(room) });
EventEmitter.emit(LISTENER, { message });
callback();
} catch (e) {
log(e);
}
};
export const handleIgnore = async (selectedUser: TUserModel, ignore: boolean, rid: string) => {
try {
await Services.ignoreUser({
rid,
userId: selectedUser._id,
ignore
});
const message = I18n.t(ignore ? 'User_has_been_ignored' : 'User_has_been_unignored');
EventEmitter.emit(LISTENER, { message });
} catch (e) {
log(e);
}
};
export const handleOwner = async (
selectedUser: TUserModel,
isOwner: boolean,
username: string,
room: TSubscriptionModel,
callback: Function
): Promise<void> => {
try {
await Services.toggleRoomOwner({
roomId: room.rid,
t: room.t,
userId: selectedUser._id,
isOwner
});
const message = isOwner ? 'User__username__is_now_a_owner_of__room_name_' : 'User__username__removed_from__room_name__owners';
EventEmitter.emit(LISTENER, {
message: I18n.t(message, {
username,
room_name: getRoomTitle(room)
})
});
} catch (e) {
log(e);
}
callback();
};

View File

@ -1,123 +1,103 @@
import { Q } from '@nozbe/watermelondb'; import { NavigationProp, RouteProp, useNavigation, useRoute } from '@react-navigation/native';
import React from 'react'; import React, { useEffect, useReducer } from 'react';
import { FlatList } from 'react-native'; import { FlatList, Text, View } from 'react-native';
import { connect } from 'react-redux';
import { Observable, Subscription } from 'rxjs';
import { themes } from '../../lib/constants'; import { TActionSheetOptionsItem, useActionSheet } from '../../containers/ActionSheet';
import { TActionSheetOptions, TActionSheetOptionsItem, withActionSheet } from '../../containers/ActionSheet';
import ActivityIndicator from '../../containers/ActivityIndicator'; import ActivityIndicator from '../../containers/ActivityIndicator';
import { CustomIcon } from '../../containers/CustomIcon';
import * as HeaderButton from '../../containers/HeaderButton'; import * as HeaderButton from '../../containers/HeaderButton';
import * as List from '../../containers/List'; import * as List from '../../containers/List';
import { RadioButton } from '../../containers/RadioButton';
import SafeAreaView from '../../containers/SafeAreaView'; import SafeAreaView from '../../containers/SafeAreaView';
import SearchBox from '../../containers/SearchBox'; import SearchBox from '../../containers/SearchBox';
import StatusBar from '../../containers/StatusBar'; import StatusBar from '../../containers/StatusBar';
import { LISTENER } from '../../containers/Toast';
import { IApplicationState, IBaseScreen, IUser, SubscriptionType, TSubscriptionModel, TUserModel } from '../../definitions';
import I18n from '../../i18n';
import database from '../../lib/database';
import { CustomIcon } from '../../containers/CustomIcon';
import UserItem from '../../containers/UserItem'; import UserItem from '../../containers/UserItem';
import { getUserSelector } from '../../selectors/login'; import { TSubscriptionModel, TUserModel } from '../../definitions';
import { ModalStackParamList } from '../../stacks/MasterDetailStack/types'; import I18n from '../../i18n';
import { TSupportedThemes, withTheme } from '../../theme'; import { useAppSelector, usePermissions } from '../../lib/hooks';
import EventEmitter from '../../lib/methods/helpers/events'; import { getRoomTitle, isGroupChat } from '../../lib/methods/helpers';
import { goRoom, TGoRoomItem } from '../../lib/methods/helpers/goRoom'; import { showConfirmationAlert } from '../../lib/methods/helpers/info';
import { showConfirmationAlert, showErrorAlert } from '../../lib/methods/helpers/info';
import log from '../../lib/methods/helpers/log'; import log from '../../lib/methods/helpers/log';
import scrollPersistTaps from '../../lib/methods/helpers/scrollPersistTaps'; import scrollPersistTaps from '../../lib/methods/helpers/scrollPersistTaps';
import { TSupportedPermissions } from '../../reducers/permissions';
import { RoomTypes } from '../../lib/methods';
import { compareServerVersion, debounce, getRoomTitle, hasPermission, isGroupChat } from '../../lib/methods/helpers';
import styles from './styles';
import { Services } from '../../lib/services'; import { Services } from '../../lib/services';
import { TSupportedPermissions } from '../../reducers/permissions';
import { getUserSelector } from '../../selectors/login';
import { ModalStackParamList } from '../../stacks/MasterDetailStack/types';
import { useTheme } from '../../theme';
import ActionsSection from './components/ActionsSection';
import {
fetchRole,
fetchRoomMembersRoles,
handleIgnore,
handleLeader,
handleModerator,
handleMute,
handleOwner,
handleRemoveFromTeam,
handleRemoveUserFromRoom,
navToDirectMessage,
TRoomType
} from './helpers';
import styles from './styles';
const PAGE_SIZE = 25; const PAGE_SIZE = 25;
interface IRoomMembersViewProps extends IBaseScreen<ModalStackParamList, 'RoomMembersView'> {
rid: string;
members: string[];
baseUrl: string;
room: TSubscriptionModel;
user: {
id: string;
token: string;
roles: string[];
};
showActionSheet: (params: TActionSheetOptions) => {};
theme: TSupportedThemes;
isMasterDetail: boolean;
useRealName: boolean;
muteUserPermission: string[];
setLeaderPermission: string[];
setOwnerPermission: string[];
setModeratorPermission: string[];
removeUserPermission: string[];
editTeamMemberPermission: string[];
viewAllTeamChannelsPermission: string[];
viewAllTeamsPermission: string[];
serverVersion: string;
}
interface IRoomMembersViewState { interface IRoomMembersViewState {
isLoading: boolean; isLoading: boolean;
allUsers: boolean; allUsers: boolean;
filtering: string; filtering: string;
rid: string;
members: TUserModel[]; members: TUserModel[];
membersFiltered: TUserModel[];
room: TSubscriptionModel; room: TSubscriptionModel;
end: boolean; end: boolean;
roomRoles: any;
filter: string;
page: number; page: number;
} }
class RoomMembersView extends React.Component<IRoomMembersViewProps, IRoomMembersViewState> { const RightIcon = ({ check, label }: { check: boolean; label: string }) => {
private mounted: boolean; const { colors } = useTheme();
private permissions: { [key in TSupportedPermissions]?: boolean }; return (
private roomObservable!: Observable<TSubscriptionModel>; <CustomIcon
private subscription!: Subscription; testID={check ? `action-sheet-set-${label}-checked` : `action-sheet-set-${label}-unchecked`}
private roomRoles: any; name={check ? 'checkbox-checked' : 'checkbox-unchecked'}
size={20}
color={check ? colors.tintActive : colors.auxiliaryTintColor}
/>
);
};
constructor(props: IRoomMembersViewProps) { const RoomMembersView = (): React.ReactElement => {
super(props); const { showActionSheet } = useActionSheet();
this.mounted = false; const { colors } = useTheme();
this.permissions = {};
const rid = props.route.params?.rid; const { params } = useRoute<RouteProp<ModalStackParamList, 'RoomMembersView'>>();
const room = props.route.params?.room; const navigation = useNavigation<NavigationProp<ModalStackParamList, 'RoomMembersView'>>();
this.state = {
const isMasterDetail = useAppSelector(state => state.app.isMasterDetail);
const useRealName = useAppSelector(state => state.settings.UI_Use_Real_Name);
const user = useAppSelector(state => getUserSelector(state));
const [state, updateState] = useReducer(
(state: IRoomMembersViewState, newState: Partial<IRoomMembersViewState>) => ({ ...state, ...newState }),
{
isLoading: false, isLoading: false,
allUsers: false, allUsers: false,
filtering: '', filtering: '',
rid,
members: [], members: [],
membersFiltered: [], room: params.room || ({} as TSubscriptionModel),
room: room || ({} as TSubscriptionModel),
end: false, end: false,
roomRoles: null,
filter: '',
page: 0 page: 0
};
if (room && room.observe) {
this.roomObservable = room.observe();
this.subscription = this.roomObservable.subscribe(changes => {
if (this.mounted) {
this.setState({ room: changes });
} else {
this.setState({ room: changes });
}
});
}
this.setHeader();
} }
);
async componentDidMount() { const teamPermissions: TSupportedPermissions[] = state.room.teamMain
const { room } = this.state; ? ['edit-team-member', 'view-all-team-channels', 'view-all-teams']
this.mounted = true; : [];
this.fetchMembers();
if (isGroupChat(room)) { const [
return;
}
const {
muteUserPermission, muteUserPermission,
setLeaderPermission, setLeaderPermission,
setOwnerPermission, setOwnerPermission,
@ -126,186 +106,97 @@ class RoomMembersView extends React.Component<IRoomMembersViewProps, IRoomMember
editTeamMemberPermission, editTeamMemberPermission,
viewAllTeamChannelsPermission, viewAllTeamChannelsPermission,
viewAllTeamsPermission viewAllTeamsPermission
} = this.props; ] = usePermissions(['mute-user', 'set-leader', 'set-owner', 'set-moderator', 'remove-user', ...teamPermissions], params.rid);
const result = await hasPermission( useEffect(() => {
[ const subscription = params?.room?.observe && params.room.observe().subscribe(changes => updateState({ room: changes }));
setHeader(true);
fetchMembers(true);
return () => subscription?.unsubscribe();
}, []);
useEffect(() => {
const fetchRoles = () => {
if (isGroupChat(state.room)) {
return;
}
if (
muteUserPermission ||
setLeaderPermission ||
setOwnerPermission ||
setModeratorPermission ||
removeUserPermission ||
editTeamMemberPermission ||
viewAllTeamChannelsPermission ||
viewAllTeamsPermission
) {
fetchRoomMembersRoles(state.room.t as any, state.room.rid, updateState);
}
};
fetchRoles();
}, [
muteUserPermission, muteUserPermission,
setLeaderPermission, setLeaderPermission,
setOwnerPermission, setOwnerPermission,
setModeratorPermission, setModeratorPermission,
removeUserPermission, removeUserPermission,
...(room.teamMain ? [editTeamMemberPermission, viewAllTeamChannelsPermission, viewAllTeamsPermission] : []) editTeamMemberPermission,
], viewAllTeamChannelsPermission,
room.rid viewAllTeamsPermission
); ]);
this.permissions = { const toggleStatus = (status: boolean) => {
'mute-user': result[0], try {
'set-leader': result[1], updateState({ members: [], allUsers: status, end: false });
'set-owner': result[2], fetchMembers(status);
'set-moderator': result[3], setHeader(status);
'remove-user': result[4], } catch (e) {
...(room.teamMain log(e);
? {
'edit-team-member': result[5],
'view-all-team-channels': result[6],
'view-all-teams': result[7]
} }
: {})
}; };
const hasSinglePermission = Object.values(this.permissions).some(p => !!p); const setHeader = (allUsers: boolean) => {
if (hasSinglePermission) {
this.fetchRoomMembersRoles();
}
}
componentWillUnmount() {
if (this.subscription && this.subscription.unsubscribe) {
this.subscription.unsubscribe();
}
}
setHeader = () => {
const { allUsers } = this.state;
const { navigation } = this.props;
const toggleText = allUsers ? I18n.t('Online') : I18n.t('All');
navigation.setOptions({ navigation.setOptions({
title: I18n.t('Members'), title: I18n.t('Members'),
headerRight: () => ( headerRight: () => (
<HeaderButton.Container> <HeaderButton.Container>
<HeaderButton.Item title={toggleText} onPress={this.toggleStatus} testID='room-members-view-toggle-status' /> <HeaderButton.Item
iconName='filter'
onPress={() =>
showActionSheet({
options: [
{
title: I18n.t('Online'),
onPress: () => toggleStatus(true),
right: () => <RadioButton check={allUsers} />,
testID: 'room-members-view-toggle-status-online'
},
{
title: I18n.t('All'),
onPress: () => toggleStatus(false),
right: () => <RadioButton check={!allUsers} />,
testID: 'room-members-view-toggle-status-all'
}
]
})
}
testID='room-members-view-filter'
/>
</HeaderButton.Container> </HeaderButton.Container>
) )
}); });
}; };
get isServerVersionLowerThan3_16() { const getUserDisplayName = (user: TUserModel) => (useRealName ? user.name : user.username) || user.username;
const { serverVersion } = this.props;
return compareServerVersion(serverVersion, 'lowerThan', '3.16.0');
}
onSearchChangeText = debounce((text: string) => { const onPressUser = (selectedUser: TUserModel) => {
const { members } = this.state; const { room, roomRoles, members } = state;
text = text.trim();
if (this.isServerVersionLowerThan3_16) {
let membersFiltered: TUserModel[] = [];
if (members && members.length > 0 && text) {
membersFiltered = members.filter(
m => m.username.toLowerCase().match(text.toLowerCase()) || m.name?.toLowerCase().match(text.toLowerCase())
);
}
return this.setState({ filtering: text, membersFiltered });
}
this.setState({ filtering: text, page: 0, members: [], end: false }, () => {
this.fetchMembers();
});
}, 500);
navToDirectMessage = async (item: IUser) => {
try {
const db = database.active;
const subsCollection = db.get('subscriptions');
const query = await subsCollection.query(Q.where('name', item.username)).fetch();
if (query.length) {
const [room] = query;
this.goRoom(room);
} else {
const result = await Services.createDirectMessage(item.username);
if (result.success) {
this.goRoom({ rid: result.room?._id as string, name: item.username, t: SubscriptionType.DIRECT });
}
}
} catch (e) {
log(e);
}
};
handleRemoveFromTeam = async (selectedUser: TUserModel) => {
try {
const { navigation } = this.props;
const { room } = this.state;
const result = await Services.teamListRoomsOfUser({ teamId: room.teamId as string, userId: selectedUser._id });
if (result.success) {
if (result.rooms?.length) {
const teamChannels = result.rooms.map((r: any) => ({
rid: r._id,
name: r.name,
teamId: r.teamId,
alert: r.isLastOwner
}));
navigation.navigate('SelectListView', {
title: 'Remove_Member',
infoText: 'Remove_User_Team_Channels',
data: teamChannels,
nextAction: (selected: any) => this.removeFromTeam(selectedUser, selected),
showAlert: () => showErrorAlert(I18n.t('Last_owner_team_room'), I18n.t('Cannot_remove'))
});
} else {
showConfirmationAlert({
message: I18n.t('Removing_user_from_this_team', { user: selectedUser.username }),
confirmationText: I18n.t('Yes_action_it', { action: I18n.t('remove') }),
onPress: () => this.removeFromTeam(selectedUser)
});
}
}
} catch (e) {
showConfirmationAlert({
message: I18n.t('Removing_user_from_this_team', { user: selectedUser.username }),
confirmationText: I18n.t('Yes_action_it', { action: I18n.t('remove') }),
onPress: () => this.removeFromTeam(selectedUser)
});
}
};
removeFromTeam = async (selectedUser: IUser, selected?: any) => {
try {
const { members, membersFiltered, room } = this.state;
const { navigation } = this.props;
const userId = selectedUser._id;
const result = await Services.removeTeamMember({
teamId: room.teamId,
userId,
...(selected && { rooms: selected })
});
if (result.success) {
const message = I18n.t('User_has_been_removed_from_s', { s: getRoomTitle(room) });
EventEmitter.emit(LISTENER, { message });
const newMembers = members.filter(member => member._id !== userId);
const newMembersFiltered = this.isServerVersionLowerThan3_16
? membersFiltered.filter(member => member._id !== userId)
: [];
this.setState({
members: newMembers,
membersFiltered: newMembersFiltered
});
// @ts-ignore - This is just to force a reload
navigation.navigate('RoomMembersView');
}
} catch (e: any) {
log(e);
showErrorAlert(
e.data.error ? I18n.t(e.data.error) : I18n.t('There_was_an_error_while_action', { action: I18n.t('removing_team') }),
I18n.t('Cannot_remove')
);
}
};
onPressUser = (selectedUser: TUserModel) => {
const { room } = this.state;
const { showActionSheet, user, theme } = this.props;
const options: TActionSheetOptionsItem[] = [ const options: TActionSheetOptionsItem[] = [
{ {
icon: 'message', icon: 'message',
title: I18n.t('Direct_message'), title: I18n.t('Direct_message'),
onPress: () => this.navToDirectMessage(selectedUser) onPress: () => navToDirectMessage(selectedUser, isMasterDetail)
} }
]; ];
@ -316,12 +207,12 @@ class RoomMembersView extends React.Component<IRoomMembersViewProps, IRoomMember
options.push({ options.push({
icon: 'ignore', icon: 'ignore',
title: I18n.t(isIgnored ? 'Unignore' : 'Ignore'), title: I18n.t(isIgnored ? 'Unignore' : 'Ignore'),
onPress: () => this.handleIgnore(selectedUser, !isIgnored), onPress: () => handleIgnore(selectedUser, !isIgnored, room.rid),
testID: 'action-sheet-ignore-user' testID: 'action-sheet-ignore-user'
}); });
} }
if (this.permissions['mute-user']) { if (muteUserPermission) {
const { muted = [] } = room; const { muted = [] } = room;
const userIsMuted = muted.find?.(m => m === selectedUser.username); const userIsMuted = muted.find?.(m => m === selectedUser.username);
selectedUser.muted = !!userIsMuted; selectedUser.muted = !!userIsMuted;
@ -334,7 +225,7 @@ class RoomMembersView extends React.Component<IRoomMembersViewProps, IRoomMember
roomName: getRoomTitle(room) roomName: getRoomTitle(room)
}), }),
confirmationText: I18n.t(userIsMuted ? 'Unmute' : 'Mute'), confirmationText: I18n.t(userIsMuted ? 'Unmute' : 'Mute'),
onPress: () => this.handleMute(selectedUser) onPress: () => handleMute(selectedUser, room.rid)
}); });
}, },
testID: 'action-sheet-mute-user' testID: 'action-sheet-mute-user'
@ -342,78 +233,63 @@ class RoomMembersView extends React.Component<IRoomMembersViewProps, IRoomMember
} }
// Owner // Owner
if (this.permissions['set-owner']) { if (setOwnerPermission) {
const userRoleResult = this.roomRoles.find((r: any) => r.u._id === selectedUser._id); const isOwner = fetchRole('owner', selectedUser, roomRoles);
const isOwner = userRoleResult?.roles.includes('owner');
options.push({ options.push({
icon: 'shield-check', icon: 'shield-check',
title: I18n.t('Owner'), title: I18n.t('Owner'),
onPress: () => this.handleOwner(selectedUser, !isOwner), onPress: () =>
right: () => ( handleOwner(selectedUser, !isOwner, getUserDisplayName(selectedUser), room, () =>
<CustomIcon fetchRoomMembersRoles(room.t as TRoomType, room.rid, updateState)
testID={isOwner ? 'action-sheet-set-owner-checked' : 'action-sheet-set-owner-unchecked'}
name={isOwner ? 'checkbox-checked' : 'checkbox-unchecked'}
size={20}
color={isOwner ? themes[theme].tintActive : themes[theme].auxiliaryTintColor}
/>
), ),
right: () => <RightIcon check={isOwner} label='owner' />,
testID: 'action-sheet-set-owner' testID: 'action-sheet-set-owner'
}); });
} }
// Leader // Leader
if (this.permissions['set-leader']) { if (setLeaderPermission) {
const userRoleResult = this.roomRoles.find((r: any) => r.u._id === selectedUser._id); const isLeader = fetchRole('leader', selectedUser, roomRoles);
const isLeader = userRoleResult?.roles.includes('leader');
options.push({ options.push({
icon: 'shield-alt', icon: 'shield-alt',
title: I18n.t('Leader'), title: I18n.t('Leader'),
onPress: () => this.handleLeader(selectedUser, !isLeader), onPress: () =>
right: () => ( handleLeader(selectedUser, !isLeader, room, getUserDisplayName(selectedUser), () =>
<CustomIcon fetchRoomMembersRoles(room.t as TRoomType, room.rid, updateState)
testID={isLeader ? 'action-sheet-set-leader-checked' : 'action-sheet-set-leader-unchecked'}
name={isLeader ? 'checkbox-checked' : 'checkbox-unchecked'}
size={20}
color={isLeader ? themes[theme].tintActive : themes[theme].auxiliaryTintColor}
/>
), ),
right: () => <RightIcon check={isLeader} label='leader' />,
testID: 'action-sheet-set-leader' testID: 'action-sheet-set-leader'
}); });
} }
// Moderator // Moderator
if (this.permissions['set-moderator']) { if (setModeratorPermission) {
const userRoleResult = this.roomRoles.find((r: any) => r.u._id === selectedUser._id); const isModerator = fetchRole('moderator', selectedUser, roomRoles);
const isModerator = userRoleResult?.roles.includes('moderator');
options.push({ options.push({
icon: 'shield', icon: 'shield',
title: I18n.t('Moderator'), title: I18n.t('Moderator'),
onPress: () => this.handleModerator(selectedUser, !isModerator), onPress: () =>
right: () => ( handleModerator(selectedUser, !isModerator, room, getUserDisplayName(selectedUser), () =>
<CustomIcon fetchRoomMembersRoles(room.t as TRoomType, room.rid, updateState)
testID={isModerator ? 'action-sheet-set-moderator-checked' : 'action-sheet-set-moderator-unchecked'}
name={isModerator ? 'checkbox-checked' : 'checkbox-unchecked'}
size={20}
color={isModerator ? themes[theme].tintActive : themes[theme].auxiliaryTintColor}
/>
), ),
right: () => <RightIcon check={isModerator} label='moderator' />,
testID: 'action-sheet-set-moderator' testID: 'action-sheet-set-moderator'
}); });
} }
// Remove from team // Remove from team
if (this.permissions['edit-team-member']) { if (editTeamMemberPermission) {
options.push({ options.push({
icon: 'logout', icon: 'logout',
danger: true, danger: true,
title: I18n.t('Remove_from_Team'), title: I18n.t('Remove_from_Team'),
onPress: () => this.handleRemoveFromTeam(selectedUser), onPress: () => handleRemoveFromTeam(selectedUser, updateState, room, members),
testID: 'action-sheet-remove-from-team' testID: 'action-sheet-remove-from-team'
}); });
} }
// Remove from room // Remove from room
if (this.permissions['remove-user'] && !room.teamMain) { if (removeUserPermission && !room.teamMain) {
options.push({ options.push({
icon: 'logout', icon: 'logout',
title: I18n.t('Remove_from_room'), title: I18n.t('Remove_from_room'),
@ -422,7 +298,13 @@ class RoomMembersView extends React.Component<IRoomMembersViewProps, IRoomMember
showConfirmationAlert({ showConfirmationAlert({
message: I18n.t('The_user_will_be_removed_from_s', { s: getRoomTitle(room) }), message: I18n.t('The_user_will_be_removed_from_s', { s: getRoomTitle(room) }),
confirmationText: I18n.t('Yes_remove_user'), confirmationText: I18n.t('Yes_remove_user'),
onPress: () => this.handleRemoveUserFromRoom(selectedUser) onPress: () => {
handleRemoveUserFromRoom(selectedUser, room, () =>
updateState({
members: members.filter(member => member._id !== selectedUser._id)
})
);
}
}); });
}, },
testID: 'action-sheet-remove-from-room' testID: 'action-sheet-remove-from-room'
@ -435,256 +317,83 @@ class RoomMembersView extends React.Component<IRoomMembersViewProps, IRoomMember
}); });
}; };
toggleStatus = () => { const fetchMembers = async (status: boolean) => {
try { const { members, isLoading, end, room, filter, page } = state;
const { allUsers } = this.state;
this.setState({ members: [], allUsers: !allUsers, end: false, page: 0 }, () => {
this.fetchMembers();
});
} catch (e) {
log(e);
}
};
fetchRoomMembersRoles = async () => {
try {
const { room } = this.state;
const type = room.t as SubscriptionType.CHANNEL | SubscriptionType.GROUP | SubscriptionType.OMNICHANNEL;
const result = await Services.getRoomRoles(room.rid, type);
if (result?.success) {
this.roomRoles = result.roles;
}
} catch (e) {
log(e);
}
};
fetchMembers = async () => {
const { rid, members, isLoading, allUsers, end, room, filtering, page } = this.state;
const { t } = room; const { t } = room;
if (isLoading || end) { if (isLoading || end) {
return; return;
} }
this.setState({ isLoading: true }); updateState({ isLoading: true });
try { try {
const membersResult = await Services.getRoomMembers({ const membersResult = await Services.getRoomMembers({
rid, rid: room.rid,
roomType: t, roomType: t,
type: allUsers ? 'all' : 'online', type: !status ? 'all' : 'online',
filter: filtering, filter,
skip: PAGE_SIZE * page, skip: PAGE_SIZE * page,
limit: PAGE_SIZE, limit: PAGE_SIZE,
allUsers allUsers: !status
}); });
const end = membersResult?.length < PAGE_SIZE; const end = membersResult?.length < PAGE_SIZE;
const membersResultFiltered = membersResult?.filter((member: TUserModel) => !members.some(m => m._id === member._id)); const membersResultFiltered = membersResult?.filter((member: TUserModel) => !members.some(m => m._id === member._id));
this.setState({ updateState({
members: members.concat(membersResultFiltered || []), members: [...members, ...membersResultFiltered],
isLoading: false, isLoading: false,
end, end,
page: page + 1 page: page + 1
}); });
this.setHeader();
} catch (e) { } catch (e) {
log(e); log(e);
this.setState({ isLoading: false }); updateState({ isLoading: false });
} }
}; };
goRoom = (item: TGoRoomItem) => { const filteredMembers =
const { navigation, isMasterDetail } = this.props; state.members && state.members.length > 0 && state.filter
if (isMasterDetail) { ? state.members.filter(
// @ts-ignore m =>
navigation.navigate('DrawerNavigator'); m.username.toLowerCase().match(state.filter.toLowerCase()) || m.name?.toLowerCase().match(state.filter.toLowerCase())
} else { )
navigation.popToTop(); : null;
}
goRoom({ item, isMasterDetail });
};
getUserDisplayName = (user: TUserModel) => {
const { useRealName } = this.props;
return (useRealName ? user.name : user.username) || user.username;
};
handleMute = async (user: TUserModel) => {
const { rid } = this.state;
try {
await Services.toggleMuteUserInRoom(rid, user?.username, !user?.muted);
EventEmitter.emit(LISTENER, {
message: I18n.t('User_has_been_key', { key: user?.muted ? I18n.t('unmuted') : I18n.t('muted') })
});
} catch (e) {
log(e);
}
};
handleOwner = async (selectedUser: TUserModel, isOwner: boolean) => {
try {
const { room } = this.state;
await Services.toggleRoomOwner({
roomId: room.rid,
t: room.t,
userId: selectedUser._id,
isOwner
});
const message = isOwner
? 'User__username__is_now_a_owner_of__room_name_'
: 'User__username__removed_from__room_name__owners';
EventEmitter.emit(LISTENER, {
message: I18n.t(message, {
username: this.getUserDisplayName(selectedUser),
room_name: getRoomTitle(room)
})
});
} catch (e) {
log(e);
}
this.fetchRoomMembersRoles();
};
handleLeader = async (selectedUser: TUserModel, isLeader: boolean) => {
try {
const { room } = this.state;
await Services.toggleRoomLeader({
roomId: room.rid,
t: room.t,
userId: selectedUser._id,
isLeader
});
const message = isLeader
? 'User__username__is_now_a_leader_of__room_name_'
: 'User__username__removed_from__room_name__leaders';
EventEmitter.emit(LISTENER, {
message: I18n.t(message, {
username: this.getUserDisplayName(selectedUser),
room_name: getRoomTitle(room)
})
});
} catch (e) {
log(e);
}
this.fetchRoomMembersRoles();
};
handleModerator = async (selectedUser: TUserModel, isModerator: boolean) => {
try {
const { room } = this.state;
await Services.toggleRoomModerator({
roomId: room.rid,
t: room.t,
userId: selectedUser._id,
isModerator
});
const message = isModerator
? 'User__username__is_now_a_moderator_of__room_name_'
: 'User__username__removed_from__room_name__moderators';
EventEmitter.emit(LISTENER, {
message: I18n.t(message, {
username: this.getUserDisplayName(selectedUser),
room_name: getRoomTitle(room)
})
});
} catch (e) {
log(e);
}
this.fetchRoomMembersRoles();
};
handleIgnore = async (selectedUser: TUserModel, ignore: boolean) => {
try {
const { room } = this.state;
await Services.ignoreUser({
rid: room.rid,
userId: selectedUser._id,
ignore
});
const message = I18n.t(ignore ? 'User_has_been_ignored' : 'User_has_been_unignored');
EventEmitter.emit(LISTENER, { message });
} catch (e) {
log(e);
}
};
handleRemoveUserFromRoom = async (selectedUser: TUserModel) => {
try {
const { room, members, membersFiltered } = this.state;
const userId = selectedUser._id;
// TODO: interface SubscriptionType on IRoom is wrong
await Services.removeUserFromRoom({ roomId: room.rid, t: room.t as RoomTypes, userId });
const message = I18n.t('User_has_been_removed_from_s', { s: getRoomTitle(room) });
EventEmitter.emit(LISTENER, { message });
this.setState({
members: members.filter(member => member._id !== userId),
membersFiltered: this.isServerVersionLowerThan3_16 ? membersFiltered.filter(member => member._id !== userId) : []
});
} catch (e) {
log(e);
}
};
renderSearchBar = () => <SearchBox onChangeText={text => this.onSearchChangeText(text)} testID='room-members-view-search' />;
renderItem = ({ item }: { item: TUserModel }) => {
const { theme } = this.props;
return (
<UserItem
name={item.name as string}
username={item.username}
onPress={() => this.onPressUser(item)}
testID={`room-members-view-item-${item.username}`}
theme={theme}
/>
);
};
render() {
const { filtering, members, membersFiltered, isLoading } = this.state;
const { theme } = this.props;
return ( return (
<SafeAreaView testID='room-members-view'> <SafeAreaView testID='room-members-view'>
<StatusBar /> <StatusBar />
<FlatList <FlatList
data={!!filtering && this.isServerVersionLowerThan3_16 ? membersFiltered : members} data={filteredMembers || state.members}
renderItem={this.renderItem} renderItem={({ item }) => (
style={[styles.list, { backgroundColor: themes[theme].backgroundColor }]} <View style={{ backgroundColor: colors.backgroundColor }}>
<UserItem
name={item.name as string}
username={item.username}
onPress={() => onPressUser(item)}
testID={`room-members-view-item-${item.username}`}
/>
</View>
)}
style={styles.list}
keyExtractor={item => item._id} keyExtractor={item => item._id}
ItemSeparatorComponent={List.Separator} ItemSeparatorComponent={List.Separator}
ListHeaderComponent={this.renderSearchBar} ListHeaderComponent={
ListFooterComponent={() => { <>
if (isLoading) { <ActionsSection joined={params.joined as boolean} rid={state.room.rid} t={state.room.t} />
return <ActivityIndicator />; <View style={{ backgroundColor: colors.backgroundColor }}>
<SearchBox onChangeText={text => updateState({ filter: text.trim() })} testID='room-members-view-search' />
</View>
</>
} }
return null; ListFooterComponent={() => (state.isLoading ? <ActivityIndicator /> : null)}
}}
onEndReachedThreshold={0.1} onEndReachedThreshold={0.1}
onEndReached={this.fetchMembers} onEndReached={() => fetchMembers(state.allUsers)}
maxToRenderPerBatch={5} ListEmptyComponent={() =>
windowSize={10} state.end ? <Text style={[styles.noResult, { color: colors.titleText }]}>{I18n.t('No_members_found')}</Text> : null
}
{...scrollPersistTaps} {...scrollPersistTaps}
/> />
</SafeAreaView> </SafeAreaView>
); );
} };
}
const mapStateToProps = (state: IApplicationState) => ({ export default RoomMembersView;
baseUrl: state.server.server,
user: getUserSelector(state),
isMasterDetail: state.app.isMasterDetail,
useRealName: state.settings.UI_Use_Real_Name,
muteUserPermission: state.permissions['mute-user'],
setLeaderPermission: state.permissions['set-leader'],
setOwnerPermission: state.permissions['set-owner'],
setModeratorPermission: state.permissions['set-moderator'],
removeUserPermission: state.permissions['remove-user'],
editTeamMemberPermission: state.permissions['edit-team-member'],
viewAllTeamChannelsPermission: state.permissions['view-all-team-channels'],
viewAllTeamsPermission: state.permissions['view-all-teams'],
serverVersion: state.server.version
});
export default connect(mapStateToProps)(withTheme(withActionSheet(RoomMembersView)));

View File

@ -1,20 +1,15 @@
import { StyleSheet } from 'react-native'; import { StyleSheet } from 'react-native';
import sharedStyles from '../Styles';
export default StyleSheet.create({ export default StyleSheet.create({
list: { list: {
flex: 1 flex: 1
}, },
item: { noResult: {
flexDirection: 'row', fontSize: 16,
paddingVertical: 10, paddingVertical: 56,
paddingHorizontal: 16, ...sharedStyles.textSemibold,
alignItems: 'center' ...sharedStyles.textAlignCenter
},
avatar: {
marginRight: 16
},
separator: {
height: StyleSheet.hairlineWidth,
marginLeft: 60
} }
}); });

View File

@ -211,19 +211,15 @@ class SelectedUsersView extends React.Component<ISelectedUsersViewProps, ISelect
); );
}; };
renderSelectedItem = ({ item }: { item: ISelectedUser }) => { renderSelectedItem = ({ item }: { item: ISelectedUser }) => (
const { theme } = this.props;
return (
<UserItem <UserItem
name={item.fname} name={item.fname}
username={item.name} username={item.name}
onPress={() => this._onPressSelectedItem(item)} onPress={() => this._onPressSelectedItem(item)}
testID={`selected-user-${item.name}`} testID={`selected-user-${item.name}`}
style={{ paddingRight: 15 }} style={{ paddingRight: 15 }}
theme={theme}
/> />
); );
};
renderItem = ({ item, index }: { item: ISelectedUser; index: number }) => { renderItem = ({ item, index }: { item: ISelectedUser; index: number }) => {
const { search, chats } = this.state; const { search, chats } = this.state;
@ -249,7 +245,6 @@ class SelectedUsersView extends React.Component<ISelectedUsersViewProps, ISelect
testID={`select-users-view-item-${item.name}`} testID={`select-users-view-item-${item.name}`}
icon={this.isChecked(username) ? 'check' : null} icon={this.isChecked(username) ? 'check' : null}
style={style} style={style}
theme={theme}
/> />
); );
}; };

View File

@ -146,10 +146,6 @@ describe('Room actions screen', () => {
await expect(element(by.id('room-actions-members'))).toExist(); await expect(element(by.id('room-actions-members'))).toExist();
}); });
it('should have add user', async () => {
await expect(element(by.id('room-actions-add-user'))).toExist();
});
it('should have files', async () => { it('should have files', async () => {
await expect(element(by.id('room-actions-files'))).toExist(); await expect(element(by.id('room-actions-files'))).toExist();
}); });
@ -303,24 +299,12 @@ describe('Room actions screen', () => {
.withTimeout(4000); .withTimeout(4000);
}); });
it('should have notification audio option', async () => {
await waitFor(element(by.id('notification-preference-view-audio')))
.toExist()
.withTimeout(4000);
});
it('should have notification sound option', async () => { it('should have notification sound option', async () => {
await waitFor(element(by.id('notification-preference-view-sound'))) await waitFor(element(by.id('notification-preference-view-sound')))
.toExist() .toExist()
.withTimeout(4000); .withTimeout(4000);
}); });
it('should have notification duration option', async () => {
await waitFor(element(by.id('notification-preference-view-notification-duration')))
.toExist()
.withTimeout(4000);
});
it('should have email alert option', async () => { it('should have email alert option', async () => {
await waitFor(element(by.id('notification-preference-view-email-alert'))) await waitFor(element(by.id('notification-preference-view-email-alert')))
.toExist() .toExist()
@ -361,6 +345,14 @@ describe('Room actions screen', () => {
}); });
it('should add users to the room', async () => { it('should add users to the room', async () => {
await waitFor(element(by.id('room-actions-members')))
.toExist()
.withTimeout(2000);
await element(by.id('room-actions-members')).tap();
await waitFor(element(by.id('room-members-view')))
.toExist()
.withTimeout(2000);
await waitFor(element(by.id('room-actions-add-user'))) await waitFor(element(by.id('room-actions-add-user')))
.toExist() .toExist()
.withTimeout(4000); .withTimeout(4000);
@ -392,19 +384,14 @@ describe('Room actions screen', () => {
await element(by.id('selected-users-view-submit')).tap(); await element(by.id('selected-users-view-submit')).tap();
await sleep(300); await sleep(300);
await waitFor(element(by.id('room-actions-members')))
.toExist()
.withTimeout(10000);
await element(by.id('room-actions-members')).tap();
await element(by.id('room-members-view-toggle-status')).tap();
await waitFor(element(by.id(`room-members-view-item-${user.username}`)))
.toExist()
.withTimeout(60000);
await backToActions(); await backToActions();
}); });
describe('Room Members', () => { describe('Room Members', () => {
before(async () => { before(async () => {
await waitFor(element(by.id('room-actions-members')))
.toExist()
.withTimeout(2000);
await element(by.id('room-actions-members')).tap(); await element(by.id('room-actions-members')).tap();
await waitFor(element(by.id('room-members-view'))) await waitFor(element(by.id('room-members-view')))
.toExist() .toExist()
@ -442,13 +429,30 @@ describe('Room actions screen', () => {
}; };
it('should show all users', async () => { it('should show all users', async () => {
await element(by.id('room-members-view-toggle-status')).tap(); await waitFor(element(by.id('room-members-view-filter')))
.toExist()
.withTimeout(10000);
await element(by.id('room-members-view-filter')).tap();
await waitFor(element(by.id('room-members-view-toggle-status-all')))
.toExist()
.withTimeout(2000);
await element(by.id('room-members-view-toggle-status-all')).tap();
await waitFor(element(by.id(`room-members-view-item-${user.username}`))) await waitFor(element(by.id(`room-members-view-item-${user.username}`)))
.toExist() .toExist()
.withTimeout(60000); .withTimeout(60000);
await tapBack();
}); });
it('should filter user', async () => { it('should filter user', async () => {
await waitFor(element(by.id('room-actions-members')))
.toExist()
.withTimeout(2000);
await element(by.id('room-actions-members')).tap();
await element(by.id('room-members-view-filter')).tap();
await waitFor(element(by.id('room-members-view-toggle-status-all')))
.toExist()
.withTimeout(2000);
await element(by.id('room-members-view-toggle-status-all')).tap();
await waitFor(element(by.id(`room-members-view-item-${user.username}`))) await waitFor(element(by.id(`room-members-view-item-${user.username}`)))
.toExist() .toExist()
.withTimeout(60000); .withTimeout(60000);
@ -595,11 +599,21 @@ describe('Room actions screen', () => {
await waitFor(element(by.id('room-actions-view'))) await waitFor(element(by.id('room-actions-view')))
.toExist() .toExist()
.withTimeout(5000); .withTimeout(5000);
await waitFor(element(by.id('room-actions-members')))
.toExist()
.withTimeout(2000);
await element(by.id('room-actions-members')).tap(); await element(by.id('room-actions-members')).tap();
await waitFor(element(by.id('room-members-view'))) await waitFor(element(by.id('room-members-view')))
.toExist() .toExist()
.withTimeout(2000); .withTimeout(2000);
await element(by.id('room-members-view-toggle-status')).tap(); await waitFor(element(by.id('room-members-view-filter')))
.toExist()
.withTimeout(10000);
await element(by.id('room-members-view-filter')).tap();
await waitFor(element(by.id('room-members-view-toggle-status-all')))
.toExist()
.withTimeout(2000);
await element(by.id('room-members-view-toggle-status-all')).tap();
await waitFor(element(by.id(`room-members-view-item-${user.username}`))) await waitFor(element(by.id(`room-members-view-item-${user.username}`)))
.toExist() .toExist()
.withTimeout(60000); .withTimeout(60000);
@ -625,6 +639,7 @@ describe('Room actions screen', () => {
}); });
it('should block/unblock user', async () => { it('should block/unblock user', async () => {
await element(by.id('room-actions-scrollview')).scrollTo('bottom');
await waitFor(element(by.id('room-actions-block-user'))).toExist(); await waitFor(element(by.id('room-actions-block-user'))).toExist();
await element(by.id('room-actions-block-user')).tap(); await element(by.id('room-actions-block-user')).tap();
await waitFor(element(by[textMatcher]('Unblock user'))) await waitFor(element(by[textMatcher]('Unblock user')))

View File

@ -266,6 +266,11 @@ describe('Team', () => {
}); });
it('should add users to the team', async () => { it('should add users to the team', async () => {
await element(by.id('room-actions-members')).tap();
await waitFor(element(by.id('room-members-view')))
.toExist()
.withTimeout(2000);
await waitFor(element(by.id('room-actions-add-user'))) await waitFor(element(by.id('room-actions-add-user')))
.toExist() .toExist()
.withTimeout(10000); .withTimeout(10000);
@ -296,11 +301,17 @@ describe('Team', () => {
await element(by.id('selected-users-view-submit')).tap(); await element(by.id('selected-users-view-submit')).tap();
await sleep(300); await sleep(300);
await tapBack();
await sleep(300);
await waitFor(element(by.id('room-actions-members'))) await waitFor(element(by.id('room-actions-members')))
.toExist() .toExist()
.withTimeout(10000); .withTimeout(10000);
await element(by.id('room-actions-members')).tap(); await element(by.id('room-actions-members')).tap();
await element(by.id('room-members-view-toggle-status')).tap(); await element(by.id('room-members-view-filter')).tap();
await waitFor(element(by.id('room-members-view-toggle-status-all')))
.toExist()
.withTimeout(2000);
await element(by.id('room-members-view-toggle-status-all')).tap();
await waitFor(element(by.id(`room-members-view-item-${user.username}`))) await waitFor(element(by.id(`room-members-view-item-${user.username}`)))
.toExist() .toExist()
.withTimeout(60000); .withTimeout(60000);
@ -358,7 +369,11 @@ describe('Team', () => {
}); });
it('should show all users', async () => { it('should show all users', async () => {
await element(by.id('room-members-view-toggle-status')).tap(); await element(by.id('room-members-view-filter')).tap();
await waitFor(element(by.id('room-members-view-toggle-status-all')))
.toExist()
.withTimeout(2000);
await element(by.id('room-members-view-toggle-status-all')).tap();
await waitFor(element(by.id(`room-members-view-item-${user.username}`))) await waitFor(element(by.id(`room-members-view-item-${user.username}`)))
.toExist() .toExist()
.withTimeout(60000); .withTimeout(60000);