From 2019ec58cecceb049d5a06afd0887002fab153aa Mon Sep 17 00:00:00 2001
From: Diego Mello <diegolmello@gmail.com>
Date: Thu, 4 Apr 2019 15:08:40 -0300
Subject: [PATCH] [FIX] Add Realm.safeAddListener (#785)

---
 app/containers/EmojiPicker/index.js       |  6 +++---
 app/lib/realm.js                          | 20 +++++++++++++++++++-
 app/lib/rocketchat.js                     |  4 ++--
 app/views/NewMessageView.js               |  4 ++--
 app/views/RoomActionsView/index.js        |  4 ++--
 app/views/RoomInfoEditView/index.js       |  4 ++--
 app/views/RoomInfoView/index.js           |  4 ++--
 app/views/RoomMembersView/index.js        |  4 ++--
 app/views/RoomView/List.js                |  4 ++--
 app/views/RoomView/UploadProgress.js      |  4 ++--
 app/views/RoomView/index.js               |  4 ++--
 app/views/RoomsListView/ServerDropdown.js |  4 ++--
 app/views/RoomsListView/index.js          | 16 ++++++++--------
 app/views/SelectedUsersView.js            |  4 ++--
 14 files changed, 52 insertions(+), 34 deletions(-)

diff --git a/app/containers/EmojiPicker/index.js b/app/containers/EmojiPicker/index.js
index 9860dea97..3fe6f9371 100644
--- a/app/containers/EmojiPicker/index.js
+++ b/app/containers/EmojiPicker/index.js
@@ -10,7 +10,7 @@ import TabBar from './TabBar';
 import EmojiCategory from './EmojiCategory';
 import styles from './styles';
 import categories from './categories';
-import database from '../../lib/realm';
+import database, { safeAddListener } from '../../lib/realm';
 import { emojisByCategory } from '../../emojis';
 import protectedFunction from '../../lib/methods/helpers/protectedFunction';
 
@@ -45,8 +45,8 @@ export default class EmojiPicker extends Component {
 		this.updateFrequentlyUsed();
 		this.updateCustomEmojis();
 		requestAnimationFrame(() => this.setState({ show: true }));
-		this.frequentlyUsed.addListener(this.updateFrequentlyUsed);
-		this.customEmojis.addListener(this.updateCustomEmojis);
+		safeAddListener(this.frequentlyUsed, this.updateFrequentlyUsed);
+		safeAddListener(this.customEmojis, this.updateCustomEmojis);
 	}
 
 	shouldComponentUpdate(nextProps, nextState) {
diff --git a/app/lib/realm.js b/app/lib/realm.js
index 5de570bc7..519307a3d 100644
--- a/app/lib/realm.js
+++ b/app/lib/realm.js
@@ -346,4 +346,22 @@ class DB {
 		});
 	}
 }
-export default new DB();
+const db = new DB();
+export default db;
+
+// Realm workaround for "Cannot create asynchronous query while in a write transaction"
+// inpired from https://github.com/realm/realm-js/issues/1188#issuecomment-359223918
+export function safeAddListener(results, callback, database = db) {
+	if (!results || !results.addListener) {
+		console.log('⚠️ safeAddListener called for non addListener-compliant object');
+		return;
+	}
+
+	if (database.isInTransaction) {
+		setTimeout(() => {
+			safeAddListener(results, callback);
+		}, 50);
+	} else {
+		results.addListener(callback);
+	}
+}
diff --git a/app/lib/rocketchat.js b/app/lib/rocketchat.js
index 27fdf2629..d59b8d3c8 100644
--- a/app/lib/rocketchat.js
+++ b/app/lib/rocketchat.js
@@ -6,7 +6,7 @@ import { Rocketchat as RocketchatClient } from '@rocket.chat/sdk';
 import reduxStore from './createStore';
 import defaultSettings from '../constants/settings';
 import messagesStatus from '../constants/messagesStatus';
-import database from './realm';
+import database, { safeAddListener } from './realm';
 import log from '../utils/log';
 import { isIOS, getBundleId } from '../utils/deviceInfo';
 
