diff --git a/app/containers/EmojiPicker/index.js b/app/containers/EmojiPicker/index.js index 9860dea9..3fe6f937 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 5de570bc..519307a3 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 27fdf262..d59b8d3c 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 bba05811..10f65c51 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 6c1b1526..455a2d78 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 2974a1e8..7447d4fa 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 9c5ff08f..3e960e9e 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 8248638b..012192eb 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 93c35559..e1862218 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 a562b9eb..c79f19da 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 03376214..454348e3 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 522aade3..79534460 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 e0b4feac..64133166 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 b9fea477..3b109628 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() {