diff --git a/app/lib/methods/helpers/handleIgnore.ts b/app/lib/methods/helpers/handleIgnore.ts new file mode 100644 index 000000000..70a713308 --- /dev/null +++ b/app/lib/methods/helpers/handleIgnore.ts @@ -0,0 +1,19 @@ +import { LISTENER } from '../../../containers/Toast'; +import I18n from '../../../i18n'; +import EventEmitter from './events'; +import log from './log'; +import { Services } from '../../services'; + +export const handleIgnore = async (userId: string, ignore: boolean, rid: string) => { + try { + await Services.ignoreUser({ + rid, + userId, + ignore + }); + const message = I18n.t(ignore ? 'User_has_been_ignored' : 'User_has_been_unignored'); + EventEmitter.emit(LISTENER, { message }); + } catch (e) { + log(e); + } +}; diff --git a/app/lib/methods/helpers/log/events.ts b/app/lib/methods/helpers/log/events.ts index b4562b6c5..f280a2b1c 100644 --- a/app/lib/methods/helpers/log/events.ts +++ b/app/lib/methods/helpers/log/events.ts @@ -280,6 +280,7 @@ export default { RI_GO_RI_EDIT: 'ri_go_ri_edit', RI_GO_LIVECHAT_EDIT: 'ri_go_livechat_edit', RI_GO_ROOM_USER: 'ri_go_room_user', + RI_TOGGLE_BLOCK_USER: 'ri_toggle_block_user', // ROOM INFO EDIT VIEW RI_EDIT_TOGGLE_ROOM_TYPE: 'ri_edit_toggle_room_type', diff --git a/app/stacks/types.ts b/app/stacks/types.ts index dab02f251..d83f7f2c8 100644 --- a/app/stacks/types.ts +++ b/app/stacks/types.ts @@ -68,6 +68,7 @@ export type ChatsStackParamList = { rid: string; t: SubscriptionType; showCloseModal?: boolean; + fromRid?: string; }; RoomInfoEditView: { rid: string; diff --git a/app/views/RoomActionsView/index.tsx b/app/views/RoomActionsView/index.tsx index e2d911dbb..9c3a4eaaa 100644 --- a/app/views/RoomActionsView/index.tsx +++ b/app/views/RoomActionsView/index.tsx @@ -760,7 +760,8 @@ class RoomActionsView extends React.Component { @@ -121,22 +124,29 @@ class RoomInfoView extends React.Component; + private fromRid?: string; + + private subscriptionRoomFromRid?: Subscription; + constructor(props: IRoomInfoViewProps) { super(props); const room = props.route.params?.room; const roomUser = props.route.params?.member; this.rid = props.route.params?.rid; this.t = props.route.params?.t; + this.fromRid = props.route.params?.fromRid; this.state = { room: (room || { rid: this.rid, t: this.t }) as any, roomUser: roomUser || {}, - showEdit: false + showEdit: false, + roomFromRid: undefined }; } componentDidMount() { if (this.isDirect) { this.loadUser(); + this.loadRoomFromRid(); } else { this.loadRoom(); } @@ -154,6 +164,9 @@ class RoomInfoView extends React.Component { + if (this.fromRid) { + try { + const sub = await getSubscriptionByRoomId(this.fromRid); + this.subscriptionRoomFromRid = sub?.observe().subscribe(roomFromRid => { + this.setState({ roomFromRid }); + }); + } catch (e) { + // do nothing + } + } + }; + loadRoom = async () => { const { room: roomState } = this.state; const { route, editRoomPermission, editOmnichannelContact, editLivechatRoomCustomfields } = this.props; @@ -351,11 +377,32 @@ class RoomInfoView extends React.Component void) => { + try { + if (this.isDirect) { + await this.createDirect(); + } + onPress(); + } catch { + EventEmitter.emit(LISTENER, { + message: I18n.t('error-action-not-allowed', { action: I18n.t('Create_Direct_Messages') }) + }); + } + }; + videoCall = () => { const { room } = this.state; callJitsi(room); }; + handleBlockUser = async (rid: string, blocked: string, block: boolean) => { + logEvent(events.RI_TOGGLE_BLOCK_USER); + try { + await Services.toggleBlockUser(rid, blocked, block); + } catch (e) { + log(e); + } + }; renderAvatar = (room: ISubscription, roomUser: IUserParsed) => { const { theme } = this.props; @@ -370,36 +417,54 @@ class RoomInfoView extends React.Component void, iconName: TIconsName, text: string) => { + renderButton = (onPress: () => void, iconName: TIconsName, text: string, danger?: boolean) => { const { theme } = this.props; - - const onActionPress = async () => { - try { - if (this.isDirect) { - await this.createDirect(); - } - onPress(); - } catch { - EventEmitter.emit(LISTENER, { - message: I18n.t('error-action-not-allowed', { action: I18n.t('Create_Direct_Messages') }) - }); - } - }; - + const color = danger ? themes[theme].dangerColor : themes[theme].actionTintColor; return ( - - - {text} + + + {text} ); }; renderButtons = () => { + const { roomFromRid, roomUser } = this.state; const { jitsiEnabled } = this.props; + + const isFromDm = roomFromRid?.rid ? new RegExp(roomUser._id).test(roomFromRid.rid) : false; + const isDirectFromSaved = this.isDirect && this.fromRid && roomFromRid; + + // Following the web behavior, when is a DM with myself, shouldn't appear block or ignore option + const isDmWithMyself = roomFromRid?.uids && roomFromRid.uids?.filter(uid => uid !== roomUser._id).length === 0; + + const ignored = roomFromRid?.ignored; + const isIgnored = ignored?.includes?.(roomUser._id); + + const blocker = roomFromRid?.blocker; + return ( - {this.renderButton(this.goRoom, 'message', I18n.t('Message'))} - {jitsiEnabled && this.isDirect ? this.renderButton(this.videoCall, 'camera', I18n.t('Video_call')) : null} + {this.renderButton(() => this.handleCreateDirectMessage(this.goRoom), 'message', I18n.t('Message'))} + {jitsiEnabled && this.isDirect + ? this.renderButton(() => this.handleCreateDirectMessage(this.videoCall), 'camera', I18n.t('Video_call')) + : null} + {isDirectFromSaved && !isFromDm && !isDmWithMyself + ? this.renderButton( + () => handleIgnore(roomUser._id, !isIgnored, roomFromRid.rid), + 'ignore', + I18n.t(isIgnored ? 'Unignore' : 'Ignore'), + true + ) + : null} + {isDirectFromSaved && isFromDm + ? this.renderButton( + () => this.handleBlockUser(roomFromRid.rid, roomUser._id, !blocker), + 'ignore', + I18n.t(`${blocker ? 'Unblock' : 'Block'}_user`), + true + ) + : null} ); }; diff --git a/app/views/RoomMembersView/helpers.ts b/app/views/RoomMembersView/helpers.ts index 696de2c56..ff622dbf4 100644 --- a/app/views/RoomMembersView/helpers.ts +++ b/app/views/RoomMembersView/helpers.ts @@ -217,20 +217,6 @@ export const handleRemoveUserFromRoom = async ( } }; -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, diff --git a/app/views/RoomMembersView/index.tsx b/app/views/RoomMembersView/index.tsx index 80d4a337a..99f20d35c 100644 --- a/app/views/RoomMembersView/index.tsx +++ b/app/views/RoomMembersView/index.tsx @@ -16,6 +16,7 @@ import { TSubscriptionModel, TUserModel } from '../../definitions'; import I18n from '../../i18n'; import { useAppSelector, usePermissions } from '../../lib/hooks'; import { getRoomTitle, isGroupChat } from '../../lib/methods/helpers'; +import { handleIgnore } from '../../lib/methods/helpers/handleIgnore'; import { showConfirmationAlert } from '../../lib/methods/helpers/info'; import log from '../../lib/methods/helpers/log'; import scrollPersistTaps from '../../lib/methods/helpers/scrollPersistTaps'; @@ -28,7 +29,6 @@ import ActionsSection from './components/ActionsSection'; import { fetchRole, fetchRoomMembersRoles, - handleIgnore, handleLeader, handleModerator, handleMute, @@ -207,7 +207,7 @@ const RoomMembersView = (): React.ReactElement => { options.push({ icon: 'ignore', title: I18n.t(isIgnored ? 'Unignore' : 'Ignore'), - onPress: () => handleIgnore(selectedUser, !isIgnored, room.rid), + onPress: () => handleIgnore(selectedUser._id, !isIgnored, room.rid), testID: 'action-sheet-ignore-user' }); } diff --git a/app/views/RoomView/index.tsx b/app/views/RoomView/index.tsx index 4b76660a2..03786b4c3 100644 --- a/app/views/RoomView/index.tsx +++ b/app/views/RoomView/index.tsx @@ -1109,10 +1109,13 @@ class RoomView extends React.Component { navToRoomInfo = (navParam: any) => { const { navigation, user, isMasterDetail } = this.props; + const { room } = this.state; + logEvent(events[`ROOM_GO_${navParam.t === 'd' ? 'USER' : 'ROOM'}_INFO`]); if (navParam.rid === user.id) { return; } + navParam.fromRid = room.rid; if (isMasterDetail) { navParam.showCloseModal = true; // @ts-ignore diff --git a/e2e/data.ts b/e2e/data.ts index 5947a86d9..63bb8c0c3 100644 --- a/e2e/data.ts +++ b/e2e/data.ts @@ -51,6 +51,9 @@ const data = { detoxpublicprotected: { name: 'detox-public-protected', joinCode: '123' + }, + detoxpublicignore: { + name: `detox-public-ignore-${value}` } }, groups: { diff --git a/e2e/tests/room/10-ignoreuser.spec.ts b/e2e/tests/room/10-ignoreuser.spec.ts new file mode 100644 index 000000000..9f057253a --- /dev/null +++ b/e2e/tests/room/10-ignoreuser.spec.ts @@ -0,0 +1,103 @@ +import { expect } from 'detox'; + +import data from '../../data'; +import { navigateToLogin, login, searchRoom, sleep, platformTypes, TTextMatcher, tapBack } from '../../helpers/app'; +import { sendMessage } from '../../helpers/data_setup'; + +async function navigateToRoom(user: string) { + await searchRoom(`${user}`); + await element(by.id(`rooms-list-view-item-${user}`)).tap(); + await waitFor(element(by.id('room-view'))) + .toBeVisible() + .withTimeout(5000); +} + +async function navigateToInfoView() { + await element(by.id('room-header')).tap(); + await waitFor(element(by.id('room-actions-view'))) + .toExist() + .withTimeout(5000); + await element(by.id('room-actions-info')).tap(); + await waitFor(element(by.id('room-info-view'))) + .toExist() + .withTimeout(2000); +} + +describe('Ignore/Block User', () => { + const user = data.users.alternate.username; + let textMatcher: TTextMatcher; + + before(async () => { + await device.launchApp({ permissions: { notifications: 'YES' }, delete: true }); + ({ textMatcher } = platformTypes[device.getPlatform()]); + await navigateToLogin(); + await login(data.users.regular.username, data.users.regular.password); + }); + + describe('Usage', () => { + describe('Block user from DM', () => { + it('should go to user info view', async () => { + await navigateToRoom(user); + await navigateToInfoView(); + }); + it('should block user', async () => { + await expect(element(by.id('room-info-view-ignore').withDescendant(by[textMatcher]('Block user')))).toExist(); + await element(by.id('room-info-view-ignore')).tap(); + await waitFor(element(by.id('room-info-view-ignore').withDescendant(by[textMatcher]('Unblock user')))) + .toExist() + .withTimeout(2000); + await tapBack(); + await waitFor(element(by.id('room-actions-view'))) + .toBeVisible() + .withTimeout(5000); + await tapBack(); + await expect(element(by[textMatcher]('This room is blocked'))).toExist(); + }); + + it('should unblock user', async () => { + await navigateToInfoView(); + await element(by.id('room-info-view-ignore')).tap(); + await expect(element(by.id('room-info-view-ignore').withDescendant(by[textMatcher]('Block user')))).toExist(); + await tapBack(); + await waitFor(element(by.id('room-actions-view'))) + .toBeVisible() + .withTimeout(5000); + await tapBack(); + await expect(element(by.id('messagebox'))).toBeVisible(); + await tapBack(); + }); + }); + describe('Ignore user from Message', () => { + it('should ignore user from message', async () => { + const channelName = data.channels.detoxpublicignore.name; + await navigateToRoom(channelName); + await element(by.id('room-view-join-button')).tap(); + await sleep(300); + await sendMessage(data.users.alternate, channelName, 'message-01'); + await sendMessage(data.users.alternate, channelName, 'message-02'); + await waitFor(element(by[textMatcher](user)).atIndex(0)) + .toExist() + .withTimeout(30000); + await sleep(300); + await element(by[textMatcher](user)).atIndex(0).tap(); + await expect(element(by.id('room-info-view-ignore').withDescendant(by[textMatcher]('Ignore')))).toExist(); + await element(by.id('room-info-view-ignore')).tap(); + await expect(element(by.id('room-info-view-ignore').withDescendant(by[textMatcher]('Unignore')))).toExist(); + await tapBack(); + }); + it('should tap to display message', async () => { + await expect(element(by[textMatcher]('Message ignored. Tap to display it.')).atIndex(0)).toExist(); + await element(by[textMatcher]('Message ignored. Tap to display it.')).atIndex(0).tap(); + await waitFor(element(by[textMatcher](user))) + .toBeVisible() + .withTimeout(1000); + await element(by[textMatcher](user)).atIndex(0).tap(); + await expect(element(by.id('room-info-view-ignore').withDescendant(by[textMatcher]('Unignore')))).toExist(); + await element(by.id('room-info-view-ignore')).tap(); + await expect(element(by.id('room-info-view-ignore').withDescendant(by[textMatcher]('Ignore')))).toExist(); + await tapBack(); + await expect(element(by[textMatcher]('message-02')).atIndex(0)).toBeVisible(); + }); + }); + }); +});