@@ -65,7 +65,7 @@ const RocketChat = {
 			if (data.length) {
 				return resolve(data[0]);
 			}
-			data.addListener(() => {
+			safeAddListener(data, () => {
 				if (!data.length) { return; }
 				data.removeAllListeners();
 				resolve(data[0]);
diff --git a/app/views/NewMessageView.js b/app/views/NewMessageView.js
index bba058115..10f65c51f 100644
--- a/app/views/NewMessageView.js
+++ b/app/views/NewMessageView.js
@@ -7,7 +7,7 @@ import { connect } from 'react-redux';
 import { SafeAreaView } from 'react-navigation';
 import equal from 'deep-equal';
 
-import database from '../lib/realm';
+import database, { safeAddListener } from '../lib/realm';
 import RocketChat from '../lib/rocketchat';
 import UserItem from '../presentation/UserItem';
 import debounce from '../utils/debounce';
@@ -79,7 +79,7 @@ export default class NewMessageView extends LoggedView {
 		this.state = {
 			search: []
 		};
-		this.data.addListener(this.updateState);
+		safeAddListener(this.data, this.updateState);
 	}
 
 	shouldComponentUpdate(nextProps, nextState) {
diff --git a/app/views/RoomActionsView/index.js b/app/views/RoomActionsView/index.js
index 6c1b1526a..455a2d782 100644
--- a/app/views/RoomActionsView/index.js
+++ b/app/views/RoomActionsView/index.js
@@ -14,7 +14,7 @@ import sharedStyles from '../Styles';
 import Avatar from '../../containers/Avatar';
 import Status from '../../containers/Status';
 import Touch from '../../utils/touch';
-import database from '../../lib/realm';
+import database, { safeAddListener } from '../../lib/realm';
 import RocketChat from '../../lib/rocketchat';
 import log from '../../utils/log';
 import RoomTypeIcon from '../../containers/RoomTypeIcon';
@@ -81,7 +81,7 @@ export default class RoomActionsView extends LoggedView {
 		} else if (room.t === 'd') {
 			this.updateRoomMember();
 		}
-		this.rooms.addListener(this.updateRoom);
+		safeAddListener(this.rooms, this.updateRoom);
 	}
 
 	shouldComponentUpdate(nextProps, nextState) {
diff --git a/app/views/RoomInfoEditView/index.js b/app/views/RoomInfoEditView/index.js
index 2974a1e81..7447d4fa2 100644
--- a/app/views/RoomInfoEditView/index.js
+++ b/app/views/RoomInfoEditView/index.js
@@ -14,7 +14,7 @@ import sharedStyles from '../Styles';
 import styles from './styles';
 import scrollPersistTaps from '../../utils/scrollPersistTaps';
 import { showErrorAlert, showToast } from '../../utils/info';
-import database from '../../lib/realm';
+import database, { safeAddListener } from '../../lib/realm';
 import RocketChat from '../../lib/rocketchat';
 import RCTextInput from '../../containers/TextInput';
 import Loading from '../../containers/Loading';
@@ -77,7 +77,7 @@ export default class RoomInfoEditView extends LoggedView {
 	componentDidMount() {
 		this.updateRoom();
 		this.init();
-		this.rooms.addListener(this.updateRoom);
+		safeAddListener(this.rooms, this.updateRoom);
 		const { room } = this.state;
 		this.permissions = RocketChat.hasPermission(PERMISSIONS_ARRAY, room.rid);
 	}
diff --git a/app/views/RoomInfoView/index.js b/app/views/RoomInfoView/index.js
index 9c5ff08f8..3e960e9e7 100644
--- a/app/views/RoomInfoView/index.js
+++ b/app/views/RoomInfoView/index.js
@@ -11,7 +11,7 @@ import Status from '../../containers/Status';
 import Avatar from '../../containers/Avatar';
 import styles from './styles';
 import sharedStyles from '../Styles';
-import database from '../../lib/realm';
+import database, { safeAddListener } from '../../lib/realm';
 import RocketChat from '../../lib/rocketchat';
 import log from '../../utils/log';
 import RoomTypeIcon from '../../containers/RoomTypeIcon';
@@ -87,7 +87,7 @@ export default class RoomInfoView extends LoggedView {
 	}
 
 	async componentDidMount() {
-		this.rooms.addListener(this.updateRoom);
+		safeAddListener(this.rooms, this.updateRoom);
 		const { room } = this.state;
 		const permissions = RocketChat.hasPermission([PERMISSION_EDIT_ROOM], room.rid);
 		if (permissions[PERMISSION_EDIT_ROOM]) {
diff --git a/app/views/RoomMembersView/index.js b/app/views/RoomMembersView/index.js
index 8248638b5..012192ebb 100644
--- a/app/views/RoomMembersView/index.js
+++ b/app/views/RoomMembersView/index.js
@@ -11,7 +11,7 @@ import styles from './styles';
 import UserItem from '../../presentation/UserItem';
 import scrollPersistTaps from '../../utils/scrollPersistTaps';
 import RocketChat from '../../lib/rocketchat';
-import database from '../../lib/realm';
+import database, { safeAddListener } from '../../lib/realm';
 import { showToast } from '../../utils/info';
 import log from '../../utils/log';
 import { vibrate } from '../../utils/vibration';
@@ -81,7 +81,7 @@ export default class RoomMembersView extends LoggedView {
 
 	componentDidMount() {
 		this.fetchMembers();
-		this.rooms.addListener(this.updateRoom);
+		safeAddListener(this.rooms, this.updateRoom);
 
 		const { navigation } = this.props;
 		navigation.setParams({ toggleStatus: this.toggleStatus });
diff --git a/app/views/RoomView/List.js b/app/views/RoomView/List.js
index 93c355592..e1862218e 100644
--- a/app/views/RoomView/List.js
+++ b/app/views/RoomView/List.js
@@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
 import { responsive } from 'react-native-responsive-ui';
 
 import styles from './styles';
-import database from '../../lib/realm';
+import database, { safeAddListener } from '../../lib/realm';
 import scrollPersistTaps from '../../utils/scrollPersistTaps';
 import debounce from '../../utils/debounce';
 import RocketChat from '../../lib/rocketchat';
@@ -36,7 +36,7 @@ export class List extends React.Component {
 			messages: this.data.slice(),
 			showScollToBottomButton: false
 		};
-		this.data.addListener(this.updateState);
+		safeAddListener(this.data, this.updateState);
 	}
 
 	// shouldComponentUpdate(nextProps, nextState) {
diff --git a/app/views/RoomView/UploadProgress.js b/app/views/RoomView/UploadProgress.js
index a562b9ebc..c79f19da4 100644
--- a/app/views/RoomView/UploadProgress.js
+++ b/app/views/RoomView/UploadProgress.js
@@ -6,7 +6,7 @@ import PropTypes from 'prop-types';
 import { responsive } from 'react-native-responsive-ui';
 import equal from 'deep-equal';
 
-import database from '../../lib/realm';
+import database, { safeAddListener } from '../../lib/realm';
 import RocketChat from '../../lib/rocketchat';
 import log from '../../utils/log';
 import I18n from '../../i18n';
@@ -74,7 +74,7 @@ export default class UploadProgress extends Component {
 		};
 		const { rid } = this.props;
 		this.uploads = database.objects('uploads').filtered('rid = $0', rid);
-		this.uploads.addListener(this.updateUploads);
+		safeAddListener(this.uploads, this.updateUploads);
 	}
 
 	componentDidMount() {
diff --git a/app/views/RoomView/index.js b/app/views/RoomView/index.js
index 03376214e..454348e36 100644
--- a/app/views/RoomView/index.js
+++ b/app/views/RoomView/index.js
@@ -13,7 +13,7 @@ import { openRoom as openRoomAction, closeRoom as closeRoomAction } from '../../
 import { toggleReactionPicker as toggleReactionPickerAction, actionsShow as actionsShowAction } from '../../actions/messages';
 import LoggedView from '../View';
 import { List } from './List';
-import database from '../../lib/realm';
+import database, { safeAddListener } from '../../lib/realm';
 import RocketChat from '../../lib/rocketchat';
 import Message from '../../containers/message';
 import MessageActions from '../../containers/MessageActions';
@@ -114,7 +114,7 @@ export default class RoomView extends LoggedView {
 				() => this.updateRoom()
 			);
 		}
-		this.rooms.addListener(this.updateRoom);
+		safeAddListener(this.rooms, this.updateRoom);
 		this.internalSetState({ loaded: true });
 	}
 
diff --git a/app/views/RoomsListView/ServerDropdown.js b/app/views/RoomsListView/ServerDropdown.js
index 522aade30..795344609 100644
--- a/app/views/RoomsListView/ServerDropdown.js
+++ b/app/views/RoomsListView/ServerDropdown.js
@@ -11,7 +11,7 @@ import { toggleServerDropdown as toggleServerDropdownAction } from '../../action
 import { selectServerRequest as selectServerRequestAction } from '../../actions/server';
 import { appStart as appStartAction } from '../../actions';
 import styles from './styles';
-import database from '../../lib/realm';
+import database, { safeAddListener } from '../../lib/realm';
 import Touch from '../../utils/touch';
 import RocketChat from '../../lib/rocketchat';
 import I18n from '../../i18n';
@@ -46,7 +46,7 @@ class ServerDropdown extends Component {
 			servers: this.servers
 		};
 		this.animatedValue = new Animated.Value(0);
-		this.servers.addListener(this.updateState);
+		safeAddListener(this.servers, this.updateState);
 	}
 
 	componentDidMount() {
diff --git a/app/views/RoomsListView/index.js b/app/views/RoomsListView/index.js
index e0b4feac1..64133166a 100644
--- a/app/views/RoomsListView/index.js
+++ b/app/views/RoomsListView/index.js
@@ -10,7 +10,7 @@ import Orientation from 'react-native-orientation-locker';
 
 import SearchBox from '../../containers/SearchBox';
 import ConnectionBadge from '../../containers/ConnectionBadge';
-import database from '../../lib/realm';
+import database, { safeAddListener } from '../../lib/realm';
 import RocketChat from '../../lib/rocketchat';
 import RoomItem, { ROW_HEIGHT } from '../../presentation/RoomItem';
 import styles from './styles';
@@ -277,7 +277,7 @@ export default class RoomsListView extends LoggedView {
 			if (showUnread) {
 				this.unread = this.data.filtered('archived != true && open == true').filtered('(unread > 0 || alert == true)');
 				unread = this.removeRealmInstance(this.unread);
-				this.unread.addListener(debounce(() => this.internalSetState({ unread: this.removeRealmInstance(this.unread) }), 300));
+				safeAddListener(this.unread, debounce(() => this.internalSetState({ unread: this.removeRealmInstance(this.unread) }), 300));
 			} else {
 				this.removeListener(unread);
 			}
@@ -285,7 +285,7 @@ export default class RoomsListView extends LoggedView {
 			if (showFavorites) {
 				this.favorites = this.data.filtered('f == true');
 				favorites = this.removeRealmInstance(this.favorites);
-				this.favorites.addListener(debounce(() => this.internalSetState({ favorites: this.removeRealmInstance(this.favorites) }), 300));
+				safeAddListener(this.favorites, debounce(() => this.internalSetState({ favorites: this.removeRealmInstance(this.favorites) }), 300));
 			} else {
 				this.removeListener(favorites);
 			}
@@ -307,10 +307,10 @@ export default class RoomsListView extends LoggedView {
 				this.livechat = this.data.filtered('t == $0', 'l');
 				livechat = this.removeRealmInstance(this.livechat);
 
-				this.channels.addListener(debounce(() => this.internalSetState({ channels: this.removeRealmInstance(this.channels) }), 300));
-				this.privateGroup.addListener(debounce(() => this.internalSetState({ privateGroup: this.removeRealmInstance(this.privateGroup) }), 300));
-				this.direct.addListener(debounce(() => this.internalSetState({ direct: this.removeRealmInstance(this.direct) }), 300));
-				this.livechat.addListener(debounce(() => this.internalSetState({ livechat: this.removeRealmInstance(this.livechat) }), 300));
+				safeAddListener(this.channels, debounce(() => this.internalSetState({ channels: this.removeRealmInstance(this.channels) }), 300));
+				safeAddListener(this.privateGroup, debounce(() => this.internalSetState({ privateGroup: this.removeRealmInstance(this.privateGroup) }), 300));
+				safeAddListener(this.direct, debounce(() => this.internalSetState({ direct: this.removeRealmInstance(this.direct) }), 300));
+				safeAddListener(this.livechat, debounce(() => this.internalSetState({ livechat: this.removeRealmInstance(this.livechat) }), 300));
 				this.removeListener(this.chats);
 			} else {
 				// chats
@@ -321,7 +321,7 @@ export default class RoomsListView extends LoggedView {
 				}
 				chats = this.removeRealmInstance(this.chats);
 
-				this.chats.addListener(debounce(() => this.internalSetState({ chats: this.removeRealmInstance(this.chats) }), 300));
+				safeAddListener(this.chats, debounce(() => this.internalSetState({ chats: this.removeRealmInstance(this.chats) }), 300));
 				this.removeListener(this.channels);
 				this.removeListener(this.privateGroup);
 				this.removeListener(this.direct);
diff --git a/app/views/SelectedUsersView.js b/app/views/SelectedUsersView.js
index b9fea4775..3b1096283 100644
--- a/app/views/SelectedUsersView.js
+++ b/app/views/SelectedUsersView.js
@@ -10,7 +10,7 @@ import equal from 'deep-equal';
 import {
 	addUser as addUserAction, removeUser as removeUserAction, reset as resetAction, setLoading as setLoadingAction
 } from '../actions/selectedUsers';
-import database from '../lib/realm';
+import database, { safeAddListener } from '../lib/realm';
 import RocketChat from '../lib/rocketchat';
 import UserItem from '../presentation/UserItem';
 import Loading from '../containers/Loading';
@@ -88,7 +88,7 @@ export default class SelectedUsersView extends LoggedView {
 		this.state = {
 			search: []
 		};
-		this.data.addListener(this.updateState);
+		safeAddListener(this.data, this.updateState);
 	}
 
 	componentDidMount() {
-- 
GitLab