From 531f3d014708242b23a65aef2ec52a135a40e189 Mon Sep 17 00:00:00 2001
From: Reinaldo Neto <47038980+reinaldonetof@users.noreply.github.com>
Date: Thu, 13 Oct 2022 18:29:55 -0300
Subject: [PATCH] [IMPROVE] Add ignore user to user profile (#4600)

* button ignore and pased the param

* load room from database and check is is ignored

* move handleIgnore to lib/method/helpers

* Ignore and Unignore, also reactivity

* block and unblock user

* pass fromRid from actionView to InfoView too

* remove console.log

* unsubscribe subscriptionFrom

* block and unblock user from dm

* test to block user and ignore user

* minor tweak

* tweak data

* minor tweak

* add test before tapBack

* refactor names
---
 app/lib/methods/helpers/handleIgnore.ts |  19 +++++
 app/lib/methods/helpers/log/events.ts   |   1 +
 app/stacks/types.ts                     |   1 +
 app/views/RoomActionsView/index.tsx     |   3 +-
 app/views/RoomInfoView/index.tsx        | 107 +++++++++++++++++++-----
 app/views/RoomMembersView/helpers.ts    |  14 ----
 app/views/RoomMembersView/index.tsx     |   4 +-
 app/views/RoomView/index.tsx            |   3 +
 e2e/data.ts                             |   3 +
 e2e/tests/room/10-ignoreuser.spec.ts    | 103 +++++++++++++++++++++++
 10 files changed, 220 insertions(+), 38 deletions(-)
 create mode 100644 app/lib/methods/helpers/handleIgnore.ts
 create mode 100644 e2e/tests/room/10-ignoreuser.spec.ts

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<IRoomActionsViewProps, IRoomAction
 								rid,
 								t,
 								room,
-								member
+								member,
+								fromRid: room.rid
 							}
 						})
 					}
diff --git a/app/views/RoomInfoView/index.tsx b/app/views/RoomInfoView/index.tsx
index 0140cf558..9c7c9dde7 100644
--- a/app/views/RoomInfoView/index.tsx
+++ b/app/views/RoomInfoView/index.tsx
@@ -36,6 +36,8 @@ import { ILivechatVisitor } from '../../definitions/ILivechatVisitor';
 import { callJitsi } from '../../lib/methods';
 import { getRoomTitle, getUidDirectMessage, hasPermission } from '../../lib/methods/helpers';
 import { Services } from '../../lib/services';
+import { getSubscriptionByRoomId } from '../../lib/database/services/Subscription';
+import { handleIgnore } from '../../lib/methods/helpers/handleIgnore';
 
 interface IGetRoomTitle {
 	room: ISubscription;
@@ -108,6 +110,7 @@ interface IRoomInfoViewState {
 	room: ISubscription;
 	roomUser: IUserParsed | ILivechatVisitorModified;
 	showEdit: boolean;
+	roomFromRid?: TSubscriptionModel;
 }
 
 class RoomInfoView extends React.Component<IRoomInfoViewProps, IRoomInfoViewState> {
@@ -121,22 +124,29 @@ class RoomInfoView extends React.Component<IRoomInfoViewProps, IRoomInfoViewStat
 
 	private roomObservable?: Observable<TSubscriptionModel>;
 
+	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<IRoomInfoViewProps, IRoomInfoViewStat
 		if (this.subscription && this.subscription.unsubscribe) {
 			this.subscription.unsubscribe();
 		}
+		if (this.subscriptionRoomFromRid && this.subscriptionRoomFromRid.unsubscribe) {
+			this.subscriptionRoomFromRid.unsubscribe();
+		}
 		if (this.unsubscribeFocus) {
 			this.unsubscribeFocus();
 		}
@@ -266,6 +279,19 @@ class RoomInfoView extends React.Component<IRoomInfoViewProps, IRoomInfoViewStat
 		}
 	};
 
+	loadRoomFromRid = async () => {
+		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<IRoomInfoViewProps, IRoomInfoViewStat
 		}
 	};
 
+	handleCreateDirectMessage = async (onPress: () => 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<IRoomInfoViewProps, IRoomInfoViewStat
 		);
 	};
 
-	renderButton = (onPress: () => 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 (
-			<BorderlessButton onPress={onActionPress} style={styles.roomButton}>
-				<CustomIcon name={iconName} size={30} color={themes[theme].actionTintColor} />
-				<Text style={[styles.roomButtonText, { color: themes[theme].actionTintColor }]}>{text}</Text>
+			<BorderlessButton testID={`room-info-view-${iconName}`} onPress={onPress} style={styles.roomButton}>
+				<CustomIcon name={iconName} size={30} color={color} />
+				<Text style={[styles.roomButtonText, { color }]}>{text}</Text>
 			</BorderlessButton>
 		);
 	};
 
 	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 (
 			<View style={styles.roomButtonsContainer}>
-				{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}
 			</View>
 		);
 	};
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<IRoomViewProps, IRoomViewState> {
 
 	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();
+			});
+		});
+	});
+});