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

View File

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

View File

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

View File

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

View File

@ -272,6 +272,10 @@ export default {
RA_MOVE_TO_TEAM_F: 'ra_move_to_team_f',
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
RI_GO_RI_EDIT: 'ri_go_ri_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));
}
function popToTop() {
navigationRef.current?.dispatch(StackActions.popToTop());
}
export default {
navigationRef,
routeNameRef,
navigate,
back,
replace
replace,
popToTop
};

View File

@ -95,7 +95,7 @@ const ChatsStackNavigator = () => {
<ChatsStack.Screen name='SelectListView' component={SelectListView} options={SelectListView.navigationOptions} />
<ChatsStack.Screen name='RoomInfoView' component={RoomInfoView} options={RoomInfoView.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='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='SelectListView' component={SelectListView} />
<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
name='SearchMessagesView'
component={SearchMessagesView}

View File

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

View File

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

View File

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

View File

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

View File

@ -64,10 +64,6 @@ interface IRoomActionsViewProps extends IActionSheetProvider, IBaseScreen<ChatsS
encryptionEnabled: boolean;
fontScale: number;
serverVersion: string | null;
addUserToJoinedRoomPermission?: string[];
addUserToAnyCRoomPermission?: string[];
addUserToAnyPRoomPermission?: string[];
createInviteLinksPermission?: string[];
editRoomPermission?: string[];
toggleRoomE2EEncryptionPermission?: string[];
viewBroadcastMemberListPermission?: string[];
@ -94,8 +90,6 @@ interface IRoomActionsViewState {
joined: boolean;
canViewMembers: boolean;
canAutoTranslate: boolean;
canAddUser: boolean;
canInviteUser: boolean;
canEdit: boolean;
canToggleEncryption: boolean;
canCreateTeam: boolean;
@ -146,8 +140,6 @@ class RoomActionsView extends React.Component<IRoomActionsViewProps, IRoomAction
joined: !!room,
canViewMembers: false,
canAutoTranslate: false,
canAddUser: false,
canInviteUser: false,
canEdit: false,
canToggleEncryption: false,
canCreateTeam: false,
@ -206,8 +198,6 @@ class RoomActionsView extends React.Component<IRoomActionsViewProps, IRoomAction
}
const canAutoTranslate = canAutoTranslateMethod();
const canAddUser = await this.canAddUser();
const canInviteUser = await this.canInviteUser();
const canEdit = await this.canEdit();
const canToggleEncryption = await this.canToggleEncryption();
const canViewMembers = await this.canViewMembers();
@ -217,8 +207,6 @@ class RoomActionsView extends React.Component<IRoomActionsViewProps, IRoomAction
this.setState({
canAutoTranslate,
canAddUser,
canInviteUser,
canEdit,
canToggleEncryption,
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 () => {
const { room } = this.state;
const { editRoomPermission } = this.props;
@ -1135,7 +1089,7 @@ class RoomActionsView extends React.Component<IRoomActionsViewProps, IRoomAction
};
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 isGroupChatHandler = isGroupChat(room);
@ -1154,7 +1108,7 @@ class RoomActionsView extends React.Component<IRoomActionsViewProps, IRoomAction
<List.Item
title='Members'
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'
left={() => <List.Icon name='team' />}
showActionIndicator
@ -1164,45 +1118,6 @@ class RoomActionsView extends React.Component<IRoomActionsViewProps, IRoomAction
</>
) : 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 ? (
<>
<List.Item
@ -1384,10 +1299,6 @@ const mapStateToProps = (state: IApplicationState) => ({
encryptionEnabled: state.encryption.enabled,
serverVersion: state.server.version,
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'],
toggleRoomE2EEncryptionPermission: state.permissions['toggle-room-e2e-encryption'],
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 React from 'react';
import { FlatList } from 'react-native';
import { connect } from 'react-redux';
import { Observable, Subscription } from 'rxjs';
import { NavigationProp, RouteProp, useNavigation, useRoute } from '@react-navigation/native';
import React, { useEffect, useReducer } from 'react';
import { FlatList, Text, View } from 'react-native';
import { themes } from '../../lib/constants';
import { TActionSheetOptions, TActionSheetOptionsItem, withActionSheet } from '../../containers/ActionSheet';
import { TActionSheetOptionsItem, useActionSheet } from '../../containers/ActionSheet';
import ActivityIndicator from '../../containers/ActivityIndicator';
import { CustomIcon } from '../../containers/CustomIcon';
import * as HeaderButton from '../../containers/HeaderButton';
import * as List from '../../containers/List';
import { RadioButton } from '../../containers/RadioButton';
import SafeAreaView from '../../containers/SafeAreaView';
import SearchBox from '../../containers/SearchBox';
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 { getUserSelector } from '../../selectors/login';
import { ModalStackParamList } from '../../stacks/MasterDetailStack/types';
import { TSupportedThemes, withTheme } from '../../theme';
import EventEmitter from '../../lib/methods/helpers/events';
import { goRoom, TGoRoomItem } from '../../lib/methods/helpers/goRoom';
import { showConfirmationAlert, showErrorAlert } from '../../lib/methods/helpers/info';
import { TSubscriptionModel, TUserModel } from '../../definitions';
import I18n from '../../i18n';
import { useAppSelector, usePermissions } from '../../lib/hooks';
import { getRoomTitle, isGroupChat } from '../../lib/methods/helpers';
import { showConfirmationAlert } from '../../lib/methods/helpers/info';
import log from '../../lib/methods/helpers/log';
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 { 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;
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 {
isLoading: boolean;
allUsers: boolean;
filtering: string;
rid: string;
members: TUserModel[];
membersFiltered: TUserModel[];
room: TSubscriptionModel;
end: boolean;
roomRoles: any;
filter: string;
page: number;
}
class RoomMembersView extends React.Component<IRoomMembersViewProps, IRoomMembersViewState> {
private mounted: boolean;
private permissions: { [key in TSupportedPermissions]?: boolean };
private roomObservable!: Observable<TSubscriptionModel>;
private subscription!: Subscription;
private roomRoles: any;
const RightIcon = ({ check, label }: { check: boolean; label: string }) => {
const { colors } = useTheme();
return (
<CustomIcon
testID={check ? `action-sheet-set-${label}-checked` : `action-sheet-set-${label}-unchecked`}
name={check ? 'checkbox-checked' : 'checkbox-unchecked'}
size={20}
color={check ? colors.tintActive : colors.auxiliaryTintColor}
/>
);
};
constructor(props: IRoomMembersViewProps) {
super(props);
this.mounted = false;
this.permissions = {};
const rid = props.route.params?.rid;
const room = props.route.params?.room;
this.state = {
const RoomMembersView = (): React.ReactElement => {
const { showActionSheet } = useActionSheet();
const { colors } = useTheme();
const { params } = useRoute<RouteProp<ModalStackParamList, 'RoomMembersView'>>();
const navigation = useNavigation<NavigationProp<ModalStackParamList, 'RoomMembersView'>>();
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,
allUsers: false,
filtering: '',
rid,
members: [],
membersFiltered: [],
room: room || ({} as TSubscriptionModel),
room: params.room || ({} as TSubscriptionModel),
end: false,
roomRoles: null,
filter: '',
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 { room } = this.state;
this.mounted = true;
this.fetchMembers();
const teamPermissions: TSupportedPermissions[] = state.room.teamMain
? ['edit-team-member', 'view-all-team-channels', 'view-all-teams']
: [];
if (isGroupChat(room)) {
return;
}
const {
const [
muteUserPermission,
setLeaderPermission,
setOwnerPermission,
@ -126,186 +106,97 @@ class RoomMembersView extends React.Component<IRoomMembersViewProps, IRoomMember
editTeamMemberPermission,
viewAllTeamChannelsPermission,
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,
setLeaderPermission,
setOwnerPermission,
setModeratorPermission,
removeUserPermission,
...(room.teamMain ? [editTeamMemberPermission, viewAllTeamChannelsPermission, viewAllTeamsPermission] : [])
],
room.rid
);
editTeamMemberPermission,
viewAllTeamChannelsPermission,
viewAllTeamsPermission
]);
this.permissions = {
'mute-user': result[0],
'set-leader': result[1],
'set-owner': result[2],
'set-moderator': result[3],
'remove-user': result[4],
...(room.teamMain
? {
'edit-team-member': result[5],
'view-all-team-channels': result[6],
'view-all-teams': result[7]
const toggleStatus = (status: boolean) => {
try {
updateState({ members: [], allUsers: status, end: false });
fetchMembers(status);
setHeader(status);
} catch (e) {
log(e);
}
: {})
};
const hasSinglePermission = Object.values(this.permissions).some(p => !!p);
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');
const setHeader = (allUsers: boolean) => {
navigation.setOptions({
title: I18n.t('Members'),
headerRight: () => (
<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>
)
});
};
get isServerVersionLowerThan3_16() {
const { serverVersion } = this.props;
return compareServerVersion(serverVersion, 'lowerThan', '3.16.0');
}
const getUserDisplayName = (user: TUserModel) => (useRealName ? user.name : user.username) || user.username;
onSearchChangeText = debounce((text: string) => {
const { members } = this.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 onPressUser = (selectedUser: TUserModel) => {
const { room, roomRoles, members } = state;
const options: TActionSheetOptionsItem[] = [
{
icon: '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({
icon: 'ignore',
title: I18n.t(isIgnored ? 'Unignore' : 'Ignore'),
onPress: () => this.handleIgnore(selectedUser, !isIgnored),
onPress: () => handleIgnore(selectedUser, !isIgnored, room.rid),
testID: 'action-sheet-ignore-user'
});
}
if (this.permissions['mute-user']) {
if (muteUserPermission) {
const { muted = [] } = room;
const userIsMuted = muted.find?.(m => m === selectedUser.username);
selectedUser.muted = !!userIsMuted;
@ -334,7 +225,7 @@ class RoomMembersView extends React.Component<IRoomMembersViewProps, IRoomMember
roomName: getRoomTitle(room)
}),
confirmationText: I18n.t(userIsMuted ? 'Unmute' : 'Mute'),
onPress: () => this.handleMute(selectedUser)
onPress: () => handleMute(selectedUser, room.rid)
});
},
testID: 'action-sheet-mute-user'
@ -342,78 +233,63 @@ class RoomMembersView extends React.Component<IRoomMembersViewProps, IRoomMember
}
// Owner
if (this.permissions['set-owner']) {
const userRoleResult = this.roomRoles.find((r: any) => r.u._id === selectedUser._id);
const isOwner = userRoleResult?.roles.includes('owner');
if (setOwnerPermission) {
const isOwner = fetchRole('owner', selectedUser, roomRoles);
options.push({
icon: 'shield-check',
title: I18n.t('Owner'),
onPress: () => this.handleOwner(selectedUser, !isOwner),
right: () => (
<CustomIcon
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}
/>
onPress: () =>
handleOwner(selectedUser, !isOwner, getUserDisplayName(selectedUser), room, () =>
fetchRoomMembersRoles(room.t as TRoomType, room.rid, updateState)
),
right: () => <RightIcon check={isOwner} label='owner' />,
testID: 'action-sheet-set-owner'
});
}
// Leader
if (this.permissions['set-leader']) {
const userRoleResult = this.roomRoles.find((r: any) => r.u._id === selectedUser._id);
const isLeader = userRoleResult?.roles.includes('leader');
if (setLeaderPermission) {
const isLeader = fetchRole('leader', selectedUser, roomRoles);
options.push({
icon: 'shield-alt',
title: I18n.t('Leader'),
onPress: () => this.handleLeader(selectedUser, !isLeader),
right: () => (
<CustomIcon
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}
/>
onPress: () =>
handleLeader(selectedUser, !isLeader, room, getUserDisplayName(selectedUser), () =>
fetchRoomMembersRoles(room.t as TRoomType, room.rid, updateState)
),
right: () => <RightIcon check={isLeader} label='leader' />,
testID: 'action-sheet-set-leader'
});
}
// Moderator
if (this.permissions['set-moderator']) {
const userRoleResult = this.roomRoles.find((r: any) => r.u._id === selectedUser._id);
const isModerator = userRoleResult?.roles.includes('moderator');
if (setModeratorPermission) {
const isModerator = fetchRole('moderator', selectedUser, roomRoles);
options.push({
icon: 'shield',
title: I18n.t('Moderator'),
onPress: () => this.handleModerator(selectedUser, !isModerator),
right: () => (
<CustomIcon
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}
/>
onPress: () =>
handleModerator(selectedUser, !isModerator, room, getUserDisplayName(selectedUser), () =>
fetchRoomMembersRoles(room.t as TRoomType, room.rid, updateState)
),
right: () => <RightIcon check={isModerator} label='moderator' />,
testID: 'action-sheet-set-moderator'
});
}
// Remove from team
if (this.permissions['edit-team-member']) {
if (editTeamMemberPermission) {
options.push({
icon: 'logout',
danger: true,
title: I18n.t('Remove_from_Team'),
onPress: () => this.handleRemoveFromTeam(selectedUser),
onPress: () => handleRemoveFromTeam(selectedUser, updateState, room, members),
testID: 'action-sheet-remove-from-team'
});
}
// Remove from room
if (this.permissions['remove-user'] && !room.teamMain) {
if (removeUserPermission && !room.teamMain) {
options.push({
icon: 'logout',
title: I18n.t('Remove_from_room'),
@ -422,7 +298,13 @@ class RoomMembersView extends React.Component<IRoomMembersViewProps, IRoomMember
showConfirmationAlert({
message: I18n.t('The_user_will_be_removed_from_s', { s: getRoomTitle(room) }),
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'
@ -435,256 +317,83 @@ class RoomMembersView extends React.Component<IRoomMembersViewProps, IRoomMember
});
};
toggleStatus = () => {
try {
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 fetchMembers = async (status: boolean) => {
const { members, isLoading, end, room, filter, page } = state;
const { t } = room;
if (isLoading || end) {
return;
}
this.setState({ isLoading: true });
updateState({ isLoading: true });
try {
const membersResult = await Services.getRoomMembers({
rid,
rid: room.rid,
roomType: t,
type: allUsers ? 'all' : 'online',
filter: filtering,
type: !status ? 'all' : 'online',
filter,
skip: PAGE_SIZE * page,
limit: PAGE_SIZE,
allUsers
allUsers: !status
});
const end = membersResult?.length < PAGE_SIZE;
const membersResultFiltered = membersResult?.filter((member: TUserModel) => !members.some(m => m._id === member._id));
this.setState({
members: members.concat(membersResultFiltered || []),
updateState({
members: [...members, ...membersResultFiltered],
isLoading: false,
end,
page: page + 1
});
this.setHeader();
} catch (e) {
log(e);
this.setState({ isLoading: false });
updateState({ isLoading: false });
}
};
goRoom = (item: TGoRoomItem) => {
const { navigation, isMasterDetail } = this.props;
if (isMasterDetail) {
// @ts-ignore
navigation.navigate('DrawerNavigator');
} else {
navigation.popToTop();
}
goRoom({ item, isMasterDetail });
};
const filteredMembers =
state.members && state.members.length > 0 && state.filter
? state.members.filter(
m =>
m.username.toLowerCase().match(state.filter.toLowerCase()) || m.name?.toLowerCase().match(state.filter.toLowerCase())
)
: null;
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 (
<SafeAreaView testID='room-members-view'>
<StatusBar />
<FlatList
data={!!filtering && this.isServerVersionLowerThan3_16 ? membersFiltered : members}
renderItem={this.renderItem}
style={[styles.list, { backgroundColor: themes[theme].backgroundColor }]}
data={filteredMembers || state.members}
renderItem={({ item }) => (
<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}
ItemSeparatorComponent={List.Separator}
ListHeaderComponent={this.renderSearchBar}
ListFooterComponent={() => {
if (isLoading) {
return <ActivityIndicator />;
ListHeaderComponent={
<>
<ActionsSection joined={params.joined as boolean} rid={state.room.rid} t={state.room.t} />
<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}
onEndReached={this.fetchMembers}
maxToRenderPerBatch={5}
windowSize={10}
onEndReached={() => fetchMembers(state.allUsers)}
ListEmptyComponent={() =>
state.end ? <Text style={[styles.noResult, { color: colors.titleText }]}>{I18n.t('No_members_found')}</Text> : null
}
{...scrollPersistTaps}
/>
</SafeAreaView>
);
}
}
};
const mapStateToProps = (state: IApplicationState) => ({
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)));
export default RoomMembersView;

View File

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

View File

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

View File

@ -146,10 +146,6 @@ describe('Room actions screen', () => {
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 () => {
await expect(element(by.id('room-actions-files'))).toExist();
});
@ -303,24 +299,12 @@ describe('Room actions screen', () => {
.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 () => {
await waitFor(element(by.id('notification-preference-view-sound')))
.toExist()
.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 () => {
await waitFor(element(by.id('notification-preference-view-email-alert')))
.toExist()
@ -361,6 +345,14 @@ describe('Room actions screen', () => {
});
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')))
.toExist()
.withTimeout(4000);
@ -392,19 +384,14 @@ describe('Room actions screen', () => {
await element(by.id('selected-users-view-submit')).tap();
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();
});
describe('Room Members', () => {
before(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()
@ -442,13 +429,30 @@ describe('Room actions screen', () => {
};
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}`)))
.toExist()
.withTimeout(60000);
await tapBack();
});
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}`)))
.toExist()
.withTimeout(60000);
@ -595,11 +599,21 @@ describe('Room actions screen', () => {
await waitFor(element(by.id('room-actions-view')))
.toExist()
.withTimeout(5000);
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 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}`)))
.toExist()
.withTimeout(60000);
@ -625,6 +639,7 @@ describe('Room actions screen', () => {
});
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 element(by.id('room-actions-block-user')).tap();
await waitFor(element(by[textMatcher]('Unblock user')))

View File

@ -266,6 +266,11 @@ describe('Team', () => {
});
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')))
.toExist()
.withTimeout(10000);
@ -296,11 +301,17 @@ describe('Team', () => {
await element(by.id('selected-users-view-submit')).tap();
await sleep(300);
await tapBack();
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 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}`)))
.toExist()
.withTimeout(60000);
@ -358,7 +369,11 @@ describe('Team', () => {
});
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}`)))
.toExist()
.withTimeout(60000);