[CHORE] Migrate to Watermelon (#1171)

* Install

* Create subscriptions

* Subscription observing and sorting

* Saving last message

* Stash

* Stash

* stash

* Stash

* Rooms list listing :)

* Animated set state

* Search working

* Fix load rooms on login

* stash db class

* set active db with path

* Remove db on logout

* stash

* Created updateMessages

* Inserting/updating threads

* Persisting thread messages

* Removed unused list

* Loading messages from watermelon

* Debounce updates and rerender message

* optional fields

* Fix realm conflict issues

* Fix some render issues

* stash

* List mount

* stash

* fix message id

* Fix tmsg

* - Save subscription.rid as id on watermelon and _id as _id
- Send room as param to room view

* Throttle room updates

* stash

* comment removeClippedSubviews

* Fetch thread name

* try/catch updateMessages

* Show loading while RoomView.init is still running

* stash

* Fix updateMessages

* Threads

* Delete message

* Permalink

* Pin

* Star

* Report

* MessageActions refactor

* Edit message

* Reply message

* Add reaction

* Auto translate

* Fix connection issues

* Mark message as error if something happened on the call

* Error actions

* get custom emoji

* Always run console.log when __DEV__

* Try to create serversDB

* Don't call updateMessages. Execute that entire logic for one message id instead.

* Refactor update messages

* ServersDB User [Realm -> Watermelon]

* Fix models

* Custom emojis

* Custom emojis on emoji picker

* Frequently used emojis

* Fix add reaction on message

* stash

* Fix

* Read messages

* Fix thread

* Fetch thread header

* Follow/unfollow thread

* Fix thread

* Upload file

* Thread tweak

* Realm -> Watermelon [Share Extension]

* Add RoomsUpdatedAt to Servers Table

* Settings

* Settings

* Fix logout

* SendFileMessage ServersDB

* ServersDB on serverDropdown

* Remove serversDB from Realm

* Load thread messages

* Delete message

* Improve getSettings

* Improve

* Remove subscription

* Remove update

* Update room via socket

* Small refactor

* Fix logout and improve migration

* Refactor updateMessages

* Improve migration

* Remove unnecessary update

* Revert remove runAfterInteractions

* Fix serverDropdown

* Fix merge

* Init room actions Watermelon

* Room actions Watermelon

* Remove realm on room members

* Room swipe -> Watermelon

* Fix hideChannel

* Get roles watermelon

* Get permissions watermelon

* Users typing + memory db

* Auto translate watermelon

* New Message View

* Selected Users View

* try/catch

* Get Slash Commands watermelon

* Slash Commands message box

* Custom emojis message box

* Get rooms message box

* Room info view

* Room info edit

* Save active users

* Small refactor

* Message Actions

* hasPermission await

* last hasPermission fix

* Active users on redux

* Add user roles

* Users typing on redux and remove memory db

* Fix saga delay

* Fix few issues

* Fix slash commands preview

* Draft message

* Add muted

* Unread count watermelon

* Remove realm

* Fiz RoomItem rerenders

* Remove realm config

* Rerender status update on RoomItem

* Refactor RoomsListView

* Fix load missed messages

* Fix room update

* Message refactor

* Fixing lint

* removeClippedSubviews on iOS only

* Added few interaction managers

* Fix few rerenders

* Fix RoomItem status typo

* Fix RoomView.SCU

* Fix broadcast

* Fix user status on RoomActionsView

* Fix RocketChat.hasPermission

* Fix database inconsistencies

* Fix few update issues

* Add rxjs and remove with observables

* Fix tests

* Remove subscriptions

* Fix RoomsListView SCU

* Change database structure and set all schemas to 1

* Fix RoomsListView search

* Fixed errors, removed rerenders and added animation

* Fixed a few errors

* Fix lint

* Fix issues caught by LGTM

* fix ios build

* Fix load unjoined channel messages

* Log on database path on startup

* Fix join channel

* Remove react-native-realm-path

* Set user status on login.user reducer

* Fix status not rendering on RoomsListView

* Fix few reducers

* Fix users going offline

* Never use "watermelon" term directly. Replaced by "database"

* Fix custom emoji

* Creating room from app must update roomUpdatedAt

* Log subscribeRoom start

* Fix room subscribe right after creating a DM

* Refactor is read only on messages actions

* Fix typo

* Fix typo

* Review

* Fix schema

* Fix muted & freq emoji & unpin & unstar

* Remove throttleTime to room info & fix reset on edit room

* Fix openServerDropdown spec & Fix unarchive

* Fix MessageAction

* Refactor RoomInfoEditView

* Remove unnecessary condition

* Remove unnecessary condition

* Remove unnecessary condition

* Remove get database

* Rename Command.js to SlashCommand.js

* Create sanitizer util

* Fix indentation

* Create subscription.t index

* Refactor queries on RoomsListView

* Create subscription.name index

* Fix getPermissions

* Fix indentation

* Add missing await

* Fix rocketchat.hasPermission

* Unnecessary change

* Star, pin e delete message refactored

* Refactor customEmojis reducer

* Remove code

* Remove logs

* Remove throttle

* Call this.init on foreground focus on RoomView

* Bump servers schema migration

* Always mark message as sent after a success

* Fetch only messages needed on updateMessages

* Just leave a comment for now

* Fetch only subscriptions returned by fetch

* Set room param on RoomView header in find room

* Update kotlin

* Fix auto translate constructor

* Fix few setState on constructor

* Fix empty room image blinking while mounting

* Improve fetch/persist execution for custom emojis, permissions and settings

* Query only user tapped on RoomMembersView

* Fix typo on canOpenRoom
This commit is contained in:
Diego Mello 2019-09-16 17:26:32 -03:00 committed by GitHub
parent 2d8b1c5ac2
commit 9ba37107c7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
127 changed files with 15653 additions and 14650 deletions

View File

@ -1,26 +0,0 @@
export default class Realm {
schema = [];
data = [];
constructor(params) {
require('lodash').each(params.schema, (schema) => {
this.data[schema.name] = [];
this.data[schema.name].filtered = () => this.data[schema.name];
});
this.schema = params.schema;
}
objects(schemaName) {
return this.data[schemaName];
}
write = (fn) => {
fn();
}
create(schemaName, data) {
this.data[schemaName].push(data);
return data;
}
}

View File

@ -1,4 +1,5 @@
apply plugin: "com.android.application" apply plugin: "com.android.application"
apply plugin: 'kotlin-android'
apply plugin: "io.fabric" apply plugin: "io.fabric"
apply plugin: "com.google.firebase.firebase-perf" apply plugin: "com.google.firebase.firebase-perf"
apply plugin: 'com.bugsnag.android.gradle' apply plugin: 'com.bugsnag.android.gradle'
@ -204,6 +205,7 @@ android {
dependencies { dependencies {
addUnimodulesDependencies() addUnimodulesDependencies()
implementation project(':watermelondb')
implementation project(':reactnativenotifications') implementation project(':reactnativenotifications')
implementation project(":reactnativekeyboardinput") implementation project(":reactnativekeyboardinput")
implementation fileTree(dir: "libs", include: ["*.jar"]) implementation fileTree(dir: "libs", include: ["*.jar"])

View File

@ -31,6 +31,8 @@ import io.invertase.firebase.fabric.crashlytics.RNFirebaseCrashlyticsPackage;
import io.invertase.firebase.analytics.RNFirebaseAnalyticsPackage; import io.invertase.firebase.analytics.RNFirebaseAnalyticsPackage;
import io.invertase.firebase.perf.RNFirebasePerformancePackage; import io.invertase.firebase.perf.RNFirebasePerformancePackage;
import com.nozbe.watermelondb.WatermelonDBPackage;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
@ -53,6 +55,7 @@ public class MainApplication extends Application implements ReactApplication, IN
packages.add(new RNFirebasePerformancePackage()); packages.add(new RNFirebasePerformancePackage());
packages.add(new KeyboardInputPackage(MainApplication.this)); packages.add(new KeyboardInputPackage(MainApplication.this));
packages.add(new RNNotificationsPackage(MainApplication.this)); packages.add(new RNNotificationsPackage(MainApplication.this));
packages.add(new WatermelonDBPackage());
packages.add(new ModuleRegistryAdapter(mModuleRegistryProvider)); packages.add(new ModuleRegistryAdapter(mModuleRegistryProvider));
return packages; return packages;
} }

View File

@ -6,10 +6,7 @@ buildscript {
compileSdkVersion = 28 compileSdkVersion = 28
targetSdkVersion = 28 targetSdkVersion = 28
glideVersion = "4.9.0" glideVersion = "4.9.0"
// googlePlayServicesVersion = "17.0.0" kotlin_version = "1.3.50"
// supportLibVersion = "1.0.2"
// mediaCompatVersion = '1.0.1'
// supportV4Version = '1.0.0'
} }
repositories { repositories {
mavenLocal() mavenLocal()
@ -24,6 +21,7 @@ buildscript {
classpath 'com.google.gms:google-services:4.2.0' classpath 'com.google.gms:google-services:4.2.0'
classpath 'io.fabric.tools:gradle:1.28.1' classpath 'io.fabric.tools:gradle:1.28.1'
classpath 'com.google.firebase:perf-plugin:1.2.1' classpath 'com.google.firebase:perf-plugin:1.2.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath 'com.bugsnag:bugsnag-android-gradle-plugin:4.+' classpath 'com.bugsnag:bugsnag-android-gradle-plugin:4.+'
// NOTE: Do not place your application dependencies here; they belong // NOTE: Do not place your application dependencies here; they belong

View File

@ -2,6 +2,8 @@ apply from: '../node_modules/react-native-unimodules/gradle.groovy'
includeUnimodulesProjects() includeUnimodulesProjects()
rootProject.name = 'RocketChatRN' rootProject.name = 'RocketChatRN'
include ':watermelondb'
project(':watermelondb').projectDir = new File(rootProject.projectDir, '../node_modules/@nozbe/watermelondb/native/android')
include ':reactnativenotifications' include ':reactnativenotifications'
project(':reactnativenotifications').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-notifications/android/app') project(':reactnativenotifications').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-notifications/android/app')
include ':reactnativekeyboardinput' include ':reactnativekeyboardinput'

View File

@ -32,31 +32,7 @@ export const ROOMS = createRequestTypes('ROOMS', [
]); ]);
export const ROOM = createRequestTypes('ROOM', ['LEAVE', 'ERASE', 'USER_TYPING']); export const ROOM = createRequestTypes('ROOM', ['LEAVE', 'ERASE', 'USER_TYPING']);
export const APP = createRequestTypes('APP', ['START', 'READY', 'INIT']); export const APP = createRequestTypes('APP', ['START', 'READY', 'INIT']);
export const MESSAGES = createRequestTypes('MESSAGES', [ export const MESSAGES = createRequestTypes('MESSAGES', ['REPLY_BROADCAST']);
...defaultTypes,
'ACTIONS_SHOW',
'ACTIONS_HIDE',
'ERROR_ACTIONS_SHOW',
'ERROR_ACTIONS_HIDE',
'DELETE_REQUEST',
'DELETE_SUCCESS',
'DELETE_FAILURE',
'EDIT_INIT',
'EDIT_CANCEL',
'EDIT_REQUEST',
'EDIT_SUCCESS',
'EDIT_FAILURE',
'TOGGLE_STAR_REQUEST',
'TOGGLE_STAR_SUCCESS',
'TOGGLE_STAR_FAILURE',
'TOGGLE_PIN_REQUEST',
'TOGGLE_PIN_SUCCESS',
'TOGGLE_PIN_FAILURE',
'REPLY_INIT',
'REPLY_CANCEL',
'TOGGLE_REACTION_PICKER',
'REPLY_BROADCAST'
]);
export const CREATE_CHANNEL = createRequestTypes('CREATE_CHANNEL', [...defaultTypes]); export const CREATE_CHANNEL = createRequestTypes('CREATE_CHANNEL', [...defaultTypes]);
export const SELECTED_USERS = createRequestTypes('SELECTED_USERS', ['ADD_USER', 'REMOVE_USER', 'RESET', 'SET_LOADING']); export const SELECTED_USERS = createRequestTypes('SELECTED_USERS', ['ADD_USER', 'REMOVE_USER', 'RESET', 'SET_LOADING']);
export const SERVER = createRequestTypes('SERVER', [ export const SERVER = createRequestTypes('SERVER', [
@ -75,3 +51,6 @@ export const SORT_PREFERENCES = createRequestTypes('SORT_PREFERENCES', ['SET_ALL
export const NOTIFICATION = createRequestTypes('NOTIFICATION', ['RECEIVED', 'REMOVE']); export const NOTIFICATION = createRequestTypes('NOTIFICATION', ['RECEIVED', 'REMOVE']);
export const TOGGLE_MARKDOWN = 'TOGGLE_MARKDOWN'; export const TOGGLE_MARKDOWN = 'TOGGLE_MARKDOWN';
export const TOGGLE_CRASH_REPORT = 'TOGGLE_CRASH_REPORT'; export const TOGGLE_CRASH_REPORT = 'TOGGLE_CRASH_REPORT';
export const SET_CUSTOM_EMOJIS = 'SET_CUSTOM_EMOJIS';
export const SET_ACTIVE_USERS = 'SET_ACTIVE_USERS';
export const USERS_TYPING = createRequestTypes('USERS_TYPING', ['ADD', 'REMOVE', 'CLEAR']);

View File

@ -0,0 +1,8 @@
import { SET_ACTIVE_USERS } from './actionsTypes';
export function setActiveUsers(activeUsers) {
return {
type: SET_ACTIVE_USERS,
activeUsers
};
}

View File

@ -0,0 +1,8 @@
import * as types from './actionsTypes';
export function setCustomEmojis(emojis) {
return {
type: types.SET_CUSTOM_EMOJIS,
emojis
};
}

View File

@ -1,143 +1,5 @@
import * as types from './actionsTypes'; import * as types from './actionsTypes';
export function actionsShow(actionMessage) {
return {
type: types.MESSAGES.ACTIONS_SHOW,
actionMessage
};
}
export function actionsHide() {
return {
type: types.MESSAGES.ACTIONS_HIDE
};
}
export function errorActionsShow(actionMessage) {
return {
type: types.MESSAGES.ERROR_ACTIONS_SHOW,
actionMessage
};
}
export function errorActionsHide() {
return {
type: types.MESSAGES.ERROR_ACTIONS_HIDE
};
}
export function deleteRequest(message) {
return {
type: types.MESSAGES.DELETE_REQUEST,
message
};
}
export function deleteSuccess() {
return {
type: types.MESSAGES.DELETE_SUCCESS
};
}
export function deleteFailure() {
return {
type: types.MESSAGES.DELETE_FAILURE
};
}
export function editInit(message) {
return {
type: types.MESSAGES.EDIT_INIT,
message
};
}
export function editCancel() {
return {
type: types.MESSAGES.EDIT_CANCEL
};
}
export function editRequest(message) {
return {
type: types.MESSAGES.EDIT_REQUEST,
message
};
}
export function editSuccess() {
return {
type: types.MESSAGES.EDIT_SUCCESS
};
}
export function editFailure() {
return {
type: types.MESSAGES.EDIT_FAILURE
};
}
export function toggleStarRequest(message) {
return {
type: types.MESSAGES.TOGGLE_STAR_REQUEST,
message
};
}
export function toggleStarSuccess() {
return {
type: types.MESSAGES.TOGGLE_STAR_SUCCESS
};
}
export function toggleStarFailure() {
return {
type: types.MESSAGES.TOGGLE_STAR_FAILURE
};
}
export function togglePinRequest(message) {
return {
type: types.MESSAGES.TOGGLE_PIN_REQUEST,
message
};
}
export function togglePinSuccess() {
return {
type: types.MESSAGES.TOGGLE_PIN_SUCCESS
};
}
export function togglePinFailure(err) {
return {
type: types.MESSAGES.TOGGLE_PIN_FAILURE,
err
};
}
export function replyInit(message, mention) {
return {
type: types.MESSAGES.REPLY_INIT,
message,
mention
};
}
export function replyCancel() {
return {
type: types.MESSAGES.REPLY_CANCEL
};
}
export function toggleReactionPicker(message) {
return {
type: types.MESSAGES.TOGGLE_REACTION_PICKER,
message
};
}
export function replyBroadcast(message) { export function replyBroadcast(message) {
return { return {
type: types.MESSAGES.REPLY_BROADCAST, type: types.MESSAGES.REPLY_BROADCAST,

View File

@ -0,0 +1,21 @@
import { USERS_TYPING } from './actionsTypes';
export function addUserTyping(username) {
return {
type: USERS_TYPING.ADD,
username
};
}
export function removeUserTyping(username) {
return {
type: USERS_TYPING.REMOVE,
username
};
}
export function clearUserTyping() {
return {
type: USERS_TYPING.CLEAR
};
}

View File

@ -2,26 +2,30 @@ import React, { Component } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { ScrollView } from 'react-native'; import { ScrollView } from 'react-native';
import ScrollableTabView from 'react-native-scrollable-tab-view'; import ScrollableTabView from 'react-native-scrollable-tab-view';
import map from 'lodash/map';
import { emojify } from 'react-emojione'; import { emojify } from 'react-emojione';
import equal from 'deep-equal'; import equal from 'deep-equal';
import { connect } from 'react-redux';
import orderBy from 'lodash/orderBy';
import { sanitizedRaw } from '@nozbe/watermelondb/RawRecord';
import TabBar from './TabBar'; import TabBar from './TabBar';
import EmojiCategory from './EmojiCategory'; import EmojiCategory from './EmojiCategory';
import styles from './styles'; import styles from './styles';
import categories from './categories'; import categories from './categories';
import database, { safeAddListener } from '../../lib/realm'; import database from '../../lib/database';
import { emojisByCategory } from '../../emojis'; import { emojisByCategory } from '../../emojis';
import protectedFunction from '../../lib/methods/helpers/protectedFunction'; import protectedFunction from '../../lib/methods/helpers/protectedFunction';
import log from '../../utils/log';
const scrollProps = { const scrollProps = {
keyboardShouldPersistTaps: 'always', keyboardShouldPersistTaps: 'always',
keyboardDismissMode: 'none' keyboardDismissMode: 'none'
}; };
export default class EmojiPicker extends Component { class EmojiPicker extends Component {
static propTypes = { static propTypes = {
baseUrl: PropTypes.string.isRequired, baseUrl: PropTypes.string.isRequired,
customEmojis: PropTypes.object,
onEmojiSelected: PropTypes.func, onEmojiSelected: PropTypes.func,
tabEmojiStyle: PropTypes.object, tabEmojiStyle: PropTypes.object,
emojisPerRow: PropTypes.number, emojisPerRow: PropTypes.number,
@ -30,27 +34,27 @@ export default class EmojiPicker extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.frequentlyUsed = database.objects('frequentlyUsedEmoji').sorted('count', true); const customEmojis = Object.keys(props.customEmojis)
this.customEmojis = database.objects('customEmojis'); .filter(item => item === props.customEmojis[item].name)
.map(item => ({
content: props.customEmojis[item].name,
extension: props.customEmojis[item].extension,
isCustom: true
}));
this.state = { this.state = {
frequentlyUsed: [], frequentlyUsed: [],
customEmojis: [], customEmojis,
show: false show: false
}; };
this.updateFrequentlyUsed = this.updateFrequentlyUsed.bind(this);
this.updateCustomEmojis = this.updateCustomEmojis.bind(this);
} }
componentDidMount() { async componentDidMount() {
this.updateFrequentlyUsed(); await this.updateFrequentlyUsed();
this.updateCustomEmojis(); this.setState({ show: true });
requestAnimationFrame(() => this.setState({ show: true }));
safeAddListener(this.frequentlyUsed, this.updateFrequentlyUsed);
safeAddListener(this.customEmojis, this.updateCustomEmojis);
} }
shouldComponentUpdate(nextProps, nextState) { shouldComponentUpdate(nextProps, nextState) {
const { frequentlyUsed, customEmojis, show } = this.state; const { frequentlyUsed, show } = this.state;
const { width } = this.props; const { width } = this.props;
if (nextState.show !== show) { if (nextState.show !== show) {
return true; return true;
@ -61,64 +65,70 @@ export default class EmojiPicker extends Component {
if (!equal(nextState.frequentlyUsed, frequentlyUsed)) { if (!equal(nextState.frequentlyUsed, frequentlyUsed)) {
return true; return true;
} }
if (!equal(nextState.customEmojis, customEmojis)) {
return true;
}
return false; return false;
} }
componentWillUnmount() { onEmojiSelected = (emoji) => {
this.frequentlyUsed.removeAllListeners(); try {
this.customEmojis.removeAllListeners();
}
onEmojiSelected(emoji) {
const { onEmojiSelected } = this.props; const { onEmojiSelected } = this.props;
if (emoji.isCustom) { if (emoji.isCustom) {
const count = this._getFrequentlyUsedCount(emoji.content);
this._addFrequentlyUsed({ this._addFrequentlyUsed({
content: emoji.content, extension: emoji.extension, count, isCustom: true content: emoji.content, extension: emoji.extension, isCustom: true
}); });
onEmojiSelected(`:${ emoji.content }:`); onEmojiSelected(`:${ emoji.content }:`);
} else { } else {
const content = emoji; const content = emoji;
const count = this._getFrequentlyUsedCount(content); this._addFrequentlyUsed({ content, isCustom: false });
this._addFrequentlyUsed({ content, count, isCustom: false });
const shortname = `:${ emoji }:`; const shortname = `:${ emoji }:`;
onEmojiSelected(emojify(shortname, { output: 'unicode' }), shortname); onEmojiSelected(emojify(shortname, { output: 'unicode' }), shortname);
} }
} catch (e) {
log(e);
}
} }
// eslint-disable-next-line react/sort-comp // eslint-disable-next-line react/sort-comp
_addFrequentlyUsed = protectedFunction((emoji) => { _addFrequentlyUsed = protectedFunction(async(emoji) => {
database.write(() => { const db = database.active;
database.create('frequentlyUsedEmoji', emoji, true); const freqEmojiCollection = db.collections.get('frequently_used_emojis');
await db.action(async() => {
try {
const freqEmojiRecord = await freqEmojiCollection.find(emoji.content);
await freqEmojiRecord.update((f) => {
f.count += 1;
});
} catch (error) {
try {
await freqEmojiCollection.create((f) => {
f._raw = sanitizedRaw({ id: emoji.content }, freqEmojiCollection.schema);
Object.assign(f, emoji);
f.count = 1;
});
} catch (e) {
// Do nothing
}
}
}); });
}) })
_getFrequentlyUsedCount = (content) => { updateFrequentlyUsed = async() => {
const emojiRow = this.frequentlyUsed.filtered('content == $0', content); const db = database.active;
return emojiRow.length ? emojiRow[0].count + 1 : 1; const frequentlyUsedRecords = await db.collections.get('frequently_used_emojis').query().fetch();
} let frequentlyUsed = orderBy(frequentlyUsedRecords, ['count'], ['desc']);
frequentlyUsed = frequentlyUsed.map((item) => {
updateFrequentlyUsed() {
const frequentlyUsed = map(this.frequentlyUsed.slice(), (item) => {
if (item.isCustom) { if (item.isCustom) {
return item; return { content: item.content, extension: item.extension, isCustom: item.isCustom };
} }
return emojify(`${ item.content }`, { output: 'unicode' }); return emojify(`${ item.content }`, { output: 'unicode' });
}); });
this.setState({ frequentlyUsed }); this.setState({ frequentlyUsed });
} }
updateCustomEmojis() {
const customEmojis = map(this.customEmojis.slice(), item => ({ content: item.name, extension: item.extension, isCustom: true }));
this.setState({ customEmojis });
}
renderCategory(category, i) { renderCategory(category, i) {
const { frequentlyUsed, customEmojis } = this.state; const { frequentlyUsed, customEmojis } = this.state;
const { emojisPerRow, width, baseUrl } = this.props; const {
emojisPerRow, width, baseUrl
} = this.props;
let emojis = []; let emojis = [];
if (i === 0) { if (i === 0) {
@ -171,3 +181,9 @@ export default class EmojiPicker extends Component {
); );
} }
} }
const mapStateToProps = state => ({
customEmojis: state.customEmojis
});
export default connect(mapStateToProps)(EmojiPicker);

View File

@ -6,17 +6,8 @@ import ActionSheet from 'react-native-action-sheet';
import moment from 'moment'; import moment from 'moment';
import * as Haptics from 'expo-haptics'; import * as Haptics from 'expo-haptics';
import {
actionsHide as actionsHideAction,
deleteRequest as deleteRequestAction,
editInit as editInitAction,
replyInit as replyInitAction,
togglePinRequest as togglePinRequestAction,
toggleReactionPicker as toggleReactionPickerAction,
toggleStarRequest as toggleStarRequestAction
} from '../actions/messages';
import RocketChat from '../lib/rocketchat'; import RocketChat from '../lib/rocketchat';
import database from '../lib/realm'; import database from '../lib/database';
import I18n from '../i18n'; import I18n from '../i18n';
import log from '../utils/log'; import log from '../utils/log';
import Navigation from '../lib/Navigation'; import Navigation from '../lib/Navigation';
@ -28,14 +19,12 @@ class MessageActions extends React.Component {
static propTypes = { static propTypes = {
actionsHide: PropTypes.func.isRequired, actionsHide: PropTypes.func.isRequired,
room: PropTypes.object.isRequired, room: PropTypes.object.isRequired,
actionMessage: PropTypes.object, message: PropTypes.object,
user: PropTypes.object, user: PropTypes.object,
deleteRequest: PropTypes.func.isRequired,
editInit: PropTypes.func.isRequired, editInit: PropTypes.func.isRequired,
toggleStarRequest: PropTypes.func.isRequired, reactionInit: PropTypes.func.isRequired,
togglePinRequest: PropTypes.func.isRequired,
toggleReactionPicker: PropTypes.func.isRequired,
replyInit: PropTypes.func.isRequired, replyInit: PropTypes.func.isRequired,
isReadOnly: PropTypes.bool,
Message_AllowDeleting: PropTypes.bool, Message_AllowDeleting: PropTypes.bool,
Message_AllowDeleting_BlockDeleteInMinutes: PropTypes.number, Message_AllowDeleting_BlockDeleteInMinutes: PropTypes.number,
Message_AllowEditing: PropTypes.bool, Message_AllowEditing: PropTypes.bool,
@ -48,22 +37,27 @@ class MessageActions extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.handleActionPress = this.handleActionPress.bind(this); this.handleActionPress = this.handleActionPress.bind(this);
this.setPermissions(); }
const { Message_AllowStarring, Message_AllowPinning, Message_Read_Receipt_Store_Users } = this.props; async componentDidMount() {
await this.setPermissions();
const {
Message_AllowStarring, Message_AllowPinning, Message_Read_Receipt_Store_Users, user, room, message, isReadOnly
} = this.props;
// Cancel // Cancel
this.options = [I18n.t('Cancel')]; this.options = [I18n.t('Cancel')];
this.CANCEL_INDEX = 0; this.CANCEL_INDEX = 0;
// Reply // Reply
if (!this.isRoomReadOnly()) { if (!isReadOnly) {
this.options.push(I18n.t('Reply')); this.options.push(I18n.t('Reply'));
this.REPLY_INDEX = this.options.length - 1; this.REPLY_INDEX = this.options.length - 1;
} }
// Edit // Edit
if (this.allowEdit(props)) { if (this.allowEdit(this.props)) {
this.options.push(I18n.t('Edit')); this.options.push(I18n.t('Edit'));
this.EDIT_INDEX = this.options.length - 1; this.EDIT_INDEX = this.options.length - 1;
} }
@ -81,25 +75,25 @@ class MessageActions extends React.Component {
this.SHARE_INDEX = this.options.length - 1; this.SHARE_INDEX = this.options.length - 1;
// Quote // Quote
if (!this.isRoomReadOnly()) { if (!isReadOnly) {
this.options.push(I18n.t('Quote')); this.options.push(I18n.t('Quote'));
this.QUOTE_INDEX = this.options.length - 1; this.QUOTE_INDEX = this.options.length - 1;
} }
// Star // Star
if (Message_AllowStarring) { if (Message_AllowStarring) {
this.options.push(I18n.t(props.actionMessage.starred ? 'Unstar' : 'Star')); this.options.push(I18n.t(message.starred ? 'Unstar' : 'Star'));
this.STAR_INDEX = this.options.length - 1; this.STAR_INDEX = this.options.length - 1;
} }
// Pin // Pin
if (Message_AllowPinning) { if (Message_AllowPinning) {
this.options.push(I18n.t(props.actionMessage.pinned ? 'Unpin' : 'Pin')); this.options.push(I18n.t(message.pinned ? 'Unpin' : 'Pin'));
this.PIN_INDEX = this.options.length - 1; this.PIN_INDEX = this.options.length - 1;
} }
// Reaction // Reaction
if (!this.isRoomReadOnly() || this.canReactWhenReadOnly()) { if (!isReadOnly || this.canReactWhenReadOnly()) {
this.options.push(I18n.t('Add_Reaction')); this.options.push(I18n.t('Add_Reaction'));
this.REACTION_INDEX = this.options.length - 1; this.REACTION_INDEX = this.options.length - 1;
} }
@ -111,8 +105,8 @@ class MessageActions extends React.Component {
} }
// Toggle Auto-translate // Toggle Auto-translate
if (props.room.autoTranslate && props.actionMessage.u && props.actionMessage.u._id !== props.user.id) { if (room.autoTranslate && message.u && message.u._id !== user.id) {
this.options.push(I18n.t(props.actionMessage.autoTranslate ? 'View_Original' : 'Translate')); this.options.push(I18n.t(message.autoTranslate ? 'View_Original' : 'Translate'));
this.TOGGLE_TRANSLATION_INDEX = this.options.length - 1; this.TOGGLE_TRANSLATION_INDEX = this.options.length - 1;
} }
@ -121,7 +115,7 @@ class MessageActions extends React.Component {
this.REPORT_INDEX = this.options.length - 1; this.REPORT_INDEX = this.options.length - 1;
// Delete // Delete
if (this.allowDelete(props)) { if (this.allowDelete(this.props)) {
this.options.push(I18n.t('Delete')); this.options.push(I18n.t('Delete'));
this.DELETE_INDEX = this.options.length - 1; this.DELETE_INDEX = this.options.length - 1;
} }
@ -131,13 +125,18 @@ class MessageActions extends React.Component {
}); });
} }
setPermissions() { async setPermissions() {
try {
const { room } = this.props; const { room } = this.props;
const permissions = ['edit-message', 'delete-message', 'force-delete-message']; const permissions = ['edit-message', 'delete-message', 'force-delete-message'];
const result = RocketChat.hasPermission(permissions, room.rid); const result = await RocketChat.hasPermission(permissions, room.rid);
this.hasEditPermission = result[permissions[0]]; this.hasEditPermission = result[permissions[0]];
this.hasDeletePermission = result[permissions[1]]; this.hasDeletePermission = result[permissions[1]];
this.hasForceDeletePermission = result[permissions[2]]; this.hasForceDeletePermission = result[permissions[2]];
} catch (e) {
log(e);
}
Promise.resolve();
} }
showActionSheet = () => { showActionSheet = () => {
@ -159,12 +158,7 @@ class MessageActions extends React.Component {
} }
} }
isOwn = props => props.actionMessage.u && props.actionMessage.u._id === props.user.id; isOwn = props => props.message.u && props.message.u._id === props.user.id;
isRoomReadOnly = () => {
const { room } = this.props;
return room.ro;
}
canReactWhenReadOnly = () => { canReactWhenReadOnly = () => {
const { room } = this.props; const { room } = this.props;
@ -172,7 +166,7 @@ class MessageActions extends React.Component {
} }
allowEdit = (props) => { allowEdit = (props) => {
if (this.isRoomReadOnly()) { if (props.isReadOnly) {
return false; return false;
} }
const editOwn = this.isOwn(props); const editOwn = this.isOwn(props);
@ -184,8 +178,8 @@ class MessageActions extends React.Component {
const blockEditInMinutes = Message_AllowEditing_BlockEditInMinutes; const blockEditInMinutes = Message_AllowEditing_BlockEditInMinutes;
if (blockEditInMinutes) { if (blockEditInMinutes) {
let msgTs; let msgTs;
if (props.actionMessage.ts != null) { if (props.message.ts != null) {
msgTs = moment(props.actionMessage.ts); msgTs = moment(props.message.ts);
} }
let currentTsDiff; let currentTsDiff;
if (msgTs != null) { if (msgTs != null) {
@ -197,12 +191,12 @@ class MessageActions extends React.Component {
} }
allowDelete = (props) => { allowDelete = (props) => {
if (this.isRoomReadOnly()) { if (props.isReadOnly) {
return false; return false;
} }
// Prevent from deleting thread start message when positioned inside the thread // Prevent from deleting thread start message when positioned inside the thread
if (props.tmid && props.tmid === props.actionMessage._id) { if (props.tmid && props.tmid === props.message.id) {
return false; return false;
} }
const deleteOwn = this.isOwn(props); const deleteOwn = this.isOwn(props);
@ -216,8 +210,8 @@ class MessageActions extends React.Component {
const blockDeleteInMinutes = Message_AllowDeleting_BlockDeleteInMinutes; const blockDeleteInMinutes = Message_AllowDeleting_BlockDeleteInMinutes;
if (blockDeleteInMinutes != null && blockDeleteInMinutes !== 0) { if (blockDeleteInMinutes != null && blockDeleteInMinutes !== 0) {
let msgTs; let msgTs;
if (props.actionMessage.ts != null) { if (props.message.ts != null) {
msgTs = moment(props.actionMessage.ts); msgTs = moment(props.message.ts);
} }
let currentTsDiff; let currentTsDiff;
if (msgTs != null) { if (msgTs != null) {
@ -229,7 +223,7 @@ class MessageActions extends React.Component {
} }
handleDelete = () => { handleDelete = () => {
const { deleteRequest, actionMessage } = this.props; const { message } = this.props;
Alert.alert( Alert.alert(
I18n.t('Are_you_sure_question_mark'), I18n.t('Are_you_sure_question_mark'),
I18n.t('You_will_not_be_able_to_recover_this_message'), I18n.t('You_will_not_be_able_to_recover_this_message'),
@ -241,7 +235,13 @@ class MessageActions extends React.Component {
{ {
text: I18n.t('Yes_action_it', { action: 'delete' }), text: I18n.t('Yes_action_it', { action: 'delete' }),
style: 'destructive', style: 'destructive',
onPress: () => deleteRequest(actionMessage) onPress: async() => {
try {
await RocketChat.deleteMessage(message.id, message.rid);
} catch (e) {
log(e);
}
}
} }
], ],
{ cancelable: false } { cancelable: false }
@ -249,66 +249,73 @@ class MessageActions extends React.Component {
} }
handleEdit = () => { handleEdit = () => {
const { actionMessage, editInit } = this.props; const { message, editInit } = this.props;
const { _id, msg, rid } = actionMessage; editInit(message);
editInit({ _id, msg, rid });
} }
handleCopy = async() => { handleCopy = async() => {
const { actionMessage } = this.props; const { message } = this.props;
await Clipboard.setString(actionMessage.msg); await Clipboard.setString(message.msg);
EventEmitter.emit(LISTENER, { message: I18n.t('Copied_to_clipboard') }); EventEmitter.emit(LISTENER, { message: I18n.t('Copied_to_clipboard') });
} }
handleShare = async() => { handleShare = async() => {
const { actionMessage } = this.props; const { message } = this.props;
const permalink = await this.getPermalink(actionMessage); const permalink = await this.getPermalink(message);
Share.share({ Share.share({
message: permalink message: permalink
}); });
}; };
handleStar = () => { handleStar = async() => {
const { actionMessage, toggleStarRequest } = this.props; const { message } = this.props;
toggleStarRequest(actionMessage); try {
await RocketChat.toggleStarMessage(message.id, message.starred);
} catch (e) {
log(e);
}
} }
handlePermalink = async() => { handlePermalink = async() => {
const { actionMessage } = this.props; const { message } = this.props;
const permalink = await this.getPermalink(actionMessage); const permalink = await this.getPermalink(message);
Clipboard.setString(permalink); Clipboard.setString(permalink);
EventEmitter.emit(LISTENER, { message: I18n.t('Permalink_copied_to_clipboard') }); EventEmitter.emit(LISTENER, { message: I18n.t('Permalink_copied_to_clipboard') });
} }
handlePin = () => { handlePin = async() => {
const { actionMessage, togglePinRequest } = this.props; const { message } = this.props;
togglePinRequest(actionMessage); try {
await RocketChat.togglePinMessage(message.id, message.pinned);
} catch (e) {
log(e);
}
} }
handleReply = () => { handleReply = () => {
const { actionMessage, replyInit } = this.props; const { message, replyInit } = this.props;
replyInit(actionMessage, true); replyInit(message, true);
} }
handleQuote = () => { handleQuote = () => {
const { actionMessage, replyInit } = this.props; const { message, replyInit } = this.props;
replyInit(actionMessage, false); replyInit(message, false);
} }
handleReaction = () => { handleReaction = () => {
const { actionMessage, toggleReactionPicker } = this.props; const { message, reactionInit } = this.props;
toggleReactionPicker(actionMessage); reactionInit(message);
} }
handleReadReceipt = () => { handleReadReceipt = () => {
const { actionMessage } = this.props; const { message } = this.props;
Navigation.navigate('ReadReceiptsView', { messageId: actionMessage._id }); Navigation.navigate('ReadReceiptsView', { messageId: message.id });
} }
handleReport = async() => { handleReport = async() => {
const { actionMessage } = this.props; const { message } = this.props;
try { try {
await RocketChat.reportMessage(actionMessage._id); await RocketChat.reportMessage(message.id);
Alert.alert(I18n.t('Message_Reported')); Alert.alert(I18n.t('Message_Reported'));
} catch (e) { } catch (e) {
log(e); log(e);
@ -316,16 +323,24 @@ class MessageActions extends React.Component {
} }
handleToggleTranslation = async() => { handleToggleTranslation = async() => {
const { actionMessage, room } = this.props; const { message, room } = this.props;
try { try {
const message = database.objectForPrimaryKey('messages', actionMessage._id); const db = database.active;
database.write(() => { await db.action(async() => {
message.autoTranslate = !message.autoTranslate; await message.update((m) => {
message._updatedAt = new Date(); m.autoTranslate = !m.autoTranslate;
m._updatedAt = new Date();
});
}); });
const translatedMessage = getMessageTranslation(message, room.autoTranslateLanguage); const translatedMessage = getMessageTranslation(message, room.autoTranslateLanguage);
if (!translatedMessage) { if (!translatedMessage) {
await RocketChat.translateMessage(actionMessage, room.autoTranslateLanguage); const m = {
_id: message.id,
rid: message.subscription.id,
u: message.u,
msg: message.msg
};
await RocketChat.translateMessage(m, room.autoTranslateLanguage);
} }
} catch (e) { } catch (e) {
log(e); log(e);
@ -390,7 +405,6 @@ class MessageActions extends React.Component {
} }
const mapStateToProps = state => ({ const mapStateToProps = state => ({
actionMessage: state.messages.actionMessage,
Message_AllowDeleting: state.settings.Message_AllowDeleting, Message_AllowDeleting: state.settings.Message_AllowDeleting,
Message_AllowDeleting_BlockDeleteInMinutes: state.settings.Message_AllowDeleting_BlockDeleteInMinutes, Message_AllowDeleting_BlockDeleteInMinutes: state.settings.Message_AllowDeleting_BlockDeleteInMinutes,
Message_AllowEditing: state.settings.Message_AllowEditing, Message_AllowEditing: state.settings.Message_AllowEditing,
@ -400,14 +414,4 @@ const mapStateToProps = state => ({
Message_Read_Receipt_Store_Users: state.settings.Message_Read_Receipt_Store_Users Message_Read_Receipt_Store_Users: state.settings.Message_Read_Receipt_Store_Users
}); });
const mapDispatchToProps = dispatch => ({ export default connect(mapStateToProps)(MessageActions);
actionsHide: () => dispatch(actionsHideAction()),
deleteRequest: message => dispatch(deleteRequestAction(message)),
editInit: message => dispatch(editInitAction(message)),
toggleStarRequest: message => dispatch(toggleStarRequestAction(message)),
togglePinRequest: message => dispatch(togglePinRequestAction(message)),
toggleReactionPicker: message => dispatch(toggleReactionPickerAction(message)),
replyInit: (message, mention) => dispatch(replyInitAction(message, mention))
});
export default connect(mapStateToProps, mapDispatchToProps)(MessageActions);

View File

@ -5,7 +5,6 @@ import moment from 'moment';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import Markdown from '../markdown'; import Markdown from '../markdown';
import { getCustomEmoji } from '../message/utils';
import { CustomIcon } from '../../lib/Icons'; import { CustomIcon } from '../../lib/Icons';
import sharedStyles from '../../views/Styles'; import sharedStyles from '../../views/Styles';
import { import {
@ -55,7 +54,8 @@ class ReplyPreview extends Component {
Message_TimeFormat: PropTypes.string.isRequired, Message_TimeFormat: PropTypes.string.isRequired,
close: PropTypes.func.isRequired, close: PropTypes.func.isRequired,
baseUrl: PropTypes.string.isRequired, baseUrl: PropTypes.string.isRequired,
username: PropTypes.string.isRequired username: PropTypes.string.isRequired,
getCustomEmoji: PropTypes.func
} }
shouldComponentUpdate() { shouldComponentUpdate() {
@ -69,7 +69,7 @@ class ReplyPreview extends Component {
render() { render() {
const { const {
message, Message_TimeFormat, baseUrl, username, useMarkdown message, Message_TimeFormat, baseUrl, username, useMarkdown, getCustomEmoji
} = this.props; } = this.props;
const time = moment(message.ts).format(Message_TimeFormat); const time = moment(message.ts).format(Message_TimeFormat);
return ( return (

View File

@ -10,16 +10,12 @@ import ImagePicker from 'react-native-image-crop-picker';
import equal from 'deep-equal'; import equal from 'deep-equal';
import DocumentPicker from 'react-native-document-picker'; import DocumentPicker from 'react-native-document-picker';
import ActionSheet from 'react-native-action-sheet'; import ActionSheet from 'react-native-action-sheet';
import { Q } from '@nozbe/watermelondb';
import { userTyping as userTypingAction } from '../../actions/room'; import { userTyping as userTypingAction } from '../../actions/room';
import {
editRequest as editRequestAction,
editCancel as editCancelAction,
replyCancel as replyCancelAction
} from '../../actions/messages';
import RocketChat from '../../lib/rocketchat'; import RocketChat from '../../lib/rocketchat';
import styles from './styles'; import styles from './styles';
import database from '../../lib/realm'; import database from '../../lib/database';
import Avatar from '../Avatar'; import Avatar from '../Avatar';
import CustomEmoji from '../EmojiPicker/CustomEmoji'; import CustomEmoji from '../EmojiPicker/CustomEmoji';
import { emojis } from '../../emojis'; import { emojis } from '../../emojis';
@ -40,10 +36,6 @@ const MENTIONS_TRACKING_TYPE_EMOJIS = ':';
const MENTIONS_TRACKING_TYPE_COMMANDS = '/'; const MENTIONS_TRACKING_TYPE_COMMANDS = '/';
const MENTIONS_COUNT_TO_DISPLAY = 4; const MENTIONS_COUNT_TO_DISPLAY = 4;
const onlyUnique = function onlyUnique(value, index, self) {
return self.indexOf(({ _id }) => value._id === _id) === index;
};
const imagePickerConfig = { const imagePickerConfig = {
cropping: true, cropping: true,
compressImageQuality: 0.8, compressImageQuality: 0.8,
@ -69,7 +61,6 @@ class MessageBox extends Component {
rid: PropTypes.string.isRequired, rid: PropTypes.string.isRequired,
baseUrl: PropTypes.string.isRequired, baseUrl: PropTypes.string.isRequired,
message: PropTypes.object, message: PropTypes.object,
replyMessage: PropTypes.object,
replying: PropTypes.bool, replying: PropTypes.bool,
editing: PropTypes.bool, editing: PropTypes.bool,
threadsEnabled: PropTypes.bool, threadsEnabled: PropTypes.bool,
@ -81,11 +72,13 @@ class MessageBox extends Component {
}), }),
roomType: PropTypes.string, roomType: PropTypes.string,
tmid: PropTypes.string, tmid: PropTypes.string,
replyWithMention: PropTypes.bool,
getCustomEmoji: PropTypes.func,
editCancel: PropTypes.func.isRequired, editCancel: PropTypes.func.isRequired,
editRequest: PropTypes.func.isRequired, editRequest: PropTypes.func.isRequired,
onSubmit: PropTypes.func.isRequired, onSubmit: PropTypes.func.isRequired,
typing: PropTypes.func, typing: PropTypes.func,
closeReply: PropTypes.func replyCancel: PropTypes.func
} }
constructor(props) { constructor(props) {
@ -102,11 +95,6 @@ class MessageBox extends Component {
commandPreview: [] commandPreview: []
}; };
this.showCommandPreview = false; this.showCommandPreview = false;
this.commands = [];
this.users = [];
this.rooms = [];
this.emojis = [];
this.customEmojis = [];
this.onEmojiSelected = this.onEmojiSelected.bind(this); this.onEmojiSelected = this.onEmojiSelected.bind(this);
this.text = ''; this.text = '';
this.fileOptions = [ this.fileOptions = [
@ -135,20 +123,34 @@ class MessageBox extends Component {
}; };
} }
componentDidMount() { async componentDidMount() {
const db = database.active;
const { rid, tmid } = this.props; const { rid, tmid } = this.props;
let msg; let msg;
try {
const threadsCollection = db.collections.get('threads');
const subsCollection = db.collections.get('subscriptions');
if (tmid) { if (tmid) {
const thread = database.objectForPrimaryKey('threads', tmid); try {
const thread = await threadsCollection.find(tmid);
if (thread) { if (thread) {
msg = thread.draftMessage; msg = thread.draftMessage;
} }
} catch (error) {
console.log('Messagebox.didMount: Thread not found');
}
} else { } else {
const [room] = database.objects('subscriptions').filtered('rid = $0', rid); try {
if (room) { const room = await subsCollection.find(rid);
msg = room.draftMessage; msg = room.draftMessage;
} catch (error) {
console.log('Messagebox.didMount: Room not found');
} }
} }
} catch (e) {
log(e);
}
if (msg) { if (msg) {
this.setInput(msg); this.setInput(msg);
this.setShowSend(true); this.setShowSend(true);
@ -160,17 +162,16 @@ class MessageBox extends Component {
} }
componentWillReceiveProps(nextProps) { componentWillReceiveProps(nextProps) {
const { message, replyMessage, isFocused } = this.props; const { isFocused, editing, replying } = this.props;
if (!isFocused) { if (!isFocused) {
return; return;
} }
if (!equal(message, nextProps.message) && nextProps.message.msg) { if (editing !== nextProps.editing && nextProps.editing) {
this.setInput(nextProps.message.msg); this.setInput(nextProps.message.msg);
if (this.text) { if (this.text) {
this.setShowSend(true); this.setShowSend(true);
} }
this.focus(); } else if (replying !== nextProps.replying && nextProps.replying) {
} else if (!equal(replyMessage, nextProps.replyMessage)) {
this.focus(); this.focus();
} else if (!nextProps.message) { } else if (!nextProps.message) {
this.clearInput(); this.clearInput();
@ -181,6 +182,7 @@ class MessageBox extends Component {
const { const {
showEmojiKeyboard, showSend, recording, mentions, file, commandPreview showEmojiKeyboard, showSend, recording, mentions, file, commandPreview
} = this.state; } = this.state;
const { const {
roomType, replying, editing, isFocused roomType, replying, editing, isFocused
} = this.props; } = this.props;
@ -217,7 +219,12 @@ class MessageBox extends Component {
return false; return false;
} }
onChangeText = debounce((text) => { componentWillUnmount() {
console.countReset(`${ this.constructor.name }.render calls`);
}
onChangeText = debounce(async(text) => {
const db = database.active;
const isTextEmpty = text.length === 0; const isTextEmpty = text.length === 0;
this.setShowSend(!isTextEmpty); this.setShowSend(!isTextEmpty);
this.handleTyping(!isTextEmpty); this.handleTyping(!isTextEmpty);
@ -226,10 +233,15 @@ class MessageBox extends Component {
const slashCommand = text.match(/^\/([a-z0-9._-]+) (.+)/im); const slashCommand = text.match(/^\/([a-z0-9._-]+) (.+)/im);
if (slashCommand) { if (slashCommand) {
const [, name, params] = slashCommand; const [, name, params] = slashCommand;
const command = database.objects('slashCommand').filtered('command == $0', name); const commandsCollection = db.collections.get('slash_commands');
if (command && command[0] && command[0].providesPreview) { try {
const command = await commandsCollection.find(name);
if (command.providesPreview) {
return this.setCommandPreview(name, params); return this.setCommandPreview(name, params);
} }
} catch (e) {
console.log('Slash command not found');
}
} }
if (!isTextEmpty) { if (!isTextEmpty) {
@ -324,114 +336,49 @@ class MessageBox extends Component {
} }
getFixedMentions = (keyword) => { getFixedMentions = (keyword) => {
let result = [];
if ('all'.indexOf(keyword) !== -1) { if ('all'.indexOf(keyword) !== -1) {
this.users = [{ _id: -1, username: 'all' }, ...this.users]; result = [{ _id: -1, username: 'all' }];
} }
if ('here'.indexOf(keyword) !== -1) { if ('here'.indexOf(keyword) !== -1) {
this.users = [{ _id: -2, username: 'here' }, ...this.users]; result = [{ _id: -2, username: 'here' }, ...result];
} }
return result;
} }
getUsers = async(keyword) => { getUsers = debounce(async(keyword) => {
this.users = database.objects('users'); let res = await RocketChat.search({ text: keyword, filterRooms: false, filterUsers: true });
res = [...this.getFixedMentions(keyword), ...res];
this.setState({ mentions: res });
}, 300)
getRooms = debounce(async(keyword = '') => {
const res = await RocketChat.search({ text: keyword, filterRooms: true, filterUsers: false });
this.setState({ mentions: res });
}, 300)
getEmojis = debounce(async(keyword) => {
const db = database.active;
if (keyword) { if (keyword) {
this.users = this.users.filtered('username CONTAINS[c] $0', keyword); const customEmojisCollection = db.collections.get('custom_emojis');
let customEmojis = await customEmojisCollection.query(
Q.where('name', Q.like(`${ Q.sanitizeLikeString(keyword) }%`))
).fetch();
customEmojis = customEmojis.slice(0, MENTIONS_COUNT_TO_DISPLAY);
const filteredEmojis = emojis.filter(emoji => emoji.indexOf(keyword) !== -1).slice(0, MENTIONS_COUNT_TO_DISPLAY);
const mergedEmojis = [...customEmojis, ...filteredEmojis].slice(0, MENTIONS_COUNT_TO_DISPLAY);
this.setState({ mentions: mergedEmojis || [] });
} }
this.getFixedMentions(keyword); }, 300)
this.setState({ mentions: this.users.slice() });
const usernames = []; getSlashCommands = debounce(async(keyword) => {
const db = database.active;
if (keyword && this.users.length > 7) { const commandsCollection = db.collections.get('slash_commands');
return; const commands = await commandsCollection.query(
} Q.where('id', Q.like(`${ Q.sanitizeLikeString(keyword) }%`))
).fetch();
this.users.forEach(user => usernames.push(user.username)); this.setState({ mentions: commands || [] });
}, 300)
if (this.oldPromise) {
this.oldPromise();
}
try {
const results = await Promise.race([
RocketChat.spotlight(keyword, usernames, { users: true }),
new Promise((resolve, reject) => (this.oldPromise = reject))
]);
if (results.users && results.users.length) {
database.write(() => {
results.users.forEach((user) => {
try {
database.create('users', user, true);
} catch (e) {
log(e);
}
});
});
}
} catch (e) {
console.warn('spotlight canceled');
} finally {
delete this.oldPromise;
this.users = database.objects('users').filtered('username CONTAINS[c] $0', keyword).slice(0, MENTIONS_COUNT_TO_DISPLAY);
this.getFixedMentions(keyword);
this.setState({ mentions: this.users });
}
}
getRooms = async(keyword = '') => {
this.roomsCache = this.roomsCache || [];
this.rooms = database.objects('subscriptions')
.filtered('t != $0', 'd');
if (keyword) {
this.rooms = this.rooms.filtered('name CONTAINS[c] $0', keyword);
}
const rooms = [];
this.rooms.forEach(room => rooms.push(room));
this.roomsCache.forEach((room) => {
if (room.name && room.name.toUpperCase().indexOf(keyword.toUpperCase()) !== -1) {
rooms.push(room);
}
});
if (rooms.length > 3) {
this.setState({ mentions: rooms });
return;
}
if (this.oldPromise) {
this.oldPromise();
}
try {
const results = await Promise.race([
RocketChat.spotlight(keyword, [...rooms, ...this.roomsCache].map(r => r.name), { rooms: true }),
new Promise((resolve, reject) => (this.oldPromise = reject))
]);
if (results.rooms && results.rooms.length) {
this.roomsCache = [...this.roomsCache, ...results.rooms].filter(onlyUnique);
}
this.setState({ mentions: [...rooms.slice(), ...results.rooms] });
} catch (e) {
console.warn('spotlight canceled');
} finally {
delete this.oldPromise;
}
}
getEmojis = (keyword) => {
if (keyword) {
this.customEmojis = database.objects('customEmojis').filtered('name CONTAINS[c] $0', keyword).slice(0, MENTIONS_COUNT_TO_DISPLAY);
this.emojis = emojis.filter(emoji => emoji.indexOf(keyword) !== -1).slice(0, MENTIONS_COUNT_TO_DISPLAY);
const mergedEmojis = [...this.customEmojis, ...this.emojis].slice(0, MENTIONS_COUNT_TO_DISPLAY);
this.setState({ mentions: mergedEmojis });
}
}
getSlashCommands = (keyword) => {
this.commands = database.objects('slashCommand').filtered('command CONTAINS[c] $0', keyword);
this.setState({ mentions: this.commands });
}
focus = () => { focus = () => {
if (this.component && this.component.focus) { if (this.component && this.component.focus) {
@ -629,7 +576,7 @@ class MessageBox extends Component {
submit = async() => { submit = async() => {
const { const {
message: editingMessage, editRequest, onSubmit, rid: roomId onSubmit, rid: roomId
} = this.props; } = this.props;
const message = this.text; const message = this.text;
@ -646,10 +593,13 @@ class MessageBox extends Component {
} = this.props; } = this.props;
// Slash command // Slash command
if (message[0] === MENTIONS_TRACKING_TYPE_COMMANDS) { if (message[0] === MENTIONS_TRACKING_TYPE_COMMANDS) {
const db = database.active;
const commandsCollection = db.collections.get('slash_commands');
const command = message.replace(/ .*/, '').slice(1); const command = message.replace(/ .*/, '').slice(1);
const slashCommand = database.objects('slashCommand').filtered('command CONTAINS[c] $0', command); const slashCommand = await commandsCollection.query(
Q.where('id', Q.like(`${ Q.sanitizeLikeString(command) }%`))
).fetch();
if (slashCommand.length > 0) { if (slashCommand.length > 0) {
try { try {
const messageWithoutCommand = message.substr(message.indexOf(' ') + 1); const messageWithoutCommand = message.substr(message.indexOf(' ') + 1);
@ -663,32 +613,35 @@ class MessageBox extends Component {
} }
// Edit // Edit
if (editing) { if (editing) {
const { _id, rid } = editingMessage; const { message: editingMessage, editRequest } = this.props;
editRequest({ _id, msg: message, rid }); const { id, subscription: { id: rid } } = editingMessage;
editRequest({ id, msg: message, rid });
// Reply // Reply
} else if (replying) { } else if (replying) {
const { replyMessage, closeReply, threadsEnabled } = this.props; const {
message: replyingMessage, replyCancel, threadsEnabled, replyWithMention
} = this.props;
// Thread // Thread
if (threadsEnabled && replyMessage.mention) { if (threadsEnabled && replyWithMention) {
onSubmit(message, replyMessage._id); onSubmit(message, replyingMessage.id);
// Legacy reply or quote (quote is a reply without mention) // Legacy reply or quote (quote is a reply without mention)
} else { } else {
const { user, roomType } = this.props; const { user, roomType } = this.props;
const permalink = await this.getPermalink(replyMessage); const permalink = await this.getPermalink(replyingMessage);
let msg = `[ ](${ permalink }) `; let msg = `[ ](${ permalink }) `;
// if original message wasn't sent by current user and neither from a direct room // if original message wasn't sent by current user and neither from a direct room
if (user.username !== replyMessage.u.username && roomType !== 'd' && replyMessage.mention) { if (user.username !== replyingMessage.u.username && roomType !== 'd' && replyWithMention) {
msg += `@${ replyMessage.u.username } `; msg += `@${ replyingMessage.u.username } `;
} }
msg = `${ msg } ${ message }`; msg = `${ msg } ${ message }`;
onSubmit(msg); onSubmit(msg);
} }
closeReply(); replyCancel();
// Normal message // Normal message
} else { } else {
@ -726,11 +679,6 @@ class MessageBox extends Component {
trackingType: '', trackingType: '',
commandPreview: [] commandPreview: []
}); });
this.users = [];
this.rooms = [];
this.customEmojis = [];
this.emojis = [];
this.commands = [];
} }
renderFixedMentionItem = item => ( renderFixedMentionItem = item => (
@ -797,33 +745,33 @@ class MessageBox extends Component {
switch (trackingType) { switch (trackingType) {
case MENTIONS_TRACKING_TYPE_EMOJIS: case MENTIONS_TRACKING_TYPE_EMOJIS:
return ( return (
<React.Fragment> <>
{this.renderMentionEmoji(item)} {this.renderMentionEmoji(item)}
<Text key='mention-item-name' style={styles.mentionText}>:{ item.name || item }:</Text> <Text key='mention-item-name' style={styles.mentionText}>:{ item.name || item }:</Text>
</React.Fragment> </>
); );
case MENTIONS_TRACKING_TYPE_COMMANDS: case MENTIONS_TRACKING_TYPE_COMMANDS:
return ( return (
<React.Fragment> <>
<Text key='mention-item-command' style={styles.slash}>/</Text> <Text key='mention-item-command' style={styles.slash}>/</Text>
<Text key='mention-item-param'>{ item.command}</Text> <Text key='mention-item-param'>{ item.command}</Text>
</React.Fragment> </>
); );
default: default:
return ( return (
<React.Fragment> <>
<Avatar <Avatar
key='mention-item-avatar' key='mention-item-avatar'
style={styles.avatar} style={styles.avatar}
text={item.username || item.name} text={item.username || item.name}
size={30} size={30}
type={item.username ? 'd' : 'c'} type={item.t}
baseUrl={baseUrl} baseUrl={baseUrl}
userId={user.id} userId={user.id}
token={user.token} token={user.token}
/> />
<Text key='mention-item-name' style={styles.mentionText}>{ item.username || item.name || item }</Text> <Text key='mention-item-name' style={styles.mentionText}>{ item.username || item.name || item }</Text>
</React.Fragment> </>
); );
} }
})() })()
@ -880,12 +828,12 @@ class MessageBox extends Component {
renderReplyPreview = () => { renderReplyPreview = () => {
const { const {
replyMessage, replying, closeReply, user message, replying, replyCancel, user, getCustomEmoji
} = this.props; } = this.props;
if (!replying) { if (!replying) {
return null; return null;
} }
return <ReplyPreview key='reply-preview' message={replyMessage} close={closeReply} username={user.username} />; return <ReplyPreview key='reply-preview' message={message} close={replyCancel} username={user.username} getCustomEmoji={getCustomEmoji} />;
}; };
renderContent = () => { renderContent = () => {
@ -896,7 +844,7 @@ class MessageBox extends Component {
return (<Recording onFinish={this.finishAudioMessage} />); return (<Recording onFinish={this.finishAudioMessage} />);
} }
return ( return (
<React.Fragment> <>
{this.renderCommandPreview()} {this.renderCommandPreview()}
{this.renderMentions()} {this.renderMentions()}
<View style={styles.composer} key='messagebox'> <View style={styles.composer} key='messagebox'>
@ -935,14 +883,15 @@ class MessageBox extends Component {
/> />
</View> </View>
</View> </View>
</React.Fragment> </>
); );
} }
render() { render() {
console.count(`${ this.constructor.name }.render calls`);
const { showEmojiKeyboard, file } = this.state; const { showEmojiKeyboard, file } = this.state;
return ( return (
<React.Fragment> <>
<KeyboardAccessoryView <KeyboardAccessoryView
renderContent={this.renderContent} renderContent={this.renderContent}
kbInputRef={this.component} kbInputRef={this.component}
@ -960,16 +909,12 @@ class MessageBox extends Component {
close={() => this.setState({ file: {} })} close={() => this.setState({ file: {} })}
submit={this.sendMediaMessage} submit={this.sendMediaMessage}
/> />
</React.Fragment> </>
); );
} }
} }
const mapStateToProps = state => ({ const mapStateToProps = state => ({
message: state.messages.message,
replyMessage: state.messages.replyMessage,
replying: state.messages.replying,
editing: state.messages.editing,
baseUrl: state.settings.Site_Url || state.server ? state.server.server : '', baseUrl: state.settings.Site_Url || state.server ? state.server.server : '',
threadsEnabled: state.settings.Threads_enabled, threadsEnabled: state.settings.Threads_enabled,
user: { user: {
@ -980,10 +925,7 @@ const mapStateToProps = state => ({
}); });
const dispatchToProps = ({ const dispatchToProps = ({
editCancel: () => editCancelAction(), typing: (rid, status) => userTypingAction(rid, status)
editRequest: message => editRequestAction(message),
typing: (rid, status) => userTypingAction(rid, status),
closeReply: () => replyCancelAction()
}); });
export default connect(mapStateToProps, dispatchToProps, null, { forwardRef: true })(MessageBox); export default connect(mapStateToProps, dispatchToProps, null, { forwardRef: true })(MessageBox);

View File

@ -1,33 +1,18 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import ActionSheet from 'react-native-action-sheet'; import ActionSheet from 'react-native-action-sheet';
import { errorActionsHide as errorActionsHideAction } from '../actions/messages';
import RocketChat from '../lib/rocketchat'; import RocketChat from '../lib/rocketchat';
import database from '../lib/realm'; import database from '../lib/database';
import protectedFunction from '../lib/methods/helpers/protectedFunction'; import protectedFunction from '../lib/methods/helpers/protectedFunction';
import I18n from '../i18n'; import I18n from '../i18n';
class MessageErrorActions extends React.Component { class MessageErrorActions extends React.Component {
static propTypes = { static propTypes = {
errorActionsHide: PropTypes.func.isRequired, actionsHide: PropTypes.func.isRequired,
actionMessage: PropTypes.object message: PropTypes.object
}; };
handleResend = protectedFunction(() => {
const { actionMessage } = this.props;
RocketChat.resendMessage(actionMessage._id);
});
handleDelete = protectedFunction(() => {
const { actionMessage } = this.props;
database.write(() => {
const msg = database.objects('messages').filtered('_id = $0', actionMessage._id);
database.delete(msg);
});
})
// eslint-disable-next-line react/sort-comp // eslint-disable-next-line react/sort-comp
constructor(props) { constructor(props) {
super(props); super(props);
@ -41,6 +26,19 @@ class MessageErrorActions extends React.Component {
}); });
} }
handleResend = protectedFunction(async() => {
const { message } = this.props;
await RocketChat.resendMessage(message);
});
handleDelete = protectedFunction(async() => {
const { message } = this.props;
const db = database.active;
await db.action(async() => {
await message.destroyPermanently();
});
})
showActionSheet = () => { showActionSheet = () => {
ActionSheet.showActionSheetWithOptions({ ActionSheet.showActionSheetWithOptions({
options: this.options, options: this.options,
@ -53,7 +51,7 @@ class MessageErrorActions extends React.Component {
} }
handleActionPress = (actionIndex) => { handleActionPress = (actionIndex) => {
const { errorActionsHide } = this.props; const { actionsHide } = this.props;
switch (actionIndex) { switch (actionIndex) {
case this.RESEND_INDEX: case this.RESEND_INDEX:
this.handleResend(); this.handleResend();
@ -64,7 +62,7 @@ class MessageErrorActions extends React.Component {
default: default:
break; break;
} }
errorActionsHide(); actionsHide();
} }
render() { render() {
@ -74,12 +72,4 @@ class MessageErrorActions extends React.Component {
} }
} }
const mapStateToProps = state => ({ export default MessageErrorActions;
actionMessage: state.messages.actionMessage
});
const mapDispatchToProps = dispatch => ({
errorActionsHide: () => dispatch(errorActionsHideAction())
});
export default connect(mapStateToProps, mapDispatchToProps)(MessageErrorActions);

View File

@ -7,7 +7,6 @@ import Modal from 'react-native-modal';
import Touchable from 'react-native-platform-touchable'; import Touchable from 'react-native-platform-touchable';
import Emoji from './message/Emoji'; import Emoji from './message/Emoji';
import { getCustomEmoji } from './message/utils';
import I18n from '../i18n'; import I18n from '../i18n';
import { CustomIcon } from '../lib/Icons'; import { CustomIcon } from '../lib/Icons';
import sharedStyles from '../views/Styles'; import sharedStyles from '../views/Styles';
@ -62,7 +61,9 @@ const styles = StyleSheet.create({
const standardEmojiStyle = { fontSize: 20 }; const standardEmojiStyle = { fontSize: 20 };
const customEmojiStyle = { width: 20, height: 20 }; const customEmojiStyle = { width: 20, height: 20 };
const Item = React.memo(({ item, user, baseUrl }) => { const Item = React.memo(({
item, user, baseUrl, getCustomEmoji
}) => {
const count = item.usernames.length; const count = item.usernames.length;
let usernames = item.usernames.slice(0, 3) let usernames = item.usernames.slice(0, 3)
.map(username => (username === user.username ? I18n.t('you') : username)).join(', '); .map(username => (username === user.username ? I18n.t('you') : username)).join(', ');
@ -146,7 +147,8 @@ ModalContent.displayName = 'ReactionsModalContent';
Item.propTypes = { Item.propTypes = {
item: PropTypes.object, item: PropTypes.object,
user: PropTypes.object, user: PropTypes.object,
baseUrl: PropTypes.string baseUrl: PropTypes.string,
getCustomEmoji: PropTypes.func
}; };
Item.displayName = 'ReactionsModalItem'; Item.displayName = 'ReactionsModalItem';

View File

@ -3,56 +3,26 @@ import PropTypes from 'prop-types';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import Status from './Status'; import Status from './Status';
import database, { safeAddListener } from '../../lib/realm';
class StatusContainer extends React.PureComponent { class StatusContainer extends React.PureComponent {
static propTypes = { static propTypes = {
id: PropTypes.string,
style: PropTypes.any, style: PropTypes.any,
size: PropTypes.number, size: PropTypes.number,
offline: PropTypes.bool status: PropTypes.string
}; };
static defaultProps = { static defaultProps = {
size: 16 size: 16
} }
constructor(props) {
super(props);
this.user = database.memoryDatabase.objects('activeUsers').filtered('id == $0', props.id);
this.state = {
user: this.user[0] || {}
};
safeAddListener(this.user, this.updateState);
}
componentWillUnmount() {
this.user.removeAllListeners();
}
get status() {
const { user } = this.state;
const { offline } = this.props;
if (offline || !user) {
return 'offline';
}
return user.status || 'offline';
}
updateState = () => {
if (this.user.length) {
this.setState({ user: this.user[0] });
}
}
render() { render() {
const { style, size } = this.props; const { style, size, status } = this.props;
return <Status size={size} style={style} status={this.status} />; return <Status size={size} style={style} status={status} />;
} }
} }
const mapStateToProps = state => ({ const mapStateToProps = (state, ownProps) => ({
offline: !state.meteor.connected status: state.meteor.connected ? state.activeUsers[ownProps.id] : 'offline'
}); });
export default connect(mapStateToProps)(StatusContainer); export default connect(mapStateToProps)(StatusContainer);

View File

@ -24,10 +24,10 @@ const Content = React.memo((props) => {
getCustomEmoji={props.getCustomEmoji} getCustomEmoji={props.getCustomEmoji}
username={props.user.username} username={props.user.username}
isEdited={props.isEdited} isEdited={props.isEdited}
numberOfLines={props.tmid ? 1 : 0} numberOfLines={(props.tmid && !props.isThreadRoom) ? 1 : 0}
channels={props.channels} channels={props.channels}
mentions={props.mentions} mentions={props.mentions}
useMarkdown={props.useMarkdown && !props.tmid} useMarkdown={props.useMarkdown && (!props.tmid || props.isThreadRoom)}
navToRoomInfo={props.navToRoomInfo} navToRoomInfo={props.navToRoomInfo}
tmid={props.tmid} tmid={props.tmid}
/> />
@ -45,6 +45,7 @@ Content.propTypes = {
isTemp: PropTypes.bool, isTemp: PropTypes.bool,
isInfo: PropTypes.bool, isInfo: PropTypes.bool,
tmid: PropTypes.string, tmid: PropTypes.string,
isThreadRoom: PropTypes.bool,
msg: PropTypes.string, msg: PropTypes.string,
isEdited: PropTypes.bool, isEdited: PropTypes.bool,
useMarkdown: PropTypes.bool, useMarkdown: PropTypes.bool,

View File

@ -32,7 +32,7 @@ const MessageAvatar = React.memo(({
); );
} }
return null; return null;
}, (prevProps, nextProps) => prevProps.isHeader === nextProps.isHeader); });
MessageAvatar.propTypes = { MessageAvatar.propTypes = {
isHeader: PropTypes.bool, isHeader: PropTypes.bool,

View File

@ -8,9 +8,9 @@ import styles from './styles';
import Emoji from './Emoji'; import Emoji from './Emoji';
import { BUTTON_HIT_SLOP } from './utils'; import { BUTTON_HIT_SLOP } from './utils';
const AddReaction = React.memo(({ toggleReactionPicker }) => ( const AddReaction = React.memo(({ reactionInit }) => (
<Touchable <Touchable
onPress={toggleReactionPicker} onPress={reactionInit}
key='message-add-reaction' key='message-add-reaction'
testID='message-add-reaction' testID='message-add-reaction'
style={styles.reactionButton} style={styles.reactionButton}
@ -52,7 +52,7 @@ const Reaction = React.memo(({
}); });
const Reactions = React.memo(({ const Reactions = React.memo(({
reactions, user, baseUrl, onReactionPress, toggleReactionPicker, onReactionLongPress, getCustomEmoji reactions, user, baseUrl, onReactionPress, reactionInit, onReactionLongPress, getCustomEmoji
}) => { }) => {
if (!reactions || reactions.length === 0) { if (!reactions || reactions.length === 0) {
return null; return null;
@ -70,11 +70,10 @@ const Reactions = React.memo(({
getCustomEmoji={getCustomEmoji} getCustomEmoji={getCustomEmoji}
/> />
))} ))}
<AddReaction toggleReactionPicker={toggleReactionPicker} /> <AddReaction reactionInit={reactionInit} />
</View> </View>
); );
}); });
// FIXME: can't compare because it's a Realm object (it may be fixed by JSON.parse(JSON.stringify(reactions)))
Reaction.propTypes = { Reaction.propTypes = {
reaction: PropTypes.object, reaction: PropTypes.object,
@ -91,14 +90,14 @@ Reactions.propTypes = {
user: PropTypes.object, user: PropTypes.object,
baseUrl: PropTypes.string, baseUrl: PropTypes.string,
onReactionPress: PropTypes.func, onReactionPress: PropTypes.func,
toggleReactionPicker: PropTypes.func, reactionInit: PropTypes.func,
onReactionLongPress: PropTypes.func, onReactionLongPress: PropTypes.func,
getCustomEmoji: PropTypes.func getCustomEmoji: PropTypes.func
}; };
Reactions.displayName = 'MessageReactions'; Reactions.displayName = 'MessageReactions';
AddReaction.propTypes = { AddReaction.propTypes = {
toggleReactionPicker: PropTypes.func reactionInit: PropTypes.func
}; };
AddReaction.displayName = 'MessageAddReaction'; AddReaction.displayName = 'MessageAddReaction';

View File

@ -9,14 +9,14 @@ import DisclosureIndicator from '../DisclosureIndicator';
import styles from './styles'; import styles from './styles';
const RepliedThread = React.memo(({ const RepliedThread = React.memo(({
tmid, tmsg, isHeader, isTemp, fetchThreadName tmid, tmsg, isHeader, isTemp, fetchThreadName, id
}) => { }) => {
if (!tmid || !isHeader || isTemp) { if (!tmid || !isHeader || isTemp) {
return null; return null;
} }
if (!tmsg) { if (!tmsg) {
fetchThreadName(tmid); fetchThreadName(tmid, id);
return null; return null;
} }
@ -49,6 +49,7 @@ const RepliedThread = React.memo(({
RepliedThread.propTypes = { RepliedThread.propTypes = {
tmid: PropTypes.string, tmid: PropTypes.string,
tmsg: PropTypes.string, tmsg: PropTypes.string,
id: PropTypes.string,
isHeader: PropTypes.bool, isHeader: PropTypes.bool,
isTemp: PropTypes.bool, isTemp: PropTypes.bool,
fetchThreadName: PropTypes.func fetchThreadName: PropTypes.func

View File

@ -8,9 +8,9 @@ import { CustomIcon } from '../../lib/Icons';
import { THREAD } from './constants'; import { THREAD } from './constants';
const Thread = React.memo(({ const Thread = React.memo(({
msg, tcount, tlm, customThreadTimeFormat msg, tcount, tlm, customThreadTimeFormat, isThreadRoom
}) => { }) => {
if (!tlm) { if (!tlm || isThreadRoom) {
return null; return null;
} }
@ -39,7 +39,8 @@ Thread.propTypes = {
msg: PropTypes.string, msg: PropTypes.string,
tcount: PropTypes.string, tcount: PropTypes.string,
tlm: PropTypes.string, tlm: PropTypes.string,
customThreadTimeFormat: PropTypes.string customThreadTimeFormat: PropTypes.string,
isThreadRoom: PropTypes.bool
}; };
Thread.displayName = 'MessageThread'; Thread.displayName = 'MessageThread';

View File

@ -4,7 +4,7 @@ import { KeyboardUtils } from 'react-native-keyboard-input';
import Message from './Message'; import Message from './Message';
import debounce from '../../utils/debounce'; import debounce from '../../utils/debounce';
import { SYSTEM_MESSAGES, getCustomEmoji, getMessageTranslation } from './utils'; import { SYSTEM_MESSAGES, getMessageTranslation } from './utils';
import messagesStatus from '../../constants/messagesStatus'; import messagesStatus from '../../constants/messagesStatus';
export default class MessageContainer extends React.Component { export default class MessageContainer extends React.Component {
@ -21,22 +21,23 @@ export default class MessageContainer extends React.Component {
archived: PropTypes.bool, archived: PropTypes.bool,
broadcast: PropTypes.bool, broadcast: PropTypes.bool,
previousItem: PropTypes.object, previousItem: PropTypes.object,
_updatedAt: PropTypes.instanceOf(Date),
baseUrl: PropTypes.string, baseUrl: PropTypes.string,
Message_GroupingPeriod: PropTypes.number, Message_GroupingPeriod: PropTypes.number,
isReadReceiptEnabled: PropTypes.bool, isReadReceiptEnabled: PropTypes.bool,
isThreadRoom: PropTypes.bool,
useRealName: PropTypes.bool, useRealName: PropTypes.bool,
useMarkdown: PropTypes.bool, useMarkdown: PropTypes.bool,
autoTranslateRoom: PropTypes.bool, autoTranslateRoom: PropTypes.bool,
autoTranslateLanguage: PropTypes.string, autoTranslateLanguage: PropTypes.string,
status: PropTypes.number, status: PropTypes.number,
getCustomEmoji: PropTypes.func,
onLongPress: PropTypes.func, onLongPress: PropTypes.func,
onReactionPress: PropTypes.func, onReactionPress: PropTypes.func,
onDiscussionPress: PropTypes.func, onDiscussionPress: PropTypes.func,
onThreadPress: PropTypes.func, onThreadPress: PropTypes.func,
errorActionsShow: PropTypes.func, errorActionsShow: PropTypes.func,
replyBroadcast: PropTypes.func, replyBroadcast: PropTypes.func,
toggleReactionPicker: PropTypes.func, reactionInit: PropTypes.func,
fetchThreadName: PropTypes.func, fetchThreadName: PropTypes.func,
onOpenFileModal: PropTypes.func, onOpenFileModal: PropTypes.func,
onReactionLongPress: PropTypes.func, onReactionLongPress: PropTypes.func,
@ -45,55 +46,53 @@ export default class MessageContainer extends React.Component {
static defaultProps = { static defaultProps = {
onLongPress: () => {}, onLongPress: () => {},
_updatedAt: new Date(),
archived: false, archived: false,
broadcast: false broadcast: false
} }
shouldComponentUpdate(nextProps) { componentDidMount() {
const { const { item } = this.props;
status, item, _updatedAt, autoTranslateRoom if (item && item.observe) {
} = this.props; const observable = item.observe();
this.subscription = observable.subscribe(() => {
if (status !== nextProps.status) { this.forceUpdate();
return true; });
} }
if (autoTranslateRoom !== nextProps.autoTranslateRoom) {
return true;
}
if (item.tmsg !== nextProps.item.tmsg) {
return true;
}
if (item.unread !== nextProps.item.unread) {
return true;
} }
return _updatedAt.toISOString() !== nextProps._updatedAt.toISOString(); shouldComponentUpdate() {
return false;
}
componentWillUnmount() {
if (this.subscription && this.subscription.unsubscribe) {
this.subscription.unsubscribe();
}
} }
onPress = debounce(() => { onPress = debounce(() => {
const { item } = this.props; const { item, isThreadRoom } = this.props;
KeyboardUtils.dismiss(); KeyboardUtils.dismiss();
if ((item.tlm || item.tmid)) { if (((item.tlm || item.tmid) && !isThreadRoom)) {
this.onThreadPress(); this.onThreadPress();
} }
}, 300, true); }, 300, true);
onLongPress = () => { onLongPress = () => {
const { archived, onLongPress } = this.props; const { archived, onLongPress, item } = this.props;
if (this.isInfo || this.hasError || archived) { if (this.isInfo || this.hasError || archived) {
return; return;
} }
if (onLongPress) { if (onLongPress) {
onLongPress(this.parseMessage()); onLongPress(item);
} }
} }
onErrorPress = () => { onErrorPress = () => {
const { errorActionsShow } = this.props; const { errorActionsShow, item } = this.props;
if (errorActionsShow) { if (errorActionsShow) {
errorActionsShow(this.parseMessage()); errorActionsShow(item);
} }
} }
@ -132,6 +131,7 @@ export default class MessageContainer extends React.Component {
if (this.hasError || (previousItem && previousItem.status === messagesStatus.ERROR)) { if (this.hasError || (previousItem && previousItem.status === messagesStatus.ERROR)) {
return true; return true;
} }
try {
if (previousItem && ( if (previousItem && (
(previousItem.ts.toDateString() === item.ts.toDateString()) (previousItem.ts.toDateString() === item.ts.toDateString())
&& (previousItem.u.username === item.u.username) && (previousItem.u.username === item.u.username)
@ -142,13 +142,19 @@ export default class MessageContainer extends React.Component {
return false; return false;
} }
return true; return true;
} catch (error) {
return true;
}
} }
get isThreadReply() { get isThreadReply() {
const { const {
item, previousItem item, previousItem, isThreadRoom
} = this.props; } = this.props;
if (previousItem && item.tmid && (previousItem.tmid !== item.tmid) && (previousItem._id !== item.tmid)) { if (isThreadRoom) {
return false;
}
if (previousItem && item.tmid && (previousItem.tmid !== item.tmid) && (previousItem.id !== item.tmid)) {
return true; return true;
} }
return false; return false;
@ -156,9 +162,12 @@ export default class MessageContainer extends React.Component {
get isThreadSequential() { get isThreadSequential() {
const { const {
item, previousItem item, previousItem, isThreadRoom
} = this.props; } = this.props;
if (previousItem && item.tmid && ((previousItem.tmid === item.tmid) || (previousItem._id === item.tmid))) { if (isThreadRoom) {
return false;
}
if (previousItem && item.tmid && ((previousItem.tmid === item.tmid) || (previousItem.id === item.tmid))) {
return true; return true;
} }
return false; return false;
@ -179,31 +188,26 @@ export default class MessageContainer extends React.Component {
return item.status === messagesStatus.ERROR; return item.status === messagesStatus.ERROR;
} }
parseMessage = () => { reactionInit = () => {
const { item } = this.props; const { reactionInit, item } = this.props;
return JSON.parse(JSON.stringify(item)); if (reactionInit) {
} reactionInit(item);
toggleReactionPicker = () => {
const { toggleReactionPicker } = this.props;
if (toggleReactionPicker) {
toggleReactionPicker(this.parseMessage());
} }
} }
replyBroadcast = () => { replyBroadcast = () => {
const { replyBroadcast } = this.props; const { replyBroadcast, item } = this.props;
if (replyBroadcast) { if (replyBroadcast) {
replyBroadcast(this.parseMessage()); replyBroadcast(item);
} }
} }
render() { render() {
const { const {
item, user, style, archived, baseUrl, useRealName, broadcast, fetchThreadName, customThreadTimeFormat, onOpenFileModal, timeFormat, useMarkdown, isReadReceiptEnabled, autoTranslateRoom, autoTranslateLanguage, navToRoomInfo item, user, style, archived, baseUrl, useRealName, broadcast, fetchThreadName, customThreadTimeFormat, onOpenFileModal, timeFormat, useMarkdown, isReadReceiptEnabled, autoTranslateRoom, autoTranslateLanguage, navToRoomInfo, getCustomEmoji, isThreadRoom
} = this.props; } = this.props;
const { const {
_id, msg, ts, attachments, urls, reactions, t, avatar, u, alias, editedBy, role, drid, dcount, dlm, tmid, tcount, tlm, tmsg, mentions, channels, unread, autoTranslate: autoTranslateMessage id, msg, ts, attachments, urls, reactions, t, avatar, u, alias, editedBy, role, drid, dcount, dlm, tmid, tcount, tlm, tmsg, mentions, channels, unread, autoTranslate: autoTranslateMessage
} = item; } = item;
let message = msg; let message = msg;
@ -215,7 +219,7 @@ export default class MessageContainer extends React.Component {
return ( return (
<Message <Message
id={_id} id={id}
msg={message} msg={message}
author={u} author={u}
ts={ts} ts={ts}
@ -251,6 +255,7 @@ export default class MessageContainer extends React.Component {
isHeader={this.isHeader} isHeader={this.isHeader}
isThreadReply={this.isThreadReply} isThreadReply={this.isThreadReply}
isThreadSequential={this.isThreadSequential} isThreadSequential={this.isThreadSequential}
isThreadRoom={isThreadRoom}
isInfo={this.isInfo} isInfo={this.isInfo}
isTemp={this.isTemp} isTemp={this.isTemp}
hasError={this.hasError} hasError={this.hasError}
@ -260,7 +265,7 @@ export default class MessageContainer extends React.Component {
onReactionLongPress={this.onReactionLongPress} onReactionLongPress={this.onReactionLongPress}
onReactionPress={this.onReactionPress} onReactionPress={this.onReactionPress}
replyBroadcast={this.replyBroadcast} replyBroadcast={this.replyBroadcast}
toggleReactionPicker={this.toggleReactionPicker} reactionInit={this.reactionInit}
onDiscussionPress={this.onDiscussionPress} onDiscussionPress={this.onDiscussionPress}
onOpenFileModal={onOpenFileModal} onOpenFileModal={onOpenFileModal}
getCustomEmoji={getCustomEmoji} getCustomEmoji={getCustomEmoji}

View File

@ -1,7 +1,6 @@
import moment from 'moment'; import moment from 'moment';
import I18n from '../../i18n'; import I18n from '../../i18n';
import database from '../../lib/realm';
import { DISCUSSION } from './constants'; import { DISCUSSION } from './constants';
export const formatLastMessage = (lm, customFormat) => { export const formatLastMessage = (lm, customFormat) => {
@ -96,25 +95,6 @@ export const getInfoMessage = ({
return ''; return '';
}; };
export const getCustomEmoji = (content) => {
// search by name
const data = database.objects('customEmojis').filtered('name == $0', content);
if (data.length) {
return data[0];
}
// searches by alias
// RealmJS doesn't support IN operator: https://github.com/realm/realm-js/issues/450
const emojis = database.objects('customEmojis');
const findByAlias = emojis.find((emoji) => {
if (emoji.aliases.length && emoji.aliases.findIndex(alias => alias === content) !== -1) {
return true;
}
return false;
});
return findByAlias;
};
export const getMessageTranslation = (message, autoTranslateLanguage) => { export const getMessageTranslation = (message, autoTranslateLanguage) => {
if (!autoTranslateLanguage) { if (!autoTranslateLanguage) {
return null; return null;

82
app/lib/database/index.js Normal file
View File

@ -0,0 +1,82 @@
import { Database } from '@nozbe/watermelondb';
import SQLiteAdapter from '@nozbe/watermelondb/adapters/sqlite';
import RNFetchBlob from 'rn-fetch-blob';
import Subscription from './model/Subscription';
import Room from './model/Room';
import Message from './model/Message';
import Thread from './model/Thread';
import ThreadMessage from './model/ThreadMessage';
import CustomEmoji from './model/CustomEmoji';
import FrequentlyUsedEmoji from './model/FrequentlyUsedEmoji';
import Upload from './model/Upload';
import Setting from './model/Setting';
import Role from './model/Role';
import Permission from './model/Permission';
import SlashCommand from './model/SlashCommand';
import User from './model/User';
import Server from './model/Server';
import serversSchema from './schema/servers';
import appSchema from './schema/app';
import { isIOS } from '../../utils/deviceInfo';
const appGroupPath = isIOS ? `${ RNFetchBlob.fs.syncPathAppGroup('group.ios.chat.rocket') }/` : '';
if (__DEV__ && isIOS) {
console.log(appGroupPath);
}
class DB {
databases = {
serversDB: new Database({
adapter: new SQLiteAdapter({
dbName: `${ appGroupPath }default.db`,
schema: serversSchema
}),
modelClasses: [Server, User],
actionsEnabled: true
})
}
get active() {
return this.databases.activeDB;
}
get servers() {
return this.databases.serversDB;
}
setActiveDB(database = '') {
const path = database.replace(/(^\w+:|^)\/\//, '');
const dbName = `${ appGroupPath }${ path }.db`;
const adapter = new SQLiteAdapter({
dbName,
schema: appSchema
});
this.databases.activeDB = new Database({
adapter,
modelClasses: [
Subscription,
Room,
Message,
Thread,
ThreadMessage,
CustomEmoji,
FrequentlyUsedEmoji,
Upload,
Setting,
Role,
Permission,
SlashCommand
],
actionsEnabled: true
});
}
}
const db = new DB();
export default db;

View File

@ -0,0 +1,16 @@
import { Model } from '@nozbe/watermelondb';
import { field, date, json } from '@nozbe/watermelondb/decorators';
import { sanitizer } from '../utils';
export default class CustomEmoji extends Model {
static table = 'custom_emojis';
@field('name') name;
@json('aliases', sanitizer) aliases;
@field('extension') extension;
@date('_updated_at') _updatedAt;
}

View File

@ -0,0 +1,14 @@
import { Model } from '@nozbe/watermelondb';
import { field } from '@nozbe/watermelondb/decorators';
export default class FrequentlyUsedEmoji extends Model {
static table = 'frequently_used_emojis';
@field('content') content;
@field('extension') extension;
@field('is_custom') isCustom;
@field('count') count;
}

View File

@ -0,0 +1,76 @@
import { Model } from '@nozbe/watermelondb';
import {
field, relation, date, json
} from '@nozbe/watermelondb/decorators';
import { sanitizer } from '../utils';
export default class Message extends Model {
static table = 'messages';
static associations = {
subscriptions: { type: 'belongs_to', key: 'rid' }
}
@field('msg') msg;
@field('t') t;
@date('ts') ts;
@json('u', sanitizer) u;
@relation('subscriptions', 'rid') subscription;
@field('alias') alias;
@json('parse_urls', sanitizer) parseUrls;
@field('groupable') groupable;
@field('avatar') avatar;
@json('attachments', sanitizer) attachments;
@json('urls', sanitizer) urls;
@date('_updated_at') _updatedAt;
@field('status') status;
@field('pinned') pinned;
@field('starred') starred;
@json('edited_by', sanitizer) editedBy;
@json('reactions', sanitizer) reactions;
@field('role') role;
@field('drid') drid;
@field('dcount') dcount;
@date('dlm') dlm;
@field('tmid') tmid;
@field('tcount') tcount;
@date('tlm') tlm;
@json('replies', sanitizer) replies;
@json('mentions', sanitizer) mentions;
@json('channels', sanitizer) channels;
@field('unread') unread;
@field('auto_translate') autoTranslate;
@json('translations', sanitizer) translations;
@field('tmsg') tmsg;
}

View File

@ -0,0 +1,12 @@
import { Model } from '@nozbe/watermelondb';
import { json, date } from '@nozbe/watermelondb/decorators';
import { sanitizer } from '../utils';
export default class Permission extends Model {
static table = 'permissions';
@json('roles', sanitizer) roles;
@date('_updated_at') _updatedAt;
}

View File

@ -0,0 +1,8 @@
import { Model } from '@nozbe/watermelondb';
import { field } from '@nozbe/watermelondb/decorators';
export default class Role extends Model {
static table = 'roles';
@field('description') description;
}

View File

@ -0,0 +1,16 @@
import { Model } from '@nozbe/watermelondb';
import { field, json } from '@nozbe/watermelondb/decorators';
import { sanitizer } from '../utils';
export default class Room extends Model {
static table = 'rooms';
@json('custom_fields', sanitizer) customFields;
@field('broadcast') broadcast;
@field('encrypted') encrypted;
@field('ro') ro;
}

View File

@ -0,0 +1,20 @@
import { Model } from '@nozbe/watermelondb';
import { field, date } from '@nozbe/watermelondb/decorators';
export default class Server extends Model {
static table = 'servers';
@field('name') name;
@field('icon_url') iconURL;
@field('use_real_name') useRealName;
@field('file_upload_media_type_white_list') FileUpload_MediaTypeWhiteList;
@field('file_upload_max_file_size') FileUpload_MaxFileSize;
@date('rooms_updated_at') roomsUpdatedAt;
@field('version') version;
}

View File

@ -0,0 +1,14 @@
import { Model } from '@nozbe/watermelondb';
import { field, date } from '@nozbe/watermelondb/decorators';
export default class Setting extends Model {
static table = 'settings';
@field('value_as_string') valueAsString;
@field('value_as_boolean') valueAsBoolean;
@field('value_as_number') valueAsNumber;
@date('_updated_at') _updatedAt;
}

View File

@ -0,0 +1,14 @@
import { Model } from '@nozbe/watermelondb';
import { field } from '@nozbe/watermelondb/decorators';
export default class SlashCommand extends Model {
static table = 'slash_commands';
@field('params') params;
@field('description') description;
@field('client_only') clientOnly;
@field('provides_preview') providesPreview;
}

View File

@ -0,0 +1,88 @@
import { Model } from '@nozbe/watermelondb';
import {
field, date, json, children
} from '@nozbe/watermelondb/decorators';
import { sanitizer } from '../utils';
export default class Subscription extends Model {
static table = 'subscriptions';
static associations = {
messages: { type: 'has_many', foreignKey: 'rid' },
threads: { type: 'has_many', foreignKey: 'rid' },
thread_messages: { type: 'has_many', foreignKey: 'subscription_id' },
uploads: { type: 'has_many', foreignKey: 'rid' }
}
@field('_id') _id;
@field('f') f;
@field('t') t;
@date('ts') ts;
@date('ls') ls;
@field('name') name;
@field('fname') fname;
@field('rid') rid;
@field('open') open;
@field('alert') alert;
@json('roles', sanitizer) roles;
@field('unread') unread;
@field('user_mentions') userMentions;
@date('room_updated_at') roomUpdatedAt;
@field('ro') ro;
@date('last_open') lastOpen;
@field('description') description;
@field('announcement') announcement;
@field('topic') topic;
@field('blocked') blocked;
@field('blocker') blocker;
@field('react_when_read_only') reactWhenReadOnly;
@field('archived') archived;
@field('join_code_required') joinCodeRequired;
@field('notifications') notifications;
@json('muted', sanitizer) muted;
@field('broadcast') broadcast;
@field('prid') prid;
@field('draft_message') draftMessage;
@date('last_thread_sync') lastThreadSync;
@field('auto_translate') autoTranslate;
@field('auto_translate_language') autoTranslateLanguage;
@json('last_message', sanitizer) lastMessage;
@children('messages') messages;
@children('threads') threads;
@children('thread_messages') threadMessages;
}

View File

@ -0,0 +1,74 @@
import { Model } from '@nozbe/watermelondb';
import {
field, relation, date, json
} from '@nozbe/watermelondb/decorators';
import { sanitizer } from '../utils';
export default class Thread extends Model {
static table = 'threads';
static associations = {
subscriptions: { type: 'belongs_to', key: 'rid' }
}
@field('msg') msg;
@field('t') t;
@date('ts') ts;
@json('u', sanitizer) u;
@relation('subscriptions', 'rid') subscription;
@field('alias') alias;
@json('parse_urls', sanitizer) parseUrls;
@field('groupable') groupable;
@field('avatar') avatar;
@json('attachments', sanitizer) attachments;
@json('urls', sanitizer) urls;
@date('_updated_at') _updatedAt;
@field('status') status;
@field('pinned') pinned;
@field('starred') starred;
@json('edited_by', sanitizer) editedBy;
@json('reactions', sanitizer) reactions;
@field('role') role;
@field('drid') drid;
@field('dcount') dcount;
@date('dlm') dlm;
@field('tmid') tmid;
@field('tcount') tcount;
@date('tlm') tlm;
@json('replies', sanitizer) replies;
@json('mentions', sanitizer) mentions;
@json('channels', sanitizer) channels;
@field('unread') unread;
@field('auto_translate') autoTranslate;
@json('translations', sanitizer) translations;
}

View File

@ -0,0 +1,76 @@
import { Model } from '@nozbe/watermelondb';
import {
field, relation, date, json
} from '@nozbe/watermelondb/decorators';
import { sanitizer } from '../utils';
export default class ThreadMessage extends Model {
static table = 'thread_messages';
static associations = {
subscriptions: { type: 'belongs_to', key: 'subscription_id' }
}
@field('msg') msg;
@field('t') t;
@date('ts') ts;
@json('u', sanitizer) u;
@relation('subscriptions', 'subscription_id') subscription;
@field('rid') rid;
@field('alias') alias;
@json('parse_urls', sanitizer) parseUrls;
@field('groupable') groupable;
@field('avatar') avatar;
@json('attachments', sanitizer) attachments;
@json('urls', sanitizer) urls;
@date('_updated_at') _updatedAt;
@field('status') status;
@field('pinned') pinned;
@field('starred') starred;
@json('edited_by', sanitizer) editedBy;
@json('reactions', sanitizer) reactions;
@field('role') role;
@field('drid') drid;
@field('dcount') dcount;
@date('dlm') dlm;
@field('tcount') tcount;
@date('tlm') tlm;
@json('replies', sanitizer) replies;
@json('mentions', sanitizer) mentions;
@json('channels', sanitizer) channels;
@field('unread') unread;
@field('auto_translate') autoTranslate;
@json('translations', sanitizer) translations;
@field('draft_message') draftMessage;
}

View File

@ -0,0 +1,28 @@
import { Model } from '@nozbe/watermelondb';
import { field, relation } from '@nozbe/watermelondb/decorators';
export default class Upload extends Model {
static table = 'uploads';
static associations = {
subscriptions: { type: 'belongs_to', key: 'rid' }
}
@field('path') path;
@relation('subscriptions', 'rid') subscription;
@field('name') name;
@field('description') description;
@field('size') size;
@field('type') type;
@field('store') store;
@field('progress') progress;
@field('error') error;
}

View File

@ -0,0 +1,20 @@
import { Model } from '@nozbe/watermelondb';
import { field, json } from '@nozbe/watermelondb/decorators';
import { sanitizer } from '../utils';
export default class User extends Model {
static table = 'users';
@field('token') token;
@field('username') username;
@field('name') name;
@field('language') language;
@field('status') status;
@json('roles', sanitizer) roles;
}

View File

@ -0,0 +1,222 @@
import { appSchema, tableSchema } from '@nozbe/watermelondb';
export default appSchema({
version: 1,
tables: [
tableSchema({
name: 'subscriptions',
columns: [
{ name: '_id', type: 'string' },
{ name: 'f', type: 'boolean' },
{ name: 't', type: 'string', isIndexed: true },
{ name: 'ts', type: 'number' },
{ name: 'ls', type: 'number' },
{ name: 'name', type: 'string', isIndexed: true },
{ name: 'fname', type: 'string' },
{ name: 'rid', type: 'string', isIndexed: true },
{ name: 'open', type: 'boolean' },
{ name: 'alert', type: 'boolean' },
{ name: 'roles', type: 'string', isOptional: true },
{ name: 'unread', type: 'number' },
{ name: 'user_mentions', type: 'number' },
{ name: 'room_updated_at', type: 'number' },
{ name: 'ro', type: 'boolean' },
{ name: 'last_open', type: 'number', isOptional: true },
{ name: 'last_message', type: 'string', isOptional: true },
{ name: 'description', type: 'string', isOptional: true },
{ name: 'announcement', type: 'string', isOptional: true },
{ name: 'topic', type: 'string', isOptional: true },
{ name: 'blocked', type: 'boolean', isOptional: true },
{ name: 'blocker', type: 'boolean', isOptional: true },
{ name: 'react_when_read_only', type: 'boolean', isOptional: true },
{ name: 'archived', type: 'boolean' },
{ name: 'join_code_required', type: 'boolean', isOptional: true },
{ name: 'muted', type: 'string', isOptional: true },
{ name: 'broadcast', type: 'boolean', isOptional: true },
{ name: 'prid', type: 'string', isOptional: true },
{ name: 'draft_message', type: 'string', isOptional: true },
{ name: 'last_thread_sync', type: 'number', isOptional: true },
{ name: 'auto_translate', type: 'boolean', isOptional: true },
{ name: 'auto_translate_language', type: 'string' }
]
}),
tableSchema({
name: 'rooms',
columns: [
{ name: 'custom_fields', type: 'string' },
{ name: 'broadcast', type: 'boolean' },
{ name: 'encrypted', type: 'boolean' },
{ name: 'ro', type: 'boolean' }
]
}),
tableSchema({
name: 'messages',
columns: [
{ name: 'msg', type: 'string', isOptional: true },
{ name: 't', type: 'string', isOptional: true },
{ name: 'rid', type: 'string', isIndexed: true },
{ name: 'ts', type: 'number' },
{ name: 'u', type: 'string' },
{ name: 'alias', type: 'string' },
{ name: 'parse_urls', type: 'string' },
{ name: 'groupable', type: 'boolean', isOptional: true },
{ name: 'avatar', type: 'string', isOptional: true },
{ name: 'attachments', type: 'string', isOptional: true },
{ name: 'urls', type: 'string', isOptional: true },
{ name: '_updated_at', type: 'number' },
{ name: 'status', type: 'number', isOptional: true },
{ name: 'pinned', type: 'boolean', isOptional: true },
{ name: 'starred', type: 'boolean', isOptional: true },
{ name: 'edited_by', type: 'string', isOptional: true },
{ name: 'reactions', type: 'string', isOptional: true },
{ name: 'role', type: 'string', isOptional: true },
{ name: 'drid', type: 'string', isOptional: true },
{ name: 'dcount', type: 'number', isOptional: true },
{ name: 'dlm', type: 'number', isOptional: true },
{ name: 'tmid', type: 'string', isOptional: true },
{ name: 'tcount', type: 'number', isOptional: true },
{ name: 'tlm', type: 'number', isOptional: true },
{ name: 'replies', type: 'string', isOptional: true },
{ name: 'mentions', type: 'string', isOptional: true },
{ name: 'channels', type: 'string', isOptional: true },
{ name: 'unread', type: 'boolean', isOptional: true },
{ name: 'auto_translate', type: 'boolean', isOptional: true },
{ name: 'translations', type: 'string', isOptional: true },
{ name: 'tmsg', type: 'string', isOptional: true }
]
}),
tableSchema({
name: 'threads',
columns: [
{ name: 'msg', type: 'string', isOptional: true },
{ name: 't', type: 'string', isOptional: true },
{ name: 'rid', type: 'string', isIndexed: true },
{ name: '_updated_at', type: 'number' },
{ name: 'ts', type: 'number' },
{ name: 'u', type: 'string' },
{ name: 'alias', type: 'string', isOptional: true },
{ name: 'parse_urls', type: 'string', isOptional: true },
{ name: 'groupable', type: 'boolean', isOptional: true },
{ name: 'avatar', type: 'string', isOptional: true },
{ name: 'attachments', type: 'string', isOptional: true },
{ name: 'urls', type: 'string', isOptional: true },
{ name: 'status', type: 'number', isOptional: true },
{ name: 'pinned', type: 'boolean', isOptional: true },
{ name: 'starred', type: 'boolean', isOptional: true },
{ name: 'edited_by', type: 'string', isOptional: true },
{ name: 'reactions', type: 'string', isOptional: true },
{ name: 'role', type: 'string', isOptional: true },
{ name: 'drid', type: 'string', isOptional: true },
{ name: 'dcount', type: 'number', isOptional: true },
{ name: 'dlm', type: 'number', isOptional: true },
{ name: 'tmid', type: 'string', isOptional: true },
{ name: 'tcount', type: 'number', isOptional: true },
{ name: 'tlm', type: 'number', isOptional: true },
{ name: 'replies', type: 'string', isOptional: true },
{ name: 'mentions', type: 'string', isOptional: true },
{ name: 'channels', type: 'string', isOptional: true },
{ name: 'unread', type: 'boolean', isOptional: true },
{ name: 'auto_translate', type: 'boolean', isOptional: true },
{ name: 'translations', type: 'string', isOptional: true }
]
}),
tableSchema({
name: 'thread_messages',
columns: [
{ name: 'msg', type: 'string', isOptional: true },
{ name: 't', type: 'string', isOptional: true },
{ name: 'rid', type: 'string', isIndexed: true },
{ name: 'subscription_id', type: 'string', isIndexed: true },
{ name: '_updated_at', type: 'number' },
{ name: 'ts', type: 'number' },
{ name: 'u', type: 'string' },
{ name: 'alias', type: 'string', isOptional: true },
{ name: 'parse_urls', type: 'string', isOptional: true },
{ name: 'groupable', type: 'boolean', isOptional: true },
{ name: 'avatar', type: 'string', isOptional: true },
{ name: 'attachments', type: 'string', isOptional: true },
{ name: 'urls', type: 'string', isOptional: true },
{ name: 'status', type: 'number', isOptional: true },
{ name: 'pinned', type: 'boolean', isOptional: true },
{ name: 'starred', type: 'boolean', isOptional: true },
{ name: 'edited_by', type: 'string', isOptional: true },
{ name: 'reactions', type: 'string', isOptional: true },
{ name: 'role', type: 'string', isOptional: true },
{ name: 'drid', type: 'string', isOptional: true },
{ name: 'dcount', type: 'number', isOptional: true },
{ name: 'dlm', type: 'number', isOptional: true },
{ name: 'tcount', type: 'number', isOptional: true },
{ name: 'tlm', type: 'number', isOptional: true },
{ name: 'replies', type: 'string', isOptional: true },
{ name: 'mentions', type: 'string', isOptional: true },
{ name: 'channels', type: 'string', isOptional: true },
{ name: 'unread', type: 'boolean', isOptional: true },
{ name: 'auto_translate', type: 'boolean', isOptional: true },
{ name: 'translations', type: 'string', isOptional: true }
]
}),
tableSchema({
name: 'custom_emojis',
columns: [
{ name: 'name', type: 'string', isOptional: true },
{ name: 'aliases', type: 'string', isOptional: true },
{ name: 'extension', type: 'string' },
{ name: '_updated_at', type: 'number' }
]
}),
tableSchema({
name: 'frequently_used_emojis',
columns: [
{ name: 'content', type: 'string', isOptional: true },
{ name: 'extension', type: 'string', isOptional: true },
{ name: 'is_custom', type: 'boolean' },
{ name: 'count', type: 'number' }
]
}),
tableSchema({
name: 'uploads',
columns: [
{ name: 'path', type: 'string', isOptional: true },
{ name: 'rid', type: 'string', isIndexed: true },
{ name: 'name', type: 'string', isOptional: true },
{ name: 'description', type: 'string', isOptional: true },
{ name: 'size', type: 'number' },
{ name: 'type', type: 'string', isOptional: true },
{ name: 'store', type: 'string', isOptional: true },
{ name: 'progress', type: 'number' },
{ name: 'error', type: 'boolean' }
]
}),
tableSchema({
name: 'settings',
columns: [
{ name: 'value_as_string', type: 'string', isOptional: true },
{ name: 'value_as_boolean', type: 'boolean', isOptional: true },
{ name: 'value_as_number', type: 'number', isOptional: true },
{ name: '_updated_at', type: 'number', isOptional: true }
]
}),
tableSchema({
name: 'roles',
columns: [
{ name: 'description', type: 'string', isOptional: true }
]
}),
tableSchema({
name: 'permissions',
columns: [
{ name: 'roles', type: 'string' },
{ name: '_updated_at', type: 'number', isOptional: true }
]
}),
tableSchema({
name: 'slash_commands',
columns: [
{ name: 'params', type: 'string', isOptional: true },
{ name: 'description', type: 'string', isOptional: true },
{ name: 'client_only', type: 'boolean', isOptional: true },
{ name: 'provides_preview', type: 'boolean', isOptional: true }
]
})
]
});

View File

@ -0,0 +1,30 @@
import { appSchema, tableSchema } from '@nozbe/watermelondb';
export default appSchema({
version: 2,
tables: [
tableSchema({
name: 'users',
columns: [
{ name: 'token', type: 'string', isOptional: true },
{ name: 'username', type: 'string', isOptional: true },
{ name: 'name', type: 'string', isOptional: true },
{ name: 'language', type: 'string', isOptional: true },
{ name: 'status', type: 'string', isOptional: true },
{ name: 'roles', type: 'string', isOptional: true }
]
}),
tableSchema({
name: 'servers',
columns: [
{ name: 'name', type: 'string', isOptional: true },
{ name: 'icon_url', type: 'string', isOptional: true },
{ name: 'use_real_name', type: 'boolean', isOptional: true },
{ name: 'file_upload_media_type_white_list', type: 'string', isOptional: true },
{ name: 'file_upload_max_file_size', type: 'number', isOptional: true },
{ name: 'rooms_updated_at', type: 'number', isOptional: true },
{ name: 'version', type: 'string', isOptional: true }
]
})
]
});

View File

@ -0,0 +1 @@
export const sanitizer = r => r;

View File

@ -1,4 +1,4 @@
import database from '../realm'; import database from '../database';
const restTypes = { const restTypes = {
channel: 'channels', direct: 'im', group: 'groups' channel: 'channels', direct: 'im', group: 'groups'
@ -18,14 +18,19 @@ async function open({ type, rid }) {
} }
export default async function canOpenRoom({ rid, path }) { export default async function canOpenRoom({ rid, path }) {
try {
const db = database.active;
const subsCollection = db.collections.get('subscriptions');
const [type] = path.split('/'); const [type] = path.split('/');
if (type === 'channel') { if (type === 'channel') {
return true; return true;
} }
const room = database.objects('subscriptions').filtered('rid == $0', rid); try {
if (room.length) { await subsCollection.find(rid);
return true; return true;
} catch (error) {
// Do nothing
} }
try { try {
@ -33,4 +38,7 @@ export default async function canOpenRoom({ rid, path }) {
} catch (e) { } catch (e) {
return false; return false;
} }
} catch (e) {
return false;
}
} }

View File

@ -1,45 +1,110 @@
import { InteractionManager } from 'react-native'; import { InteractionManager } from 'react-native';
import semver from 'semver'; import semver from 'semver';
import orderBy from 'lodash/orderBy';
import { sanitizedRaw } from '@nozbe/watermelondb/RawRecord';
import reduxStore from '../createStore'; import reduxStore from '../createStore';
import database from '../realm'; import database from '../database';
import log from '../../utils/log'; import log from '../../utils/log';
import { setCustomEmojis as setCustomEmojisAction } from '../../actions/customEmojis';
const getUpdatedSince = () => { const getUpdatedSince = (allEmojis) => {
const emoji = database.objects('customEmojis').sorted('_updatedAt', true)[0]; if (!allEmojis.length) {
return emoji && emoji._updatedAt.toISOString(); return null;
}
const ordered = orderBy(allEmojis.filter(item => item._updatedAt !== null), ['_updatedAt'], ['desc']);
return ordered && ordered[0]._updatedAt.toISOString();
}; };
const create = (customEmojis) => { const updateEmojis = async({ update = [], remove = [], allRecords }) => {
if (customEmojis && customEmojis.length) { if (!((update && update.length) || (remove && remove.length))) {
customEmojis.forEach((emoji) => { return;
try {
database.create('customEmojis', emoji, true);
} catch (e) {
// log('getEmojis create', e);
} }
const db = database.active;
const emojisCollection = db.collections.get('custom_emojis');
let emojisToCreate = [];
let emojisToUpdate = [];
let emojisToDelete = [];
// Create or update
if (update && update.length) {
emojisToCreate = update.filter(i1 => !allRecords.find(i2 => i1._id === i2.id));
emojisToUpdate = allRecords.filter(i1 => update.find(i2 => i1.id === i2._id));
emojisToCreate = emojisToCreate.map(emoji => emojisCollection.prepareCreate((e) => {
e._raw = sanitizedRaw({ id: emoji._id }, emojisCollection.schema);
Object.assign(e, emoji);
}));
emojisToUpdate = emojisToUpdate.map((emoji) => {
const newEmoji = update.find(e => e._id === emoji.id);
return emoji.prepareUpdate((e) => {
Object.assign(e, newEmoji);
});
}); });
} }
if (remove && remove.length) {
emojisToDelete = allRecords.filter(i1 => remove.find(i2 => i1.id === i2._id));
emojisToDelete = emojisToDelete.map(emoji => emoji.prepareDestroyPermanently());
}
try {
await db.action(async() => {
await db.batch(
...emojisToCreate,
...emojisToUpdate,
...emojisToDelete
);
});
return true;
} catch (e) {
log(e);
}
}; };
export async function setCustomEmojis() {
const db = database.active;
const emojisCollection = db.collections.get('custom_emojis');
const allEmojis = await emojisCollection.query().fetch();
const parsed = allEmojis.reduce((ret, item) => {
ret[item.name] = {
name: item.name,
extension: item.extension
};
item.aliases.forEach((alias) => {
ret[alias] = {
name: item.name,
extension: item.extension
};
});
return ret;
}, {});
reduxStore.dispatch(setCustomEmojisAction(parsed));
}
export default function() { export function getCustomEmojis() {
return new Promise(async(resolve) => { return new Promise(async(resolve) => {
try { try {
const serverVersion = reduxStore.getState().server.version; const serverVersion = reduxStore.getState().server.version;
const updatedSince = getUpdatedSince(); const db = database.active;
const emojisCollection = db.collections.get('custom_emojis');
const allRecords = await emojisCollection.query().fetch();
const updatedSince = await getUpdatedSince(allRecords);
// if server version is lower than 0.75.0, fetches from old api // if server version is lower than 0.75.0, fetches from old api
if (semver.lt(serverVersion, '0.75.0')) { if (semver.lt(serverVersion, '0.75.0')) {
// RC 0.61.0 // RC 0.61.0
const result = await this.sdk.get('emoji-custom'); const result = await this.sdk.get('emoji-custom');
InteractionManager.runAfterInteractions(() => { InteractionManager.runAfterInteractions(async() => {
let { emojis } = result; let { emojis } = result;
emojis = emojis.filter(emoji => !updatedSince || emoji._updatedAt > updatedSince); emojis = emojis.filter(emoji => !updatedSince || emoji._updatedAt > updatedSince);
database.write(() => { const changedEmojis = await updateEmojis({ update: emojis, allRecords });
create(emojis);
}); // `setCustomEmojis` is fired on selectServer
// We run it again only if emojis were changed
if (changedEmojis) {
setCustomEmojis();
}
return resolve(); return resolve();
}); });
} else { } else {
@ -55,27 +120,18 @@ export default function() {
return resolve(); return resolve();
} }
InteractionManager.runAfterInteractions( InteractionManager.runAfterInteractions(async() => {
() => database.write(() => {
const { emojis } = result; const { emojis } = result;
create(emojis.update); const { update, remove } = emojis;
const changedEmojis = await updateEmojis({ update, remove, allRecords });
if (emojis.delete && emojis.delete.length) { // `setCustomEmojis` is fired on selectServer
emojis.delete.forEach((emoji) => { // We run it again only if emojis were changed
try { if (changedEmojis) {
const emojiRecord = database.objectForPrimaryKey('customEmojis', emoji._id); setCustomEmojis();
if (emojiRecord) {
database.delete(emojiRecord);
}
} catch (e) {
log(e);
} }
}); });
} }
return resolve();
})
);
}
} catch (e) { } catch (e) {
log(e); log(e);
return resolve(); return resolve();

View File

@ -1,31 +1,82 @@
import { InteractionManager } from 'react-native'; import { InteractionManager } from 'react-native';
import semver from 'semver'; import semver from 'semver';
import { sanitizedRaw } from '@nozbe/watermelondb/RawRecord';
import { orderBy } from 'lodash';
import database from '../realm'; import database from '../database';
import log from '../../utils/log'; import log from '../../utils/log';
import reduxStore from '../createStore'; import reduxStore from '../createStore';
import protectedFunction from './helpers/protectedFunction';
const getUpdatedSince = () => { const getUpdatedSince = (allRecords) => {
const permissions = database.objects('permissions').sorted('_updatedAt', true)[0];
return permissions && permissions._updatedAt.toISOString();
};
const create = (permissions) => {
if (permissions && permissions.length) {
permissions.forEach((permission) => {
try { try {
database.create('permissions', permission, true); if (!allRecords.length) {
return null;
}
const ordered = orderBy(allRecords.filter(item => item._updatedAt !== null), ['_updatedAt'], ['desc']);
return ordered && ordered[0]._updatedAt.toISOString();
} catch (e) { } catch (e) {
log(e); log(e);
} }
return null;
};
const updatePermissions = async({ update = [], remove = [], allRecords }) => {
if (!((update && update.length) || (remove && remove.length))) {
return;
}
const db = database.active;
const permissionsCollection = db.collections.get('permissions');
// filter permissions
let permissionsToCreate = [];
let permissionsToUpdate = [];
let permissionsToDelete = [];
// Create or update
if (update && update.length) {
permissionsToCreate = update.filter(i1 => !allRecords.find(i2 => i1._id === i2.id));
permissionsToUpdate = allRecords.filter(i1 => update.find(i2 => i1.id === i2._id));
permissionsToCreate = permissionsToCreate.map(permission => permissionsCollection.prepareCreate(protectedFunction((p) => {
p._raw = sanitizedRaw({ id: permission._id }, permissionsCollection.schema);
Object.assign(p, permission);
})));
permissionsToUpdate = permissionsToUpdate.map((permission) => {
const newPermission = update.find(p => p._id === permission.id);
return permission.prepareUpdate(protectedFunction((p) => {
Object.assign(p, newPermission);
}));
}); });
} }
// Delete
if (remove && remove.length) {
permissionsToDelete = allRecords.filter(i1 => remove.find(i2 => i1.id === i2._id));
permissionsToDelete = permissionsToDelete.map(permission => permission.prepareDestroyPermanently());
}
const batch = [
...permissionsToCreate,
...permissionsToUpdate,
...permissionsToDelete
];
try {
await db.action(async() => {
await db.batch(...batch);
});
} catch (e) {
log(e);
}
}; };
export default function() { export default function() {
return new Promise(async(resolve) => { return new Promise(async(resolve) => {
try { try {
const serverVersion = reduxStore.getState().server.version; const serverVersion = reduxStore.getState().server.version;
const db = database.active;
const permissionsCollection = db.collections.get('permissions');
const allRecords = await permissionsCollection.query().fetch();
// if server version is lower than 0.73.0, fetches from old api // if server version is lower than 0.73.0, fetches from old api
if (semver.lt(serverVersion, '0.73.0')) { if (semver.lt(serverVersion, '0.73.0')) {
@ -34,15 +85,13 @@ export default function() {
if (!result.success) { if (!result.success) {
return resolve(); return resolve();
} }
InteractionManager.runAfterInteractions(() => { InteractionManager.runAfterInteractions(async() => {
database.write(() => { await updatePermissions({ update: result.permissions, allRecords });
create(result.permissions);
});
return resolve(); return resolve();
}); });
} else { } else {
const params = {}; const params = {};
const updatedSince = getUpdatedSince(); const updatedSince = await getUpdatedSince(allRecords);
if (updatedSince) { if (updatedSince) {
params.updatedSince = updatedSince; params.updatedSince = updatedSince;
} }
@ -53,25 +102,10 @@ export default function() {
return resolve(); return resolve();
} }
InteractionManager.runAfterInteractions( InteractionManager.runAfterInteractions(async() => {
() => database.write(() => { await updatePermissions({ update: result.update, remove: result.delete, allRecords });
create(result.update);
if (result.delete && result.delete.length) {
result.delete.forEach((p) => {
try {
const permission = database.objectForPrimaryKey('permissions', p._id);
if (permission) {
database.delete(permission);
}
} catch (e) {
log(e);
}
});
}
return resolve(); return resolve();
}) });
);
} }
} catch (e) { } catch (e) {
log(e); log(e);

View File

@ -1,9 +1,12 @@
import { InteractionManager } from 'react-native'; import { InteractionManager } from 'react-native';
import { sanitizedRaw } from '@nozbe/watermelondb/RawRecord';
import database from '../realm'; import database from '../database';
import log from '../../utils/log'; import log from '../../utils/log';
import protectedFunction from './helpers/protectedFunction';
export default function() { export default function() {
const db = database.active;
return new Promise(async(resolve) => { return new Promise(async(resolve) => {
try { try {
// RC 0.70.0 // RC 0.70.0
@ -16,14 +19,41 @@ export default function() {
const { roles } = result; const { roles } = result;
if (roles && roles.length) { if (roles && roles.length) {
InteractionManager.runAfterInteractions(() => { InteractionManager.runAfterInteractions(async() => {
database.write(() => roles.forEach((role) => { await db.action(async() => {
const rolesCollections = db.collections.get('roles');
const allRolesRecords = await rolesCollections.query().fetch();
// filter roles
let rolesToCreate = roles.filter(i1 => !allRolesRecords.find(i2 => i1._id === i2.id));
let rolesToUpdate = allRolesRecords.filter(i1 => roles.find(i2 => i1.id === i2._id));
// Create
rolesToCreate = rolesToCreate.map(role => rolesCollections.prepareCreate(protectedFunction((r) => {
r._raw = sanitizedRaw({ id: role._id }, rolesCollections.schema);
Object.assign(r, role);
})));
// Update
rolesToUpdate = rolesToUpdate.map((role) => {
const newRole = roles.find(r => r._id === role.id);
return role.prepareUpdate(protectedFunction((r) => {
Object.assign(r, newRole);
}));
});
const allRecords = [
...rolesToCreate,
...rolesToUpdate
];
try { try {
database.create('roles', role, true); await db.batch(...allRecords);
} catch (e) { } catch (e) {
log(e); log(e);
} }
})); return allRecords.length;
});
return resolve(); return resolve();
}); });
} }

View File

@ -1,23 +1,58 @@
import { InteractionManager } from 'react-native'; import { InteractionManager } from 'react-native';
import { sanitizedRaw } from '@nozbe/watermelondb/RawRecord';
import { Q } from '@nozbe/watermelondb';
import reduxStore from '../createStore'; import reduxStore from '../createStore';
import database from '../realm';
import * as actions from '../../actions'; import * as actions from '../../actions';
import log from '../../utils/log';
import settings from '../../constants/settings'; import settings from '../../constants/settings';
import log from '../../utils/log';
import database from '../database';
import protectedFunction from './helpers/protectedFunction';
function updateServer(param) { const serverInfoKeys = ['Site_Name', 'UI_Use_Real_Name', 'FileUpload_MediaTypeWhiteList', 'FileUpload_MaxFileSize'];
database.databases.serversDB.write(() => {
const serverInfoUpdate = async(serverInfo, iconSetting) => {
const serversDB = database.servers;
const serverId = reduxStore.getState().server.server;
let info = serverInfo.reduce((allSettings, setting) => {
if (setting._id === 'Site_Name') {
return { ...allSettings, name: setting.valueAsString };
}
if (setting._id === 'UI_Use_Real_Name') {
return { ...allSettings, useRealName: setting.valueAsBoolean };
}
if (setting._id === 'FileUpload_MediaTypeWhiteList') {
return { ...allSettings, FileUpload_MediaTypeWhiteList: setting.valueAsString };
}
if (setting._id === 'FileUpload_MaxFileSize') {
return { ...allSettings, FileUpload_MaxFileSize: setting.valueAsNumber };
}
return allSettings;
}, {});
if (iconSetting) {
const iconURL = `${ serverId }/${ iconSetting.value.url || iconSetting.value.defaultUrl }`;
info = { ...info, iconURL };
}
await serversDB.action(async() => {
try { try {
database.databases.serversDB.create('servers', { id: reduxStore.getState().server.server, ...param }, true); const serversCollection = serversDB.collections.get('servers');
const server = await serversCollection.find(serverId);
await server.update((record) => {
Object.assign(record, info);
});
} catch (e) { } catch (e) {
log(e); log(e);
} }
}); });
} };
export default async function() { export default async function() {
try { try {
const db = database.active;
const settingsParams = JSON.stringify(Object.keys(settings)); const settingsParams = JSON.stringify(Object.keys(settings));
// RC 0.60.0 // RC 0.60.0
const result = await fetch(`${ this.sdk.client.host }/api/v1/settings.public?query={"_id":{"$in":${ settingsParams }}}`).then(response => response.json()); const result = await fetch(`${ this.sdk.client.host }/api/v1/settings.public?query={"_id":{"$in":${ settingsParams }}}`).then(response => response.json());
@ -27,39 +62,52 @@ export default async function() {
} }
const data = result.settings || []; const data = result.settings || [];
const filteredSettings = this._prepareSettings(data.filter(item => item._id !== 'Assets_favicon_512')); const filteredSettings = this._prepareSettings(data.filter(item => item._id !== 'Assets_favicon_512'));
const filteredSettingsIds = filteredSettings.map(s => s._id);
reduxStore.dispatch(actions.addSettings(this.parseSettings(filteredSettings)));
InteractionManager.runAfterInteractions(async() => {
// filter server info
const serverInfo = filteredSettings.filter(i1 => serverInfoKeys.includes(i1._id));
const iconSetting = data.find(item => item._id === 'Assets_favicon_512');
await serverInfoUpdate(serverInfo, iconSetting);
await db.action(async() => {
const settingsCollection = db.collections.get('settings');
const allSettingsRecords = await settingsCollection
.query(Q.where('id', Q.oneOf(filteredSettingsIds)))
.fetch();
// filter settings
let settingsToCreate = filteredSettings.filter(i1 => !allSettingsRecords.find(i2 => i1._id === i2.id));
let settingsToUpdate = allSettingsRecords.filter(i1 => filteredSettings.find(i2 => i1.id === i2._id));
// Create
settingsToCreate = settingsToCreate.map(setting => settingsCollection.prepareCreate(protectedFunction((s) => {
s._raw = sanitizedRaw({ id: setting._id }, settingsCollection.schema);
Object.assign(s, setting);
})));
// Update
settingsToUpdate = settingsToUpdate.map((setting) => {
const newSetting = filteredSettings.find(s => s._id === setting.id);
return setting.prepareUpdate(protectedFunction((s) => {
Object.assign(s, newSetting);
}));
});
const allRecords = [
...settingsToCreate,
...settingsToUpdate
];
InteractionManager.runAfterInteractions(
() => database.write(
() => filteredSettings.forEach((setting) => {
try { try {
database.create('settings', { ...setting, _updatedAt: new Date() }, true); await db.batch(...allRecords);
} catch (e) { } catch (e) {
log(e); log(e);
} }
return allRecords.length;
if (setting._id === 'Site_Name') { });
updateServer.call(this, { name: setting.valueAsString }); });
}
if (setting._id === 'UI_Use_Real_Name') {
updateServer.call(this, { useRealName: setting.valueAsBoolean });
}
if (setting._id === 'FileUpload_MediaTypeWhiteList') {
updateServer.call(this, { FileUpload_MediaTypeWhiteList: setting.valueAsString });
}
if (setting._id === 'FileUpload_MaxFileSize') {
updateServer.call(this, { FileUpload_MaxFileSize: setting.valueAsNumber });
}
})
)
);
reduxStore.dispatch(actions.addSettings(this.parseSettings(filteredSettings)));
const iconSetting = data.find(item => item._id === 'Assets_favicon_512');
if (iconSetting) {
const baseUrl = reduxStore.getState().server.server;
const iconURL = `${ baseUrl }/${ iconSetting.value.url || iconSetting.value.defaultUrl }`;
updateServer.call(this, { iconURL });
}
} catch (e) { } catch (e) {
log(e); log(e);
} }

View File

@ -1,9 +1,12 @@
import { InteractionManager } from 'react-native'; import { InteractionManager } from 'react-native';
import { sanitizedRaw } from '@nozbe/watermelondb/RawRecord';
import database from '../realm'; import database from '../database';
import log from '../../utils/log'; import log from '../../utils/log';
import protectedFunction from './helpers/protectedFunction';
export default function() { export default function() {
const db = database.active;
return new Promise(async(resolve) => { return new Promise(async(resolve) => {
try { try {
// RC 0.60.2 // RC 0.60.2
@ -17,15 +20,41 @@ export default function() {
const { commands } = result; const { commands } = result;
if (commands && commands.length) { if (commands && commands.length) {
InteractionManager.runAfterInteractions(() => { InteractionManager.runAfterInteractions(async() => {
database.write(() => commands.forEach((command) => { await db.action(async() => {
const slashCommandsCollection = db.collections.get('slash_commands');
const allSlashCommandsRecords = await slashCommandsCollection.query().fetch();
// filter slash commands
let slashCommandsToCreate = commands.filter(i1 => !allSlashCommandsRecords.find(i2 => i1.command === i2.id));
let slashCommandsToUpdate = allSlashCommandsRecords.filter(i1 => commands.find(i2 => i1.id === i2.command));
// Create
slashCommandsToCreate = slashCommandsToCreate.map(command => slashCommandsCollection.prepareCreate(protectedFunction((s) => {
s._raw = sanitizedRaw({ id: command.command }, slashCommandsCollection.schema);
Object.assign(s, command);
})));
// Update
slashCommandsToUpdate = slashCommandsToUpdate.map((command) => {
const newCommand = commands.find(s => s.command === command.id);
return command.prepareUpdate(protectedFunction((s) => {
Object.assign(s, newCommand);
}));
});
const allRecords = [
...slashCommandsToCreate,
...slashCommandsToUpdate
];
try { try {
database.create('slashCommand', command, true); await db.batch(...allRecords);
} catch (e) { } catch (e) {
log(e); log(e);
} }
})); return allRecords.length;
return resolve(); });
}); });
} }
} catch (e) { } catch (e) {

View File

@ -11,18 +11,17 @@ export const merge = (subscription, room) => {
return; return;
} }
if (room) { if (room) {
if (room.rid) { if (room._updatedAt) {
subscription.rid = room.rid;
}
subscription.roomUpdatedAt = room._updatedAt; subscription.roomUpdatedAt = room._updatedAt;
subscription.lastMessage = normalizeMessage(room.lastMessage); subscription.lastMessage = normalizeMessage(room.lastMessage);
subscription.ro = room.ro;
subscription.description = room.description; subscription.description = room.description;
subscription.topic = room.topic; subscription.topic = room.topic;
subscription.announcement = room.announcement; subscription.announcement = room.announcement;
subscription.reactWhenReadOnly = room.reactWhenReadOnly; subscription.reactWhenReadOnly = room.reactWhenReadOnly;
subscription.archived = room.archived; subscription.archived = room.archived || false;
subscription.joinCodeRequired = room.joinCodeRequired; subscription.joinCodeRequired = room.joinCodeRequired;
}
subscription.ro = room.ro;
subscription.broadcast = room.broadcast; subscription.broadcast = room.broadcast;
if (!subscription.roles || !subscription.roles.length) { if (!subscription.roles || !subscription.roles.length) {
subscription.roles = []; subscription.roles = [];

View File

@ -2,12 +2,6 @@ export default fn => (...params) => {
try { try {
fn(...params); fn(...params);
} catch (e) { } catch (e) {
let error = e; console.log(e);
if (typeof error !== 'object') {
error = { error };
}
if (__DEV__) {
alert(error);
}
} }
}; };

View File

@ -1,24 +1,9 @@
import { InteractionManager } from 'react-native'; import { InteractionManager } from 'react-native';
import buildMessage from './helpers/buildMessage';
import database from '../realm';
import log from '../../utils/log'; import log from '../../utils/log';
import updateMessages from './updateMessages';
async function load({ rid: roomId, latest, t }) { async function load({ rid: roomId, latest, t }) {
if (t === 'l') {
try {
// RC 0.51.0
const data = await this.sdk.methodCall('loadHistory', roomId, null, 50, latest);
if (!data || data.status === 'error') {
return [];
}
return data.messages;
} catch (e) {
log(e);
return [];
}
}
let params = { roomId, count: 50 }; let params = { roomId, count: 50 };
if (latest) { if (latest) {
params = { ...params, latest: new Date(latest).toISOString() }; params = { ...params, latest: new Date(latest).toISOString() };
@ -31,30 +16,14 @@ async function load({ rid: roomId, latest, t }) {
return data.messages; return data.messages;
} }
export default function loadMessagesForRoom(...args) { export default function loadMessagesForRoom(args) {
return new Promise(async(resolve, reject) => { return new Promise(async(resolve, reject) => {
try { try {
const data = await load.call(this, ...args); const data = await load.call(this, args);
if (data && data.length) { if (data && data.length) {
InteractionManager.runAfterInteractions(() => { InteractionManager.runAfterInteractions(async() => {
database.write(() => data.forEach((message) => { await updateMessages({ rid: args.rid, update: data });
message = buildMessage(message);
try {
database.create('messages', message, true);
// if it's a thread "header"
if (message.tlm) {
database.create('threads', message, true);
}
// if it belongs to a thread
if (message.tmid) {
message.rid = message.tmid;
database.create('threadMessages', message, true);
}
} catch (e) {
log(e);
}
}));
return resolve(data); return resolve(data);
}); });
} else { } else {

View File

@ -1,14 +1,19 @@
import { InteractionManager } from 'react-native'; import { InteractionManager } from 'react-native';
import buildMessage from './helpers/buildMessage'; import database from '../database';
import database from '../realm';
import log from '../../utils/log'; import log from '../../utils/log';
import updateMessages from './updateMessages';
const getLastUpdate = (rid) => { const getLastUpdate = async(rid) => {
const sub = database try {
.objects('subscriptions') const db = database.active;
.filtered('rid == $0', rid)[0]; const subsCollection = db.collections.get('subscriptions');
return sub && new Date(sub.lastOpen).toISOString(); const sub = await subsCollection.find(rid);
return sub.lastOpen.toISOString();
} catch (e) {
// Do nothing
}
return null;
}; };
async function load({ rid: roomId, lastOpen }) { async function load({ rid: roomId, lastOpen }) {
@ -16,60 +21,24 @@ async function load({ rid: roomId, lastOpen }) {
if (lastOpen) { if (lastOpen) {
lastUpdate = new Date(lastOpen).toISOString(); lastUpdate = new Date(lastOpen).toISOString();
} else { } else {
lastUpdate = getLastUpdate(roomId); lastUpdate = await getLastUpdate(roomId);
} }
// RC 0.60.0 // RC 0.60.0
const { result } = await this.sdk.get('chat.syncMessages', { roomId, lastUpdate }); const { result } = await this.sdk.get('chat.syncMessages', { roomId, lastUpdate });
return result; return result;
} }
export default function loadMissedMessages(...args) { export default function loadMissedMessages(args) {
return new Promise(async(resolve, reject) => { return new Promise(async(resolve, reject) => {
try { try {
const data = (await load.call(this, ...args)); const data = (await load.call(this, { rid: args.rid, lastOpen: args.lastOpen }));
if (data) { if (data) {
if (data.updated && data.updated.length) { const { updated, deleted } = data;
const { updated } = data; InteractionManager.runAfterInteractions(async() => {
InteractionManager.runAfterInteractions(() => { await updateMessages({ rid: args.rid, update: updated, remove: deleted });
database.write(() => updated.forEach((message) => {
try {
message = buildMessage(message);
database.create('messages', message, true);
// if it's a thread "header"
if (message.tlm) {
database.create('threads', message, true);
}
if (message.tmid) {
message.rid = message.tmid;
database.create('threadMessages', message, true);
}
} catch (e) {
log(e);
}
}));
}); });
} }
if (data.deleted && data.deleted.length) {
const { deleted } = data;
InteractionManager.runAfterInteractions(() => {
try {
database.write(() => {
deleted.forEach((m) => {
const message = database.objects('messages').filtered('_id = $0', m._id);
database.delete(message);
const thread = database.objects('threads').filtered('_id = $0', m._id);
database.delete(thread);
const threadMessage = database.objects('threadMessages').filtered('_id = $0', m._id);
database.delete(threadMessage);
});
});
} catch (e) {
log(e);
}
});
}
}
resolve(); resolve();
} catch (e) { } catch (e) {
log(e); log(e);

View File

@ -1,9 +1,11 @@
import { InteractionManager } from 'react-native'; import { InteractionManager } from 'react-native';
import EJSON from 'ejson'; import { Q } from '@nozbe/watermelondb';
import { sanitizedRaw } from '@nozbe/watermelondb/RawRecord';
import buildMessage from './helpers/buildMessage'; import buildMessage from './helpers/buildMessage';
import database from '../realm'; import database from '../database';
import log from '../../utils/log'; import log from '../../utils/log';
import protectedFunction from './helpers/protectedFunction';
async function load({ tmid, offset }) { async function load({ tmid, offset }) {
try { try {
@ -21,29 +23,53 @@ async function load({ tmid, offset }) {
} }
} }
export default function loadThreadMessages({ tmid, offset = 0 }) { export default function loadThreadMessages({ tmid, rid, offset = 0 }) {
return new Promise(async(resolve, reject) => { return new Promise(async(resolve, reject) => {
try { try {
const data = await load.call(this, { tmid, offset }); let data = await load.call(this, { tmid, offset });
if (data && data.length) { if (data && data.length) {
InteractionManager.runAfterInteractions(() => { InteractionManager.runAfterInteractions(async() => {
database.write(() => data.forEach((m) => {
try { try {
const message = buildMessage(EJSON.fromJSONValue(m)); data = data.map(m => buildMessage(m));
message.rid = tmid; const db = database.active;
database.create('threadMessages', message, true); const threadMessagesCollection = db.collections.get('thread_messages');
const allThreadMessagesRecords = await threadMessagesCollection.query(Q.where('rid', tmid)).fetch();
let threadMessagesToCreate = data.filter(i1 => !allThreadMessagesRecords.find(i2 => i1._id === i2.id));
let threadMessagesToUpdate = allThreadMessagesRecords.filter(i1 => data.find(i2 => i1.id === i2._id));
threadMessagesToCreate = threadMessagesToCreate.map(threadMessage => threadMessagesCollection.prepareCreate(protectedFunction((tm) => {
tm._raw = sanitizedRaw({ id: threadMessage._id }, threadMessagesCollection.schema);
Object.assign(tm, threadMessage);
tm.subscription.id = rid;
tm.rid = threadMessage.tmid;
delete threadMessage.tmid;
})));
threadMessagesToUpdate = threadMessagesToUpdate.map((threadMessage) => {
const newThreadMessage = data.find(t => t._id === threadMessage.id);
return threadMessage.prepareUpdate(protectedFunction((tm) => {
Object.assign(tm, newThreadMessage);
tm.rid = threadMessage.tmid;
delete threadMessage.tmid;
}));
});
await db.action(async() => {
await db.batch(
...threadMessagesToCreate,
...threadMessagesToUpdate
);
});
} catch (e) { } catch (e) {
log(e); log(e);
} }
}));
return resolve(data); return resolve(data);
}); });
} else { } else {
return resolve([]); return resolve([]);
} }
} catch (e) { } catch (e) {
log(e);
reject(e); reject(e);
} }
}); });

View File

@ -1,20 +1,26 @@
import database from '../realm'; import database from '../database';
import log from '../../utils/log'; import log from '../../utils/log';
export default async function readMessages(rid) { export default async function readMessages(rid, lastOpen) {
const ls = new Date();
try { try {
// RC 0.61.0 // RC 0.61.0
const data = await this.sdk.post('subscriptions.read', { rid }); const data = await this.sdk.post('subscriptions.read', { rid });
const [subscription] = database.objects('subscriptions').filtered('rid = $0', rid); const db = database.active;
database.write(() => { await db.action(async() => {
subscription.open = true; try {
subscription.alert = false; const subscription = await db.collections.get('subscriptions').find(rid);
subscription.unread = 0; await subscription.update((s) => {
subscription.userMentions = 0; s.open = true;
subscription.groupMentions = 0; s.alert = false;
subscription.ls = ls; s.unread = 0;
subscription.lastOpen = ls; s.userMentions = 0;
s.groupMentions = 0;
s.ls = lastOpen;
s.lastOpen = lastOpen;
});
} catch (e) {
// Do nothing
}
}); });
return data; return data;
} catch (e) { } catch (e) {

View File

@ -1,4 +1,6 @@
import database from '../realm'; import { sanitizedRaw } from '@nozbe/watermelondb/RawRecord';
import database from '../database';
import log from '../../utils/log'; import log from '../../utils/log';
const uploadQueue = {}; const uploadQueue = {};
@ -7,26 +9,28 @@ export function isUploadActive(path) {
return !!uploadQueue[path]; return !!uploadQueue[path];
} }
export function cancelUpload(path) { export async function cancelUpload(item) {
if (uploadQueue[path]) { if (uploadQueue[item.path]) {
uploadQueue[path].abort(); uploadQueue[item.path].abort();
database.write(() => {
const upload = database.objects('uploads').filtered('path = $0', path);
try { try {
database.delete(upload); const db = database.active;
await db.database.action(async() => {
await item.destroyPermanently();
});
} catch (e) { } catch (e) {
log(e); log(e);
} }
}); delete uploadQueue[item.path];
delete uploadQueue[path];
} }
} }
export function sendFileMessage(rid, fileInfo, tmid, server, user) { export function sendFileMessage(rid, fileInfo, tmid, server, user) {
return new Promise((resolve, reject) => { return new Promise(async(resolve, reject) => {
try { try {
const { serversDB } = database.databases; const serversDB = database.servers;
const { FileUpload_MaxFileSize, id: Site_Url } = serversDB.objectForPrimaryKey('servers', server); const serversCollection = serversDB.collections.get('servers');
const serverInfo = await serversCollection.find(server);
const { FileUpload_MaxFileSize, id: Site_Url } = serverInfo;
const { id, token } = user; const { id, token } = user;
// -1 maxFileSize means there is no limit // -1 maxFileSize means there is no limit
@ -41,13 +45,24 @@ export function sendFileMessage(rid, fileInfo, tmid, server, user) {
fileInfo.rid = rid; fileInfo.rid = rid;
database.write(() => { const db = database.active;
const uploadsCollection = db.collections.get('uploads');
let uploadRecord;
try { try {
database.create('uploads', fileInfo, true); uploadRecord = await uploadsCollection.find(fileInfo.path);
} catch (error) {
try {
await db.action(async() => {
uploadRecord = await uploadsCollection.create((u) => {
u._raw = sanitizedRaw({ id: fileInfo.path }, uploadsCollection.schema);
Object.assign(u, fileInfo);
u.subscription.id = rid;
});
});
} catch (e) { } catch (e) {
return log(e); return log(e);
} }
}); }
uploadQueue[fileInfo.path] = xhr; uploadQueue[fileInfo.path] = xhr;
xhr.open('POST', uploadUrl); xhr.open('POST', uploadUrl);
@ -69,56 +84,55 @@ export function sendFileMessage(rid, fileInfo, tmid, server, user) {
xhr.setRequestHeader('X-Auth-Token', token); xhr.setRequestHeader('X-Auth-Token', token);
xhr.setRequestHeader('X-User-Id', id); xhr.setRequestHeader('X-User-Id', id);
xhr.upload.onprogress = ({ total, loaded }) => { xhr.upload.onprogress = async({ total, loaded }) => {
database.write(() => {
fileInfo.progress = Math.floor((loaded / total) * 100);
try { try {
database.create('uploads', fileInfo, true); await db.action(async() => {
} catch (e) { await uploadRecord.update((u) => {
return log(e); u.progress = Math.floor((loaded / total) * 100);
}
}); });
});
} catch (e) {
log(e);
}
}; };
xhr.onload = () => { xhr.onload = async() => {
if (xhr.status >= 200 && xhr.status < 400) { // If response is all good... if (xhr.status >= 200 && xhr.status < 400) { // If response is all good...
database.write(() => {
const upload = database.objects('uploads').filtered('path = $0', fileInfo.path);
try { try {
database.delete(upload); await db.action(async() => {
await uploadRecord.destroyPermanently();
});
const response = JSON.parse(xhr.response); const response = JSON.parse(xhr.response);
resolve(response); resolve(response);
} catch (e) { } catch (e) {
reject(e);
log(e); log(e);
} }
});
} else { } else {
database.write(() => {
fileInfo.error = true;
try { try {
database.create('uploads', fileInfo, true); await db.action(async() => {
await uploadRecord.update((u) => {
u.error = true;
});
});
} catch (e) {
log(e);
}
const response = JSON.parse(xhr.response); const response = JSON.parse(xhr.response);
reject(response); reject(response);
} catch (e) {
reject(e);
log(e);
}
});
} }
}; };
xhr.onerror = (error) => { xhr.onerror = async(error) => {
database.write(() => {
fileInfo.error = true;
try { try {
database.create('uploads', fileInfo, true); await db.action(async() => {
reject(error); await uploadRecord.update((u) => {
u.error = true;
});
});
} catch (e) { } catch (e) {
reject(e);
log(e); log(e);
} }
}); reject(error);
}; };
xhr.send(formData); xhr.send(formData);

View File

@ -1,38 +1,41 @@
import { sanitizedRaw } from '@nozbe/watermelondb/RawRecord';
import messagesStatus from '../../constants/messagesStatus'; import messagesStatus from '../../constants/messagesStatus';
import buildMessage from './helpers/buildMessage'; import database from '../database';
import database from '../realm';
import log from '../../utils/log'; import log from '../../utils/log';
import random from '../../utils/random'; import random from '../../utils/random';
export const getMessage = (rid, msg = '', tmid, user) => { export const getMessage = async(rid, msg = '', tmid, user) => {
const _id = random(17); const _id = random(17);
const { id, username } = user; const { id, username } = user;
const message = { try {
_id, const db = database.active;
rid, const msgCollection = db.collections.get('messages');
msg, let message;
tmid, await db.action(async() => {
ts: new Date(), message = await msgCollection.create((m) => {
_updatedAt: new Date(), m._raw = sanitizedRaw({ id: _id }, msgCollection.schema);
status: messagesStatus.TEMP, m.subscription.id = rid;
u: { m.msg = msg;
m.tmid = tmid;
m.ts = new Date();
m._updatedAt = new Date();
m.status = messagesStatus.TEMP;
m.u = {
_id: id || '1', _id: id || '1',
username username
}
}; };
try {
database.write(() => {
database.create('messages', message, true);
}); });
});
return message;
} catch (error) { } catch (error) {
console.warn('getMessage', error); console.warn('getMessage', error);
} }
return message;
}; };
export async function sendMessageCall(message) { export async function sendMessageCall(message) {
const { const {
_id, rid, msg, tmid id: _id, subscription: { id: rid }, msg, tmid
} = message; } = message;
// RC 0.60.0 // RC 0.60.0
const data = await this.sdk.post('chat.sendMessage', { const data = await this.sdk.post('chat.sendMessage', {
@ -45,24 +48,36 @@ export async function sendMessageCall(message) {
export default async function(rid, msg, tmid, user) { export default async function(rid, msg, tmid, user) {
try { try {
const message = getMessage(rid, msg, tmid, user); const db = database.active;
const [room] = database.objects('subscriptions').filtered('rid == $0', rid); const subsCollections = db.collections.get('subscriptions');
const message = await getMessage(rid, msg, tmid, user);
if (room) { if (!message) {
database.write(() => { return;
room.draftMessage = null;
});
} }
try { try {
const ret = await sendMessageCall.call(this, message); const room = await subsCollections.find(rid);
database.write(() => { await db.action(async() => {
database.create('messages', buildMessage({ ...message, ...ret }), true); await room.update((r) => {
r.draftMessage = null;
});
}); });
} catch (e) { } catch (e) {
database.write(() => { // Do nothing
message.status = messagesStatus.ERROR; }
database.create('messages', message, true);
try {
await sendMessageCall.call(this, message);
await db.action(async() => {
await message.update((m) => {
m.status = messagesStatus.SENT;
});
});
} catch (e) {
await db.action(async() => {
await message.update((m) => {
m.status = messagesStatus.ERROR;
});
}); });
} }
} catch (e) { } catch (e) {

View File

@ -1,15 +1,19 @@
import EJSON from 'ejson'; import EJSON from 'ejson';
import { sanitizedRaw } from '@nozbe/watermelondb/RawRecord';
import { InteractionManager } from 'react-native';
import log from '../../../utils/log'; import log from '../../../utils/log';
import protectedFunction from '../helpers/protectedFunction'; import protectedFunction from '../helpers/protectedFunction';
import buildMessage from '../helpers/buildMessage'; import buildMessage from '../helpers/buildMessage';
import database from '../../realm'; import database from '../../database';
import debounce from '../../../utils/debounce'; import reduxStore from '../../createStore';
import { addUserTyping, removeUserTyping, clearUserTyping } from '../../../actions/usersTyping';
const unsubscribe = subscriptions => subscriptions.forEach(sub => sub.unsubscribe().catch(() => console.log('unsubscribeRoom'))); const unsubscribe = subscriptions => subscriptions.forEach(sub => sub.unsubscribe().catch(() => console.log('unsubscribeRoom')));
const removeListener = listener => listener.stop(); const removeListener = listener => listener.stop();
export default function subscribeRoom({ rid }) { export default function subscribeRoom({ rid }) {
console.log(`[RCRN] Subscribed to room ${ rid }`);
let promises; let promises;
let connectedListener; let connectedListener;
let disconnectedListener; let disconnectedListener;
@ -18,52 +22,7 @@ export default function subscribeRoom({ rid }) {
const typingTimeouts = {}; const typingTimeouts = {};
const handleConnection = () => { const handleConnection = () => {
this.loadMissedMessages({ rid }); this.loadMissedMessages({ rid }).catch(e => console.log(e));
};
const getUserTyping = username => (
database
.memoryDatabase.objects('usersTyping')
.filtered('rid = $0 AND username = $1', rid, username)
);
const removeUserTyping = (username) => {
const userTyping = getUserTyping(username);
try {
database.memoryDatabase.write(() => {
database.memoryDatabase.delete(userTyping);
});
if (typingTimeouts[username]) {
clearTimeout(typingTimeouts[username]);
typingTimeouts[username] = null;
}
} catch (e) {
log(e);
}
};
const addUserTyping = (username) => {
const userTyping = getUserTyping(username);
// prevent duplicated
if (userTyping.length === 0) {
try {
database.memoryDatabase.write(() => {
database.memoryDatabase.create('usersTyping', { rid, username });
});
if (typingTimeouts[username]) {
clearTimeout(typingTimeouts[username]);
typingTimeouts[username] = null;
}
typingTimeouts[username] = setTimeout(() => {
removeUserTyping(username);
}, 10000);
} catch (e) {
log(e);
}
}
}; };
const handleNotifyRoomReceived = protectedFunction((ddpMessage) => { const handleNotifyRoomReceived = protectedFunction((ddpMessage) => {
@ -74,59 +33,147 @@ export default function subscribeRoom({ rid }) {
if (ev === 'typing') { if (ev === 'typing') {
const [username, typing] = ddpMessage.fields.args; const [username, typing] = ddpMessage.fields.args;
if (typing) { if (typing) {
addUserTyping(username); reduxStore.dispatch(addUserTyping(username));
} else { } else {
removeUserTyping(username); reduxStore.dispatch(removeUserTyping(username));
} }
} else if (ev === 'deleteMessage') { } else if (ev === 'deleteMessage') {
database.write(() => { InteractionManager.runAfterInteractions(async() => {
if (ddpMessage && ddpMessage.fields && ddpMessage.fields.args.length > 0) { if (ddpMessage && ddpMessage.fields && ddpMessage.fields.args.length > 0) {
try {
const { _id } = ddpMessage.fields.args[0]; const { _id } = ddpMessage.fields.args[0];
const message = database.objects('messages').filtered('_id = $0', _id); const db = database.active;
database.delete(message); const msgCollection = db.collections.get('messages');
const thread = database.objects('threads').filtered('_id = $0', _id); const threadsCollection = db.collections.get('threads');
database.delete(thread); const threadMessagesCollection = db.collections.get('thread_messages');
const threadMessage = database.objects('threadMessages').filtered('_id = $0', _id); let deleteMessage;
database.delete(threadMessage); let deleteThread;
const cleanTmids = database.objects('messages').filtered('tmid = $0', _id).snapshot(); let deleteThreadMessage;
if (cleanTmids && cleanTmids.length) {
cleanTmids.forEach((m) => {
m.tmid = null;
});
}
}
});
}
});
const read = debounce(() => { // Delete message
const [room] = database.objects('subscriptions').filtered('rid = $0', rid); try {
if (room && room._id) { const m = await msgCollection.find(_id);
this.readMessages(rid); deleteMessage = m.prepareDestroyPermanently();
} catch (e) {
// Do nothing
} }
}, 300);
// Delete thread
try {
const m = await threadsCollection.find(_id);
deleteThread = m.prepareDestroyPermanently();
} catch (e) {
// Do nothing
}
// Delete thread message
try {
const m = await threadMessagesCollection.find(_id);
deleteThreadMessage = m.prepareDestroyPermanently();
} catch (e) {
// Do nothing
}
await db.action(async() => {
await db.batch(
deleteMessage, deleteThread, deleteThreadMessage
);
});
} catch (e) {
log(e);
}
}
});
}
});
const handleMessageReceived = protectedFunction((ddpMessage) => { const handleMessageReceived = protectedFunction((ddpMessage) => {
const message = buildMessage(EJSON.fromJSONValue(ddpMessage.fields.args[0])); const message = buildMessage(EJSON.fromJSONValue(ddpMessage.fields.args[0]));
if (rid !== message.rid) { if (rid !== message.rid) {
return; return;
} }
requestAnimationFrame(() => { InteractionManager.runAfterInteractions(async() => {
try { const db = database.active;
database.write(() => { const batch = [];
database.create('messages', message, true); const subCollection = db.collections.get('subscriptions');
// if it's a thread "header" const msgCollection = db.collections.get('messages');
if (message.tlm) { const threadsCollection = db.collections.get('threads');
database.create('threads', message, true); const threadMessagesCollection = db.collections.get('thread_messages');
} else if (message.tmid) {
message.rid = message.tmid;
database.create('threadMessages', message, true);
}
});
read(); // Create or update message
try {
const messageRecord = await msgCollection.find(message._id);
batch.push(
messageRecord.prepareUpdate((m) => {
Object.assign(m, message);
})
);
} catch (error) {
batch.push(
msgCollection.prepareCreate(protectedFunction((m) => {
m._raw = sanitizedRaw({ id: message._id }, msgCollection.schema);
m.subscription.id = rid;
Object.assign(m, message);
}))
);
}
// Create or update thread
if (message.tlm) {
try {
const threadRecord = await threadsCollection.find(message._id);
batch.push(
threadRecord.prepareUpdate((t) => {
Object.assign(t, message);
})
);
} catch (error) {
batch.push(
threadsCollection.prepareCreate(protectedFunction((t) => {
t._raw = sanitizedRaw({ id: message._id }, threadsCollection.schema);
t.subscription.id = rid;
Object.assign(t, message);
}))
);
}
}
// Create or update thread message
if (message.tmid) {
try {
const threadMessageRecord = await threadMessagesCollection.find(message._id);
batch.push(
threadMessageRecord.prepareUpdate((tm) => {
Object.assign(tm, message);
tm.rid = message.tmid;
delete tm.tmid;
})
);
} catch (error) {
batch.push(
threadMessagesCollection.prepareCreate(protectedFunction((tm) => {
tm._raw = sanitizedRaw({ id: message._id }, threadMessagesCollection.schema);
Object.assign(tm, message);
tm.subscription.id = rid;
tm.rid = message.tmid;
delete tm.tmid;
}))
);
}
}
try {
await subCollection.find(rid);
this.readMessages(rid);
} catch (e) { } catch (e) {
console.warn('handleMessageReceived', e); console.log('Subscription not found. We probably subscribed to a not joined channel. No need to mark as read.');
}
try {
await db.action(async() => {
await db.batch(...batch);
});
} catch (e) {
log(e);
} }
}); });
}); });
@ -158,10 +205,7 @@ export default function subscribeRoom({ rid }) {
typingTimeouts[key] = null; typingTimeouts[key] = null;
} }
}); });
database.memoryDatabase.write(() => { reduxStore.dispatch(clearUserTyping());
const usersTyping = database.memoryDatabase.objects('usersTyping').filtered('rid == $0', rid);
database.memoryDatabase.delete(usersTyping);
});
}; };
connectedListener = this.sdk.onStreamData('connected', handleConnection); connectedListener = this.sdk.onStreamData('connected', handleConnection);

View File

@ -1,4 +1,6 @@
import database from '../../realm'; import { sanitizedRaw } from '@nozbe/watermelondb/RawRecord';
import database from '../../database';
import { merge } from '../helpers/mergeSubscriptionsRooms'; import { merge } from '../helpers/mergeSubscriptionsRooms';
import protectedFunction from '../helpers/protectedFunction'; import protectedFunction from '../helpers/protectedFunction';
import messagesStatus from '../../../constants/messagesStatus'; import messagesStatus from '../../../constants/messagesStatus';
@ -15,12 +17,112 @@ let disconnectedListener;
let streamListener; let streamListener;
let subServer; let subServer;
// TODO: batch execution
const createOrUpdateSubscription = async(subscription, room) => {
try {
const db = database.active;
const subCollection = db.collections.get('subscriptions');
const roomsCollection = db.collections.get('rooms');
if (!subscription) {
try {
const s = await subCollection.find(room._id);
// We have to create a plain obj so we can manipulate it on `merge`
// Can we do it in a better way?
subscription = {
_id: s._id,
f: s.f,
t: s.t,
ts: s.ts,
ls: s.ls,
name: s.name,
fname: s.fname,
rid: s.rid,
open: s.open,
alert: s.alert,
unread: s.unread,
userMentions: s.userMentions,
roomUpdatedAt: s.roomUpdatedAt,
ro: s.ro,
lastOpen: s.lastOpen,
description: s.description,
announcement: s.announcement,
topic: s.topic,
blocked: s.blocked,
blocker: s.blocker,
reactWhenReadOnly: s.reactWhenReadOnly,
archived: s.archived,
joinCodeRequired: s.joinCodeRequired,
muted: s.muted,
broadcast: s.broadcast,
prid: s.prid,
draftMessage: s.draftMessage,
lastThreadSync: s.lastThreadSync,
autoTranslate: s.autoTranslate,
autoTranslateLanguage: s.autoTranslateLanguage,
lastMessage: s.lastMessage
};
} catch (error) {
try {
await db.action(async() => {
await roomsCollection.create((r) => {
r._raw = sanitizedRaw({ id: room._id }, roomsCollection.schema);
Object.assign(r, room);
});
});
} catch (e) {
// Do nothing
}
return;
}
}
if (!room && subscription) {
try {
const r = await roomsCollection.find(subscription.rid);
// We have to create a plain obj so we can manipulate it on `merge`
// Can we do it in a better way?
room = {
customFields: r.customFields,
broadcast: r.broadcast,
encrypted: r.encrypted,
ro: r.ro
};
} catch (error) {
// Do nothing
}
}
const tmp = merge(subscription, room);
await db.action(async() => {
try {
const sub = await subCollection.find(tmp.rid);
await sub.update((s) => {
Object.assign(s, tmp);
});
} catch (error) {
await subCollection.create((s) => {
s._raw = sanitizedRaw({ id: tmp.rid }, subCollection.schema);
Object.assign(s, tmp);
if (s.roomUpdatedAt) {
s.roomUpdatedAt = new Date();
}
});
}
});
} catch (e) {
log(e);
}
};
export default function subscribeRooms() { export default function subscribeRooms() {
const handleConnection = () => { const handleConnection = () => {
store.dispatch(roomsRequest()); store.dispatch(roomsRequest());
}; };
const handleStreamMessageReceived = protectedFunction((ddpMessage) => { const handleStreamMessageReceived = protectedFunction(async(ddpMessage) => {
const db = database.active;
// check if the server from variable is the same as the js sdk client // check if the server from variable is the same as the js sdk client
if (this.sdk && this.sdk.client && this.sdk.client.host !== subServer) { if (this.sdk && this.sdk.client && this.sdk.client.host !== subServer) {
return; return;
@ -32,52 +134,33 @@ export default function subscribeRooms() {
const [, ev] = ddpMessage.fields.eventName.split('/'); const [, ev] = ddpMessage.fields.eventName.split('/');
if (/subscriptions/.test(ev)) { if (/subscriptions/.test(ev)) {
if (type === 'removed') { if (type === 'removed') {
let messages = [];
const [subscription] = database.objects('subscriptions').filtered('_id == $0', data._id);
if (subscription) {
messages = database.objects('messages').filtered('rid == $0', subscription.rid);
}
try { try {
database.write(() => { const subCollection = db.collections.get('subscriptions');
database.delete(messages); const sub = await subCollection.find(data.rid);
database.delete(subscription); const messages = await sub.messages.fetch();
const threads = await sub.threads.fetch();
const threadMessages = await sub.threadMessages.fetch();
const messagesToDelete = messages.map(m => m.prepareDestroyPermanently());
const threadsToDelete = threads.map(m => m.prepareDestroyPermanently());
const threadMessagesToDelete = threadMessages.map(m => m.prepareDestroyPermanently());
await db.action(async() => {
await db.batch(
sub.prepareDestroyPermanently(),
...messagesToDelete,
...threadsToDelete,
...threadMessagesToDelete,
);
}); });
} catch (e) { } catch (e) {
log(e); log(e);
} }
} else { } else {
const rooms = database.objects('rooms').filtered('_id == $0', data.rid); await createOrUpdateSubscription(data);
const tpm = merge(data, rooms[0]);
try {
database.write(() => {
database.create('subscriptions', tpm, true);
database.delete(rooms);
});
} catch (e) {
log(e);
}
} }
} }
if (/rooms/.test(ev)) { if (/rooms/.test(ev)) {
if (type === 'updated') { if (type === 'updated' || type === 'inserted') {
const [sub] = database.objects('subscriptions').filtered('rid == $0', data._id); await createOrUpdateSubscription(null, data);
try {
database.write(() => {
const tmp = merge(sub, data);
database.create('subscriptions', tmp, true);
});
} catch (e) {
log(e);
}
} else if (type === 'inserted') {
try {
database.write(() => {
database.create('rooms', data, true);
});
} catch (e) {
log(e);
}
} }
} }
if (/message/.test(ev)) { if (/message/.test(ev)) {
@ -95,15 +178,18 @@ export default function subscribeRooms() {
username: 'rocket.cat' username: 'rocket.cat'
} }
}; };
requestAnimationFrame(() => {
try { try {
database.write(() => { const msgCollection = db.collections.get('messages');
database.create('messages', message, true); await db.action(async() => {
await msgCollection.create(protectedFunction((m) => {
m._raw = sanitizedRaw({ id: message._id }, msgCollection.schema);
m.subscription.id = args.rid;
Object.assign(m, message);
}));
}); });
} catch (e) { } catch (e) {
log(e); log(e);
} }
});
} }
if (/notification/.test(ev)) { if (/notification/.test(ev)) {
const [notification] = ddpMessage.fields.args; const [notification] = ddpMessage.fields.args;

View File

@ -0,0 +1,131 @@
import { sanitizedRaw } from '@nozbe/watermelondb/RawRecord';
import { Q } from '@nozbe/watermelondb';
import buildMessage from './helpers/buildMessage';
import log from '../../utils/log';
import database from '../database';
import protectedFunction from './helpers/protectedFunction';
export default function updateMessages({ rid, update, remove }) {
try {
if (!((update && update.length) || (remove && remove.length))) {
return;
}
const db = database.active;
return db.action(async() => {
const subCollection = db.collections.get('subscriptions');
let sub;
try {
sub = await subCollection.find(rid);
} catch (error) {
sub = { id: rid };
console.log('updateMessages: subscription not found');
}
const messagesIds = update.map(m => m._id);
const msgCollection = db.collections.get('messages');
const threadCollection = db.collections.get('threads');
const threadMessagesCollection = db.collections.get('thread_messages');
const allMessagesRecords = await msgCollection
.query(Q.where('rid', rid), Q.where('id', Q.oneOf(messagesIds)))
.fetch();
const allThreadsRecords = await threadCollection
.query(Q.where('rid', rid), Q.where('id', Q.oneOf(messagesIds)))
.fetch();
const allThreadMessagesRecords = await threadMessagesCollection
.query(Q.where('subscription_id', rid), Q.where('id', Q.oneOf(messagesIds)))
.fetch();
update = update.map(m => buildMessage(m));
// filter messages
let msgsToCreate = update.filter(i1 => !allMessagesRecords.find(i2 => i1._id === i2.id));
let msgsToUpdate = allMessagesRecords.filter(i1 => update.find(i2 => i1.id === i2._id));
// filter threads
const allThreads = update.filter(m => m.tlm);
let threadsToCreate = allThreads.filter(i1 => !allThreadsRecords.find(i2 => i1._id === i2.id));
let threadsToUpdate = allThreadsRecords.filter(i1 => allThreads.find(i2 => i1.id === i2._id));
// filter thread messages
const allThreadMessages = update.filter(m => m.tmid);
let threadMessagesToCreate = allThreadMessages.filter(i1 => !allThreadMessagesRecords.find(i2 => i1._id === i2.id));
let threadMessagesToUpdate = allThreadMessagesRecords.filter(i1 => allThreadMessages.find(i2 => i1.id === i2._id));
// Create
msgsToCreate = msgsToCreate.map(message => msgCollection.prepareCreate(protectedFunction((m) => {
m._raw = sanitizedRaw({ id: message._id }, msgCollection.schema);
m.subscription.id = sub.id;
Object.assign(m, message);
})));
threadsToCreate = threadsToCreate.map(thread => threadCollection.prepareCreate(protectedFunction((t) => {
t._raw = sanitizedRaw({ id: thread._id }, threadCollection.schema);
t.subscription.id = sub.id;
Object.assign(t, thread);
})));
threadMessagesToCreate = threadMessagesToCreate.map(threadMessage => threadMessagesCollection.prepareCreate(protectedFunction((tm) => {
tm._raw = sanitizedRaw({ id: threadMessage._id }, threadMessagesCollection.schema);
Object.assign(tm, threadMessage);
tm.subscription.id = sub.id;
tm.rid = threadMessage.tmid;
delete threadMessage.tmid;
})));
// Update
msgsToUpdate = msgsToUpdate.map((message) => {
const newMessage = update.find(m => m._id === message.id);
return message.prepareUpdate(protectedFunction((m) => {
Object.assign(m, newMessage);
}));
});
threadsToUpdate = threadsToUpdate.map((thread) => {
const newThread = allThreads.find(t => t._id === thread.id);
return thread.prepareUpdate(protectedFunction((t) => {
Object.assign(t, newThread);
}));
});
threadMessagesToUpdate = threadMessagesToUpdate.map((threadMessage) => {
const newThreadMessage = allThreadMessages.find(t => t._id === threadMessage.id);
return threadMessage.prepareUpdate(protectedFunction((tm) => {
Object.assign(tm, newThreadMessage);
tm.rid = threadMessage.tmid;
delete threadMessage.tmid;
}));
});
// Delete
let msgsToDelete = [];
let threadsToDelete = [];
let threadMessagesToDelete = [];
if (remove && remove.length) {
msgsToDelete = allMessagesRecords.filter(i1 => remove.find(i2 => i1.id === i2._id));
msgsToDelete = msgsToDelete.map(m => m.prepareDestroyPermanently());
threadsToDelete = allThreadsRecords.filter(i1 => remove.find(i2 => i1.id === i2._id));
threadsToDelete = threadsToDelete.map(t => t.prepareDestroyPermanently());
threadMessagesToDelete = allThreadMessagesRecords.filter(i1 => remove.find(i2 => i1.id === i2._id));
threadMessagesToDelete = threadMessagesToDelete.map(tm => tm.prepareDestroyPermanently());
}
const allRecords = [
...msgsToCreate,
...msgsToUpdate,
...msgsToDelete,
...threadsToCreate,
...threadsToUpdate,
...threadsToDelete,
...threadMessagesToCreate,
...threadMessagesToUpdate,
...threadMessagesToDelete
];
try {
await db.batch(...allRecords);
} catch (e) {
log(e);
}
return allRecords.length;
});
} catch (e) {
log(e);
}
}

View File

@ -1,526 +0,0 @@
import Realm from 'realm';
import RNRealmPath from 'react-native-realm-path';
// import { AsyncStorage } from 'react-native';
// Realm.clearTestState();
// AsyncStorage.clear();
const userSchema = {
name: 'user',
primaryKey: 'id',
properties: {
id: 'string',
token: { type: 'string', optional: true },
username: { type: 'string', optional: true },
name: { type: 'string', optional: true },
language: { type: 'string', optional: true },
status: { type: 'string', optional: true },
roles: { type: 'string[]', optional: true }
}
};
const serversSchema = {
name: 'servers',
primaryKey: 'id',
properties: {
id: 'string',
name: { type: 'string', optional: true },
iconURL: { type: 'string', optional: true },
useRealName: { type: 'bool', optional: true },
FileUpload_MediaTypeWhiteList: { type: 'string', optional: true },
FileUpload_MaxFileSize: { type: 'int', optional: true },
roomsUpdatedAt: { type: 'date', optional: true },
version: 'string?'
}
};
const settingsSchema = {
name: 'settings',
primaryKey: '_id',
properties: {
_id: 'string',
valueAsString: { type: 'string', optional: true },
valueAsBoolean: { type: 'bool', optional: true },
valueAsNumber: { type: 'int', optional: true },
_updatedAt: { type: 'date', optional: true }
}
};
const permissionsSchema = {
name: 'permissions',
primaryKey: '_id',
properties: {
_id: 'string',
roles: 'string[]',
_updatedAt: { type: 'date', optional: true }
}
};
const roomsSchema = {
name: 'rooms',
primaryKey: '_id',
properties: {
_id: 'string',
name: 'string?',
broadcast: { type: 'bool', optional: true }
}
};
const subscriptionSchema = {
name: 'subscriptions',
primaryKey: '_id',
properties: {
_id: 'string',
f: { type: 'bool', optional: true },
t: 'string',
ts: { type: 'date', optional: true },
ls: { type: 'date', optional: true },
name: { type: 'string', indexed: true },
fname: { type: 'string', optional: true },
rid: { type: 'string', indexed: true },
open: { type: 'bool', optional: true },
alert: { type: 'bool', optional: true },
roles: 'string[]',
unread: { type: 'int', optional: true },
userMentions: { type: 'int', optional: true },
roomUpdatedAt: { type: 'date', optional: true },
ro: { type: 'bool', optional: true },
lastOpen: { type: 'date', optional: true },
lastMessage: { type: 'messages', optional: true },
description: { type: 'string', optional: true },
announcement: { type: 'string', optional: true },
topic: { type: 'string', optional: true },
blocked: { type: 'bool', optional: true },
blocker: { type: 'bool', optional: true },
reactWhenReadOnly: { type: 'bool', optional: true },
archived: { type: 'bool', optional: true },
joinCodeRequired: { type: 'bool', optional: true },
muted: 'string[]',
broadcast: { type: 'bool', optional: true },
prid: { type: 'string', optional: true },
draftMessage: { type: 'string', optional: true },
lastThreadSync: 'date?',
autoTranslate: 'bool?',
autoTranslateLanguage: 'string?',
// Notifications
emailNotifications: { type: 'string', default: 'default' },
disableNotifications: { type: 'bool', default: false },
muteGroupMentions: { type: 'bool', default: false },
hideUnreadStatus: { type: 'bool', default: false },
audioNotifications: { type: 'string', default: 'default' },
desktopNotifications: { type: 'string', default: 'default' },
audioNotificationValue: { type: 'string', default: '0 Default' },
desktopNotificationDuration: { type: 'int', default: 0 },
mobilePushNotifications: { type: 'string', default: 'default' }
}
};
const usersSchema = {
name: 'users',
primaryKey: '_id',
properties: {
_id: 'string',
username: 'string',
name: { type: 'string', optional: true }
}
};
const attachmentFields = {
name: 'attachmentFields',
properties: {
title: { type: 'string', optional: true },
value: { type: 'string', optional: true },
short: { type: 'bool', optional: true }
}
};
const attachment = {
name: 'attachment',
properties: {
description: { type: 'string', optional: true },
image_size: { type: 'int', optional: true },
image_type: { type: 'string', optional: true },
image_url: { type: 'string', optional: true },
audio_size: { type: 'int', optional: true },
audio_type: { type: 'string', optional: true },
audio_url: { type: 'string', optional: true },
video_size: { type: 'int', optional: true },
video_type: { type: 'string', optional: true },
video_url: { type: 'string', optional: true },
title: { type: 'string', optional: true },
title_link: { type: 'string', optional: true },
// title_link_download: { type: 'bool', optional: true },
type: { type: 'string', optional: true },
author_icon: { type: 'string', optional: true },
author_name: { type: 'string', optional: true },
author_link: { type: 'string', optional: true },
text: { type: 'string', optional: true },
color: { type: 'string', optional: true },
ts: { type: 'date', optional: true },
attachments: { type: 'list', objectType: 'attachment' },
fields: {
type: 'list', objectType: 'attachmentFields', default: []
}
}
};
const url = {
name: 'url',
primaryKey: 'url',
properties: {
// _id: { type: 'int', optional: true },
url: { type: 'string', optional: true },
title: { type: 'string', optional: true },
description: { type: 'string', optional: true },
image: { type: 'string', optional: true }
}
};
const messagesReactionsSchema = {
name: 'messagesReactions',
primaryKey: '_id',
properties: {
_id: 'string',
emoji: 'string',
usernames: 'string[]'
}
};
const messagesTranslationsSchema = {
name: 'messagesTranslations',
primaryKey: '_id',
properties: {
_id: 'string',
language: 'string',
value: 'string'
}
};
const messagesEditedBySchema = {
name: 'messagesEditedBy',
primaryKey: '_id',
properties: {
_id: { type: 'string', optional: true },
username: { type: 'string', optional: true }
}
};
const messagesSchema = {
name: 'messages',
primaryKey: '_id',
properties: {
_id: 'string',
msg: { type: 'string', optional: true },
t: { type: 'string', optional: true },
rid: { type: 'string', indexed: true },
ts: 'date',
u: 'users',
alias: { type: 'string', optional: true },
parseUrls: { type: 'bool', optional: true },
groupable: { type: 'bool', optional: true },
avatar: { type: 'string', optional: true },
attachments: { type: 'list', objectType: 'attachment' },
urls: { type: 'list', objectType: 'url', default: [] },
_updatedAt: { type: 'date', optional: true },
status: { type: 'int', optional: true },
pinned: { type: 'bool', optional: true },
starred: { type: 'bool', optional: true },
editedBy: 'messagesEditedBy',
reactions: { type: 'list', objectType: 'messagesReactions' },
role: { type: 'string', optional: true },
drid: { type: 'string', optional: true },
dcount: { type: 'int', optional: true },
dlm: { type: 'date', optional: true },
tmid: { type: 'string', optional: true },
tcount: { type: 'int', optional: true },
tlm: { type: 'date', optional: true },
replies: 'string[]',
mentions: { type: 'list', objectType: 'users' },
channels: { type: 'list', objectType: 'rooms' },
unread: { type: 'bool', optional: true },
autoTranslate: { type: 'bool', default: false },
translations: { type: 'list', objectType: 'messagesTranslations' }
}
};
const threadsSchema = {
name: 'threads',
primaryKey: '_id',
properties: {
_id: 'string',
msg: { type: 'string', optional: true },
t: { type: 'string', optional: true },
rid: { type: 'string', indexed: true },
ts: 'date',
u: 'users',
alias: { type: 'string', optional: true },
parseUrls: { type: 'bool', optional: true },
groupable: { type: 'bool', optional: true },
avatar: { type: 'string', optional: true },
attachments: { type: 'list', objectType: 'attachment' },
urls: { type: 'list', objectType: 'url', default: [] },
_updatedAt: { type: 'date', optional: true },
status: { type: 'int', optional: true },
pinned: { type: 'bool', optional: true },
starred: { type: 'bool', optional: true },
editedBy: 'messagesEditedBy',
reactions: { type: 'list', objectType: 'messagesReactions' },
role: { type: 'string', optional: true },
drid: { type: 'string', optional: true },
dcount: { type: 'int', optional: true },
dlm: { type: 'date', optional: true },
tmid: { type: 'string', optional: true },
tcount: { type: 'int', optional: true },
tlm: { type: 'date', optional: true },
replies: 'string[]',
mentions: { type: 'list', objectType: 'users' },
channels: { type: 'list', objectType: 'rooms' },
unread: { type: 'bool', optional: true },
autoTranslate: { type: 'bool', default: false },
translations: { type: 'list', objectType: 'messagesTranslations' },
draftMessage: 'string?'
}
};
const threadMessagesSchema = {
name: 'threadMessages',
primaryKey: '_id',
properties: {
_id: 'string',
msg: { type: 'string', optional: true },
t: { type: 'string', optional: true },
rid: { type: 'string', indexed: true },
ts: 'date',
u: 'users',
alias: { type: 'string', optional: true },
parseUrls: { type: 'bool', optional: true },
groupable: { type: 'bool', optional: true },
avatar: { type: 'string', optional: true },
attachments: { type: 'list', objectType: 'attachment' },
urls: { type: 'list', objectType: 'url', default: [] },
_updatedAt: { type: 'date', optional: true },
status: { type: 'int', optional: true },
pinned: { type: 'bool', optional: true },
starred: { type: 'bool', optional: true },
editedBy: 'messagesEditedBy',
reactions: { type: 'list', objectType: 'messagesReactions' },
role: { type: 'string', optional: true },
replies: 'string[]',
mentions: { type: 'list', objectType: 'users' },
channels: { type: 'list', objectType: 'rooms' },
unread: { type: 'bool', optional: true },
autoTranslate: { type: 'bool', default: false },
translations: { type: 'list', objectType: 'messagesTranslations' }
}
};
const frequentlyUsedEmojiSchema = {
name: 'frequentlyUsedEmoji',
primaryKey: 'content',
properties: {
content: { type: 'string', optional: true },
extension: { type: 'string', optional: true },
isCustom: 'bool',
count: 'int'
}
};
const slashCommandSchema = {
name: 'slashCommand',
primaryKey: 'command',
properties: {
command: 'string',
params: { type: 'string', optional: true },
description: { type: 'string', optional: true },
clientOnly: { type: 'bool', optional: true },
providesPreview: { type: 'bool', optional: true }
}
};
const customEmojisSchema = {
name: 'customEmojis',
primaryKey: '_id',
properties: {
_id: 'string',
name: 'string',
aliases: 'string[]',
extension: 'string',
_updatedAt: { type: 'date', optional: true }
}
};
const rolesSchema = {
name: 'roles',
primaryKey: '_id',
properties: {
_id: 'string',
description: { type: 'string', optional: true }
}
};
const uploadsSchema = {
name: 'uploads',
primaryKey: 'path',
properties: {
path: 'string',
rid: 'string',
name: { type: 'string', optional: true },
description: { type: 'string', optional: true },
size: { type: 'int', optional: true },
type: { type: 'string', optional: true },
store: { type: 'string', optional: true },
progress: { type: 'int', default: 1 },
error: { type: 'bool', default: false }
}
};
const usersTypingSchema = {
name: 'usersTyping',
properties: {
rid: { type: 'string', indexed: true },
username: { type: 'string', optional: true }
}
};
const activeUsersSchema = {
name: 'activeUsers',
primaryKey: 'id',
properties: {
id: 'string',
name: 'string?',
username: 'string?',
status: 'string?',
utcOffset: 'double?'
}
};
const schema = [
settingsSchema,
subscriptionSchema,
messagesSchema,
threadsSchema,
threadMessagesSchema,
usersSchema,
roomsSchema,
attachment,
attachmentFields,
messagesEditedBySchema,
permissionsSchema,
url,
frequentlyUsedEmojiSchema,
customEmojisSchema,
messagesReactionsSchema,
rolesSchema,
uploadsSchema,
slashCommandSchema,
messagesTranslationsSchema
];
const inMemorySchema = [usersTypingSchema, activeUsersSchema];
class DB {
databases = {
serversDB: new Realm({
path: `${ RNRealmPath.realmPath }default.realm`,
schema: [
userSchema,
serversSchema
],
schemaVersion: 10,
migration: (oldRealm, newRealm) => {
if (oldRealm.schemaVersion >= 1 && newRealm.schemaVersion <= 9) {
const newServers = newRealm.objects('servers');
// eslint-disable-next-line no-plusplus
for (let i = 0; i < newServers.length; i++) {
newServers[i].roomsUpdatedAt = null;
}
}
}
}),
inMemoryDB: new Realm({
path: `${ RNRealmPath.realmPath }memory.realm`,
schema: inMemorySchema,
schemaVersion: 2,
inMemory: true
})
}
deleteAll(...args) {
return this.database.write(() => this.database.deleteAll(...args));
}
delete(...args) {
return this.database.delete(...args);
}
write(...args) {
return this.database.write(...args);
}
create(...args) {
return this.database.create(...args);
}
objects(...args) {
return this.database.objects(...args);
}
objectForPrimaryKey(...args) {
return this.database.objectForPrimaryKey(...args);
}
get database() {
return this.databases.activeDB;
}
get memoryDatabase() {
return this.databases.inMemoryDB;
}
setActiveDB(database = '') {
const path = database.replace(/(^\w+:|^)\/\//, '');
return this.databases.activeDB = new Realm({
path: `${ RNRealmPath.realmPath }${ path }.realm`,
schema,
schemaVersion: 14,
migration: (oldRealm, newRealm) => {
if (oldRealm.schemaVersion >= 3 && newRealm.schemaVersion <= 13) {
const newSubs = newRealm.objects('subscriptions');
newRealm.delete(newSubs);
const newMessages = newRealm.objects('messages');
newRealm.delete(newMessages);
const newThreads = newRealm.objects('threads');
newRealm.delete(newThreads);
const newThreadMessages = newRealm.objects('threadMessages');
newRealm.delete(newThreadMessages);
}
if (newRealm.schemaVersion === 9) {
const newEmojis = newRealm.objects('customEmojis');
newRealm.delete(newEmojis);
const newSettings = newRealm.objects('settings');
newRealm.delete(newSettings);
}
}
});
}
}
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);
}
}

View File

@ -2,12 +2,13 @@ import { AsyncStorage, InteractionManager } from 'react-native';
import semver from 'semver'; import semver from 'semver';
import { Rocketchat as RocketchatClient } from '@rocket.chat/sdk'; import { Rocketchat as RocketchatClient } from '@rocket.chat/sdk';
import RNUserDefaults from 'rn-user-defaults'; import RNUserDefaults from 'rn-user-defaults';
import { Q } from '@nozbe/watermelondb';
import * as FileSystem from 'expo-file-system'; import * as FileSystem from 'expo-file-system';
import reduxStore from './createStore'; import reduxStore from './createStore';
import defaultSettings from '../constants/settings'; import defaultSettings from '../constants/settings';
import messagesStatus from '../constants/messagesStatus'; import messagesStatus from '../constants/messagesStatus';
import database from './realm'; import database from './database';
import log from '../utils/log'; import log from '../utils/log';
import { isIOS, getBundleId } from '../utils/deviceInfo'; import { isIOS, getBundleId } from '../utils/deviceInfo';
import { extractHostname } from '../utils/server'; import { extractHostname } from '../utils/server';
@ -29,7 +30,7 @@ import getSettings from './methods/getSettings';
import getRooms from './methods/getRooms'; import getRooms from './methods/getRooms';
import getPermissions from './methods/getPermissions'; import getPermissions from './methods/getPermissions';
import getCustomEmojis from './methods/getCustomEmojis'; import { getCustomEmojis, setCustomEmojis } from './methods/getCustomEmojis';
import getSlashCommands from './methods/getSlashCommands'; import getSlashCommands from './methods/getSlashCommands';
import getRoles from './methods/getRoles'; import getRoles from './methods/getRoles';
import canOpenRoom from './methods/canOpenRoom'; import canOpenRoom from './methods/canOpenRoom';
@ -43,6 +44,7 @@ import { sendFileMessage, cancelUpload, isUploadActive } from './methods/sendFil
import { getDeviceToken } from '../notifications/push'; import { getDeviceToken } from '../notifications/push';
import { SERVERS, SERVER_URL } from '../constants/userDefaults'; import { SERVERS, SERVER_URL } from '../constants/userDefaults';
import { setActiveUsers } from '../actions/activeUsers';
const TOKEN_KEY = 'reactnativemeteor_usertoken'; const TOKEN_KEY = 'reactnativemeteor_usertoken';
const SORT_PREFS_KEY = 'RC_SORT_PREFS_KEY'; const SORT_PREFS_KEY = 'RC_SORT_PREFS_KEY';
@ -59,7 +61,11 @@ const RocketChat = {
if (this.roomsSub) { if (this.roomsSub) {
this.roomsSub.stop(); this.roomsSub.stop();
} }
try {
this.roomsSub = await subscribeRooms.call(this); this.roomsSub = await subscribeRooms.call(this);
} catch (e) {
log(e);
}
}, },
subscribeRoom, subscribeRoom,
canOpenRoom, canOpenRoom,
@ -100,67 +106,36 @@ const RocketChat = {
message: 'The_URL_is_invalid' message: 'The_URL_is_invalid'
}; };
}, },
_setUser(ddpMessage) { stopListener(listener) {
this.activeUsers = this.activeUsers || {}; return listener && listener.stop();
const { user } = reduxStore.getState().login;
if (ddpMessage.fields && user && user.id === ddpMessage.id) {
reduxStore.dispatch(setUser(ddpMessage.fields));
}
if (ddpMessage.cleared && user && user.id === ddpMessage.id) {
reduxStore.dispatch(setUser({ status: 'offline' }));
}
if (!this._setUserTimer) {
this._setUserTimer = setTimeout(() => {
const batchUsers = this.activeUsers;
InteractionManager.runAfterInteractions(() => {
database.memoryDatabase.write(() => {
Object.keys(batchUsers).forEach((key) => {
if (batchUsers[key] && batchUsers[key].id) {
try {
const data = batchUsers[key];
if (data.removed) {
const userRecord = database.memoryDatabase.objectForPrimaryKey('activeUsers', data.id);
if (userRecord) {
userRecord.status = 'offline';
}
} else {
database.memoryDatabase.create('activeUsers', data, true);
}
} catch (error) {
console.log(error);
}
}
});
});
});
this._setUserTimer = null;
return this.activeUsers = {};
}, 10000);
}
if (!ddpMessage.fields) {
this.activeUsers[ddpMessage.id] = {
id: ddpMessage.id,
removed: true
};
} else {
this.activeUsers[ddpMessage.id] = {
id: ddpMessage.id, ...this.activeUsers[ddpMessage.id], ...ddpMessage.fields
};
}
}, },
connect({ server, user }) { connect({ server, user }) {
return new Promise((resolve) => { return new Promise((resolve) => {
if (!this.sdk || this.sdk.client.host !== server) {
database.setActiveDB(server); database.setActiveDB(server);
}
reduxStore.dispatch(connectRequest()); reduxStore.dispatch(connectRequest());
if (this.connectTimeout) { if (this.connectTimeout) {
clearTimeout(this.connectTimeout); clearTimeout(this.connectTimeout);
} }
if (this.connectedListener) {
this.connectedListener.then(this.stopListener);
}
if (this.closeListener) {
this.closeListener.then(this.stopListener);
}
if (this.usersListener) {
this.usersListener.then(this.stopListener);
}
if (this.notifyLoggedListener) {
this.notifyLoggedListener.then(this.stopListener);
}
if (this.roomsSub) { if (this.roomsSub) {
this.roomsSub.stop(); this.roomsSub.stop();
} }
@ -191,35 +166,37 @@ const RocketChat = {
}, 10000); }, 10000);
}); });
this.sdk.onStreamData('connected', () => { this.connectedListener = this.sdk.onStreamData('connected', () => {
reduxStore.dispatch(connectSuccess()); reduxStore.dispatch(connectSuccess());
// const { isAuthenticated } = reduxStore.getState().login;
// if (isAuthenticated) {
// this.getUserPresence();
// }
}); });
this.sdk.onStreamData('close', () => { this.closeListener = this.sdk.onStreamData('close', () => {
reduxStore.dispatch(disconnect()); reduxStore.dispatch(disconnect());
}); });
this.sdk.onStreamData('users', protectedFunction(ddpMessage => RocketChat._setUser(ddpMessage))); this.usersListener = this.sdk.onStreamData('users', protectedFunction(ddpMessage => RocketChat._setUser(ddpMessage)));
this.sdk.onStreamData('stream-notify-logged', protectedFunction((ddpMessage) => { this.notifyLoggedListener = this.sdk.onStreamData('stream-notify-logged', protectedFunction((ddpMessage) => {
const { eventName } = ddpMessage.fields; const { eventName } = ddpMessage.fields;
if (eventName === 'user-status') { if (eventName === 'user-status') {
const userStatus = ddpMessage.fields.args[0]; this.activeUsers = this.activeUsers || {};
const [id, username, status] = userStatus; if (!this._setUserTimer) {
if (username) { this._setUserTimer = setTimeout(() => {
database.memoryDatabase.write(() => { const activeUsersBatch = this.activeUsers;
try { InteractionManager.runAfterInteractions(() => {
database.memoryDatabase.create('activeUsers', { reduxStore.dispatch(setActiveUsers(activeUsersBatch));
id, username, status: STATUSES[status]
}, true);
} catch (error) {
console.log(error);
}
}); });
this._setUserTimer = null;
return this.activeUsers = {};
}, 10000);
}
const userStatus = ddpMessage.fields.args[0];
const [id,, status] = userStatus;
this.activeUsers[id] = STATUSES[status];
const { user: loggedUser } = reduxStore.getState().login;
if (loggedUser && loggedUser.id === id) {
reduxStore.dispatch(setUser({ status: STATUSES[status] }));
} }
} }
})); }));
@ -242,19 +219,31 @@ const RocketChat = {
this.sdk = new RocketchatClient({ host: server, protocol: 'ddp', useSsl }); this.sdk = new RocketchatClient({ host: server, protocol: 'ddp', useSsl });
// set Server // set Server
const { serversDB } = database.databases; const serversDB = database.servers;
reduxStore.dispatch(shareSelectServer(server)); reduxStore.dispatch(shareSelectServer(server));
// set User info // set User info
try {
const userId = await RNUserDefaults.get(`${ RocketChat.TOKEN_KEY }-${ server }`); const userId = await RNUserDefaults.get(`${ RocketChat.TOKEN_KEY }-${ server }`);
const user = userId && serversDB.objectForPrimaryKey('user', userId); const userCollections = serversDB.collections.get('users');
let user = null;
if (userId) {
user = await userCollections.find(userId);
user = {
id: user.id,
token: user.token,
username: user.username
};
}
reduxStore.dispatch(shareSetUser({ reduxStore.dispatch(shareSetUser({
id: user.id, id: user.id,
token: user.token, token: user.token,
username: user.username username: user.username
})); }));
await RocketChat.login({ resume: user.token }); await RocketChat.login({ resume: user.token });
} catch (e) {
log(e);
}
}, },
register(credentials) { register(credentials) {
@ -376,23 +365,26 @@ const RocketChat = {
console.log('logout_rn_user_defaults', error); console.log('logout_rn_user_defaults', error);
} }
const { serversDB } = database.databases;
const userId = await RNUserDefaults.get(`${ TOKEN_KEY }-${ server }`); const userId = await RNUserDefaults.get(`${ TOKEN_KEY }-${ server }`);
serversDB.write(() => { try {
const user = serversDB.objectForPrimaryKey('user', userId); const serversDB = database.servers;
serversDB.delete(user); await serversDB.action(async() => {
const usersCollection = serversDB.collections.get('users');
const user = await usersCollection.find(userId);
await user.destroyPermanently();
}); });
} catch (error) {
// Do nothing
}
Promise.all([ await RNUserDefaults.clear('currentServer');
RNUserDefaults.clear('currentServer'), await RNUserDefaults.clear(TOKEN_KEY);
RNUserDefaults.clear(TOKEN_KEY), await RNUserDefaults.clear(`${ TOKEN_KEY }-${ server }`);
RNUserDefaults.clear(`${ TOKEN_KEY }-${ server }`)
]).catch(error => console.log(error));
try { try {
database.deleteAll(); const db = database.active;
await db.action(() => db.unsafeResetDatabase());
} catch (error) { } catch (error) {
console.log(error); console.log(error);
} }
@ -432,19 +424,21 @@ const RocketChat = {
sendMessage, sendMessage,
getRooms, getRooms,
readMessages, readMessages,
async resendMessage(messageId) { async resendMessage(message) {
const message = await database.objects('messages').filtered('_id = $0', messageId)[0]; const db = database.active;
try { try {
database.write(() => { await db.action(async() => {
message.status = messagesStatus.TEMP; await message.update((m) => {
database.create('messages', message, true); m.status = messagesStatus.TEMP;
}); });
await sendMessageCall.call(this, JSON.parse(JSON.stringify(message))); });
await sendMessageCall.call(this, message);
} catch (error) { } catch (error) {
try { try {
database.write(() => { await db.action(async() => {
message.status = messagesStatus.ERROR; await message.update((m) => {
database.create('messages', message, true); m.status = messagesStatus.ERROR;
});
}); });
} catch (e) { } catch (e) {
log(e); log(e);
@ -464,12 +458,15 @@ const RocketChat = {
return []; return [];
} }
let data = database.objects('subscriptions').filtered('name CONTAINS[c] $0', searchText); const db = database.active;
let data = await db.collections.get('subscriptions').query(
Q.where('name', Q.like(`%${ Q.sanitizeLikeString(searchText) }%`))
).fetch();
if (filterUsers && !filterRooms) { if (filterUsers && !filterRooms) {
data = data.filtered('t = $0', 'd'); data = data.filter(item => item.t === 'd');
} else if (!filterUsers && filterRooms) { } else if (!filterUsers && filterRooms) {
data = data.filtered('t != $0', 'd'); data = data.filter(item => item.t !== 'd');
} }
data = data.slice(0, 7); data = data.slice(0, 7);
@ -525,6 +522,7 @@ const RocketChat = {
getSettings, getSettings,
getPermissions, getPermissions,
getCustomEmojis, getCustomEmojis,
setCustomEmojis,
getSlashCommands, getSlashCommands,
getRoles, getRoles,
parseSettings: settings => settings.reduce((ret, item) => { parseSettings: settings => settings.reduce((ret, item) => {
@ -537,46 +535,47 @@ const RocketChat = {
return setting; return setting;
}); });
}, },
deleteMessage(message) { deleteMessage(messageId, rid) {
const { _id, rid } = message;
// RC 0.48.0 // RC 0.48.0
return this.sdk.post('chat.delete', { roomId: rid, msgId: _id }); return this.sdk.post('chat.delete', { msgId: messageId, roomId: rid });
}, },
editMessage(message) { editMessage(message) {
const { _id, msg, rid } = message; const { id, msg, rid } = message;
// RC 0.49.0 // RC 0.49.0
return this.sdk.post('chat.update', { roomId: rid, msgId: _id, text: msg }); return this.sdk.post('chat.update', { roomId: rid, msgId: id, text: msg });
}, },
toggleStarMessage(message) { toggleStarMessage(messageId, starred) {
if (message.starred) { if (starred) {
// RC 0.59.0 // RC 0.59.0
return this.sdk.post('chat.unStarMessage', { messageId: message._id }); return this.sdk.post('chat.unStarMessage', { messageId });
} }
// RC 0.59.0 // RC 0.59.0
return this.sdk.post('chat.starMessage', { messageId: message._id }); return this.sdk.post('chat.starMessage', { messageId });
}, },
togglePinMessage(message) { togglePinMessage(messageId, pinned) {
if (message.pinned) { if (pinned) {
// RC 0.59.0 // RC 0.59.0
return this.sdk.post('chat.unPinMessage', { messageId: message._id }); return this.sdk.post('chat.unPinMessage', { messageId });
} }
// RC 0.59.0 // RC 0.59.0
return this.sdk.post('chat.pinMessage', { messageId: message._id }); return this.sdk.post('chat.pinMessage', { messageId });
}, },
reportMessage(messageId) { reportMessage(messageId) {
return this.sdk.post('chat.reportMessage', { messageId, description: 'Message reported by user' }); return this.sdk.post('chat.reportMessage', { messageId, description: 'Message reported by user' });
}, },
getRoom(rid) { async getRoom(rid) {
const [result] = database.objects('subscriptions').filtered('rid = $0', rid); try {
if (!result) { const db = database.active;
const room = await db.collections.get('subscriptions').find(rid);
return Promise.resolve(room);
} catch (error) {
return Promise.reject(new Error('Room not found')); return Promise.reject(new Error('Room not found'));
} }
return Promise.resolve(result);
}, },
async getPermalinkMessage(message) { async getPermalinkMessage(message) {
let room; let room;
try { try {
room = await RocketChat.getRoom(message.rid); room = await RocketChat.getRoom(message.subscription.id);
} catch (e) { } catch (e) {
log(e); log(e);
return null; return null;
@ -587,7 +586,7 @@ const RocketChat = {
c: 'channel', c: 'channel',
d: 'direct' d: 'direct'
}[room.t]; }[room.t];
return `${ server }/${ roomType }/${ room.name }?msg=${ message._id }`; return `${ server }/${ roomType }/${ room.name }?msg=${ message.id }`;
}, },
getPermalinkChannel(channel) { getPermalinkChannel(channel) {
const { server } = reduxStore.getState().server; const { server } = reduxStore.getState().server;
@ -725,25 +724,26 @@ const RocketChat = {
// RC 0.57.0 // RC 0.57.0
return this.sdk.methodCall('getSingleMessage', msgId); return this.sdk.methodCall('getSingleMessage', msgId);
}, },
hasPermission(permissions, rid) { async hasPermission(permissions, rid) {
const db = database.active;
const subsCollection = db.collections.get('subscriptions');
const permissionsCollection = db.collections.get('permissions');
let roomRoles = []; let roomRoles = [];
try { try {
// get the room from realm // get the room from database
const [room] = database.objects('subscriptions').filtered('rid = $0', rid); const room = await subsCollection.find(rid);
if (!room) { // get room roles
roomRoles = room.roles;
} catch (error) {
console.log('hasPermission -> Room not found');
return permissions.reduce((result, permission) => { return permissions.reduce((result, permission) => {
result[permission] = false; result[permission] = false;
return result; return result;
}, {}); }, {});
} }
// get room roles // get permissions from database
roomRoles = room.roles; try {
} catch (error) { const permissionsFiltered = await permissionsCollection.query(Q.where('id', Q.oneOf(permissions))).fetch();
console.log('hasPermission -> error', error);
}
// get permissions from realm
const permissionsFiltered = database.objects('permissions')
.filter(permission => permissions.includes(permission._id));
// get user roles on the server from redux // get user roles on the server from redux
const userRoles = (reduxStore.getState().login.user && reduxStore.getState().login.user.roles) || []; const userRoles = (reduxStore.getState().login.user && reduxStore.getState().login.user.roles) || [];
// merge both roles // merge both roles
@ -753,12 +753,15 @@ const RocketChat = {
// e.g. { 'edit-room': true, 'set-readonly': false } // e.g. { 'edit-room': true, 'set-readonly': false }
return permissions.reduce((result, permission) => { return permissions.reduce((result, permission) => {
result[permission] = false; result[permission] = false;
const permissionFound = permissionsFiltered.find(p => p._id === permission); const permissionFound = permissionsFiltered.find(p => p.id === permission);
if (permissionFound) { if (permissionFound) {
result[permission] = returnAnArray(permissionFound.roles).some(r => mergedRoles.includes(r)); result[permission] = returnAnArray(permissionFound.roles).some(r => mergedRoles.includes(r));
} }
return result; return result;
}, {}); }, {});
} catch (e) {
log(e);
}
}, },
getAvatarSuggestion() { getAvatarSuggestion() {
// RC 0.51.0 // RC 0.51.0
@ -927,6 +930,35 @@ const RocketChat = {
command, params, roomId, previewItem command, params, roomId, previewItem
}); });
}, },
_setUser(ddpMessage) {
this.activeUsers = this.activeUsers || {};
const { user } = reduxStore.getState().login;
if (ddpMessage.fields && user && user.id === ddpMessage.id) {
reduxStore.dispatch(setUser(ddpMessage.fields));
}
if (ddpMessage.cleared && user && user.id === ddpMessage.id) {
reduxStore.dispatch(setUser({ status: 'offline' }));
}
if (!this._setUserTimer) {
this._setUserTimer = setTimeout(() => {
const activeUsersBatch = this.activeUsers;
InteractionManager.runAfterInteractions(() => {
reduxStore.dispatch(setActiveUsers(activeUsersBatch));
});
this._setUserTimer = null;
return this.activeUsers = {};
}, 10000);
}
if (!ddpMessage.fields) {
this.activeUsers[ddpMessage.id] = 'offline';
} else if (ddpMessage.fields.status) {
this.activeUsers[ddpMessage.id] = ddpMessage.fields.status;
}
},
getUserPresence() { getUserPresence() {
return new Promise(async(resolve) => { return new Promise(async(resolve) => {
const serverVersion = reduxStore.getState().server.version; const serverVersion = reduxStore.getState().server.version;
@ -950,16 +982,12 @@ const RocketChat = {
// RC 1.1.0 // RC 1.1.0
const result = await this.sdk.get('users.presence', params); const result = await this.sdk.get('users.presence', params);
if (result.success) { if (result.success) {
// this.lastUserPresenceFetch = new Date(); const activeUsers = result.users.reduce((ret, item) => {
database.memoryDatabase.write(() => { ret[item._id] = item.status;
result.users.forEach((item) => { return ret;
try { }, {});
item.id = item._id; InteractionManager.runAfterInteractions(() => {
database.memoryDatabase.create('activeUsers', item, true); reduxStore.dispatch(setActiveUsers(activeUsers));
} catch (error) {
console.log(error);
}
});
}); });
this.sdk.subscribe('stream-notify-logged', 'user-status'); this.sdk.subscribe('stream-notify-logged', 'user-status');
return resolve(); return resolve();
@ -975,13 +1003,15 @@ const RocketChat = {
query, count, offset, sort query, count, offset, sort
}); });
}, },
canAutoTranslate() { async canAutoTranslate() {
const db = database.active;
try { try {
const AutoTranslate_Enabled = reduxStore.getState().settings && reduxStore.getState().settings.AutoTranslate_Enabled; const AutoTranslate_Enabled = reduxStore.getState().settings && reduxStore.getState().settings.AutoTranslate_Enabled;
if (!AutoTranslate_Enabled) { if (!AutoTranslate_Enabled) {
return false; return false;
} }
const autoTranslatePermission = database.objectForPrimaryKey('permissions', 'auto-translate'); const permissionsCollection = db.collections.get('permissions');
const autoTranslatePermission = await permissionsCollection.find('auto-translate');
const userRoles = (reduxStore.getState().login.user && reduxStore.getState().login.user.roles) || []; const userRoles = (reduxStore.getState().login.user && reduxStore.getState().login.user.roles) || [];
return autoTranslatePermission.roles.some(role => userRoles.includes(role)); return autoTranslatePermission.roles.some(role => userRoles.includes(role));
} catch (e) { } catch (e) {

View File

@ -20,7 +20,7 @@ const formatMsg = ({
let prefix = ''; let prefix = '';
const isLastMessageSentByMe = lastMessage.u.username === username; const isLastMessageSentByMe = lastMessage.u.username === username;
if (!lastMessage.msg && Object.keys(lastMessage.attachments).length) { if (!lastMessage.msg && lastMessage.attachments && Object.keys(lastMessage.attachments).length) {
const user = isLastMessageSentByMe ? I18n.t('You') : lastMessage.u.username; const user = isLastMessageSentByMe ? I18n.t('You') : lastMessage.u.username;
return I18n.t('User_sent_an_attachment', { user }); return I18n.t('User_sent_an_attachment', { user });
} }

View File

@ -1,20 +1,20 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import Status from '../../containers/Status'; import Status from '../../containers/Status/Status';
import RoomTypeIcon from '../../containers/RoomTypeIcon'; import RoomTypeIcon from '../../containers/RoomTypeIcon';
import styles from './styles'; import styles from './styles';
const TypeIcon = React.memo(({ type, id, prid }) => { const TypeIcon = React.memo(({ type, prid, status }) => {
if (type === 'd') { if (type === 'd') {
return <Status style={styles.status} size={10} id={id} />; return <Status style={styles.status} size={10} status={status} />;
} }
return <RoomTypeIcon type={prid ? 'discussion' : type} />; return <RoomTypeIcon type={prid ? 'discussion' : type} />;
}); });
TypeIcon.propTypes = { TypeIcon.propTypes = {
type: PropTypes.string, type: PropTypes.string,
id: PropTypes.string, status: PropTypes.string,
prid: PropTypes.string prid: PropTypes.string
}; };

View File

@ -1,12 +1,20 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { View, Text, Animated } from 'react-native'; import { View, Text, Animated } from 'react-native';
import { RectButton, PanGestureHandler, State } from 'react-native-gesture-handler'; import {
RectButton,
PanGestureHandler,
State
} from 'react-native-gesture-handler';
import { connect } from 'react-redux';
import Avatar from '../../containers/Avatar'; import Avatar from '../../containers/Avatar';
import I18n from '../../i18n'; import I18n from '../../i18n';
import styles, { import styles, {
ROW_HEIGHT, ACTION_WIDTH, SMALL_SWIPE, LONG_SWIPE ROW_HEIGHT,
ACTION_WIDTH,
SMALL_SWIPE,
LONG_SWIPE
} from './styles'; } from './styles';
import UnreadBadge from './UnreadBadge'; import UnreadBadge from './UnreadBadge';
import TypeIcon from './TypeIcon'; import TypeIcon from './TypeIcon';
@ -16,9 +24,20 @@ import { LeftActions, RightActions } from './Actions';
export { ROW_HEIGHT }; export { ROW_HEIGHT };
const attrs = ['name', 'unread', 'userMentions', 'showLastMessage', 'alert', 'type', 'width', 'isRead', 'favorite']; const attrs = [
'name',
'unread',
'userMentions',
'showLastMessage',
'alert',
'type',
'width',
'isRead',
'favorite',
'status'
];
export default class RoomItem extends React.Component { class RoomItem extends React.Component {
static propTypes = { static propTypes = {
type: PropTypes.string.isRequired, type: PropTypes.string.isRequired,
name: PropTypes.string.isRequired, name: PropTypes.string.isRequired,
@ -41,17 +60,16 @@ export default class RoomItem extends React.Component {
favorite: PropTypes.bool, favorite: PropTypes.bool,
isRead: PropTypes.bool, isRead: PropTypes.bool,
rid: PropTypes.string, rid: PropTypes.string,
status: PropTypes.string,
toggleFav: PropTypes.func, toggleFav: PropTypes.func,
toggleRead: PropTypes.func, toggleRead: PropTypes.func,
hideChannel: PropTypes.func hideChannel: PropTypes.func
} };
static defaultProps = { static defaultProps = {
avatarSize: 48 avatarSize: 48
} };
// Making jest happy: https://github.com/facebook/react-native/issues/22175
// eslint-disable-next-line no-useless-constructor
constructor(props) { constructor(props) {
super(props); super(props);
this.dragX = new Animated.Value(0); this.dragX = new Animated.Value(0);
@ -70,13 +88,7 @@ export default class RoomItem extends React.Component {
} }
shouldComponentUpdate(nextProps) { shouldComponentUpdate(nextProps) {
const { lastMessage, _updatedAt } = this.props; const { _updatedAt } = this.props;
const oldlastMessage = lastMessage;
const newLastmessage = nextProps.lastMessage;
if (oldlastMessage && newLastmessage && oldlastMessage.ts !== newLastmessage.ts) {
return true;
}
if (_updatedAt && nextProps._updatedAt && nextProps._updatedAt.toISOString() !== _updatedAt.toISOString()) { if (_updatedAt && nextProps._updatedAt && nextProps._updatedAt.toISOString() !== _updatedAt.toISOString()) {
return true; return true;
} }
@ -165,31 +177,31 @@ export default class RoomItem extends React.Component {
toggleFav(rid, favorite); toggleFav(rid, favorite);
} }
this.close(); this.close();
} };
toggleRead = () => { toggleRead = () => {
const { toggleRead, rid, isRead } = this.props; const { toggleRead, rid, isRead } = this.props;
if (toggleRead) { if (toggleRead) {
toggleRead(rid, isRead); toggleRead(rid, isRead);
} }
} };
hideChannel = () => { hideChannel = () => {
const { hideChannel, rid, type } = this.props; const { hideChannel, rid, type } = this.props;
if (hideChannel) { if (hideChannel) {
hideChannel(rid, type); hideChannel(rid, type);
} }
} };
onToggleReadPress = () => { onToggleReadPress = () => {
this.toggleRead(); this.toggleRead();
this.close(); this.close();
} };
onHidePress = () => { onHidePress = () => {
this.hideChannel(); this.hideChannel();
this.close(); this.close();
} };
onPress = () => { onPress = () => {
const { rowState } = this.state; const { rowState } = this.state;
@ -201,11 +213,11 @@ export default class RoomItem extends React.Component {
if (onPress) { if (onPress) {
onPress(); onPress();
} }
} };
render() { render() {
const { const {
unread, userMentions, name, _updatedAt, alert, testID, type, avatarSize, baseUrl, userId, username, token, id, prid, showLastMessage, lastMessage, isRead, width, favorite unread, userMentions, name, _updatedAt, alert, testID, type, avatarSize, baseUrl, userId, username, token, id, prid, showLastMessage, lastMessage, isRead, width, favorite, status
} = this.props; } = this.props;
const date = formatDate(_updatedAt); const date = formatDate(_updatedAt);
@ -246,11 +258,9 @@ export default class RoomItem extends React.Component {
onHidePress={this.onHidePress} onHidePress={this.onHidePress}
/> />
<Animated.View <Animated.View
style={ style={{
{
transform: [{ translateX: this.transX }] transform: [{ translateX: this.transX }]
} }}
}
> >
<RectButton <RectButton
onPress={this.onPress} onPress={this.onPress}
@ -263,16 +273,59 @@ export default class RoomItem extends React.Component {
style={styles.container} style={styles.container}
accessibilityLabel={accessibilityLabel} accessibilityLabel={accessibilityLabel}
> >
<Avatar text={name} size={avatarSize} type={type} baseUrl={baseUrl} style={styles.avatar} userId={userId} token={token} /> <Avatar
text={name}
size={avatarSize}
type={type}
baseUrl={baseUrl}
style={styles.avatar}
userId={userId}
token={token}
/>
<View style={styles.centerContainer}> <View style={styles.centerContainer}>
<View style={styles.titleContainer}> <View style={styles.titleContainer}>
<TypeIcon type={type} id={id} prid={prid} /> <TypeIcon
<Text style={[styles.title, alert && styles.alert]} ellipsizeMode='tail' numberOfLines={1}>{ name }</Text> type={type}
{_updatedAt ? <Text style={[styles.date, alert && styles.updateAlert]} ellipsizeMode='tail' numberOfLines={1}>{ capitalize(date) }</Text> : null} id={id}
prid={prid}
status={status}
/>
<Text
style={[
styles.title,
alert && styles.alert
]}
ellipsizeMode='tail'
numberOfLines={1}
>
{name}
</Text>
{_updatedAt ? (
<Text
style={[
styles.date,
alert && styles.updateAlert
]}
ellipsizeMode='tail'
numberOfLines={1}
>
{capitalize(date)}
</Text>
) : null}
</View> </View>
<View style={styles.row}> <View style={styles.row}>
<LastMessage lastMessage={lastMessage} type={type} showLastMessage={showLastMessage} username={username} alert={alert} /> <LastMessage
<UnreadBadge unread={unread} userMentions={userMentions} type={type} /> lastMessage={lastMessage}
type={type}
showLastMessage={showLastMessage}
username={username}
alert={alert}
/>
<UnreadBadge
unread={unread}
userMentions={userMentions}
type={type}
/>
</View> </View>
</View> </View>
</View> </View>
@ -283,3 +336,9 @@ export default class RoomItem extends React.Component {
); );
} }
} }
const mapStateToProps = (state, ownProps) => ({
status: state.meteor.connected && ownProps.type === 'd' ? state.activeUsers[ownProps.id] : 'offline'
});
export default connect(mapStateToProps)(RoomItem);

View File

@ -0,0 +1,15 @@
import { SET_ACTIVE_USERS } from '../actions/actionsTypes';
const initialState = {};
export default function activeUsers(state = initialState, action) {
switch (action.type) {
case SET_ACTIVE_USERS:
return {
...state,
...action.activeUsers
};
default:
return state;
}
}

View File

@ -7,7 +7,7 @@ const initialState = {
error: {} error: {}
}; };
export default function messages(state = initialState, action) { export default function(state = initialState, action) {
switch (action.type) { switch (action.type) {
case CREATE_CHANNEL.REQUEST: case CREATE_CHANNEL.REQUEST:
return { return {

View File

@ -0,0 +1,14 @@
import { SET_CUSTOM_EMOJIS } from '../actions/actionsTypes';
const initialState = {
customEmojis: {}
};
export default function customEmojis(state = initialState, action) {
switch (action.type) {
case SET_CUSTOM_EMOJIS:
return action.emojis;
default:
return state;
}
}

View File

@ -2,7 +2,6 @@ import { combineReducers } from 'redux';
import settings from './reducers'; import settings from './reducers';
import login from './login'; import login from './login';
import meteor from './connect'; import meteor from './connect';
import messages from './messages';
import rooms from './rooms'; import rooms from './rooms';
import server from './server'; import server from './server';
import selectedUsers from './selectedUsers'; import selectedUsers from './selectedUsers';
@ -13,12 +12,14 @@ import notification from './notification';
import markdown from './markdown'; import markdown from './markdown';
import share from './share'; import share from './share';
import crashReport from './crashReport'; import crashReport from './crashReport';
import customEmojis from './customEmojis';
import activeUsers from './activeUsers';
import usersTyping from './usersTyping';
export default combineReducers({ export default combineReducers({
settings, settings,
login, login,
meteor, meteor,
messages,
server, server,
selectedUsers, selectedUsers,
createChannel, createChannel,
@ -28,5 +29,8 @@ export default combineReducers({
notification, notification,
markdown, markdown,
share, share,
crashReport crashReport,
customEmojis,
activeUsers,
usersTyping
}); });

View File

@ -1,96 +0,0 @@
import * as types from '../actions/actionsTypes';
const initialState = {
message: {},
actionMessage: {},
replyMessage: {},
replying: false,
editing: false,
showActions: false,
showErrorActions: false,
showReactionPicker: false
};
export default function messages(state = initialState, action) {
switch (action.type) {
case types.MESSAGES.ACTIONS_SHOW:
return {
...state,
showActions: true,
actionMessage: action.actionMessage
};
case types.MESSAGES.ACTIONS_HIDE:
return {
...state,
showActions: false
};
case types.MESSAGES.ERROR_ACTIONS_SHOW:
return {
...state,
showErrorActions: true,
actionMessage: action.actionMessage
};
case types.MESSAGES.ERROR_ACTIONS_HIDE:
return {
...state,
showErrorActions: false
};
case types.MESSAGES.EDIT_INIT:
return {
...state,
message: action.message,
editing: true
};
case types.MESSAGES.EDIT_CANCEL:
return {
...state,
message: {},
editing: false
};
case types.MESSAGES.EDIT_SUCCESS:
return {
...state,
message: {},
editing: false
};
case types.MESSAGES.EDIT_FAILURE:
return {
...state,
message: {},
editing: false
};
case types.MESSAGES.REPLY_INIT:
return {
...state,
replyMessage: {
...action.message,
mention: action.mention
},
replying: true
};
case types.MESSAGES.REPLY_CANCEL:
return {
...state,
replyMessage: {},
replying: false
};
case types.MESSAGES.SET_INPUT:
return {
...state,
message: action.message
};
case types.MESSAGES.CLEAR_INPUT:
return {
...state,
message: {}
};
case types.MESSAGES.TOGGLE_REACTION_PICKER:
return {
...state,
showReactionPicker: !state.showReactionPicker,
actionMessage: action.message
};
default:
return state;
}
}

View File

@ -5,7 +5,7 @@ const initialState = {
loading: false loading: false
}; };
export default function messages(state = initialState, action) { export default function(state = initialState, action) {
switch (action.type) { switch (action.type) {
case SELECTED_USERS.ADD_USER: case SELECTED_USERS.ADD_USER:
return { return {

View File

@ -0,0 +1,19 @@
import { USERS_TYPING } from '../actions/actionsTypes';
const initialState = [];
export default function usersTyping(state = initialState, action) {
switch (action.type) {
case USERS_TYPING.ADD:
if (state.findIndex(item => item === action.username) === -1) {
return [...state, action.username];
}
return state;
case USERS_TYPING.REMOVE:
return state.filter(item => item !== action.username);
case USERS_TYPING.CLEAR:
return initialState;
default:
return state;
}
}

View File

@ -6,7 +6,7 @@ import RNUserDefaults from 'rn-user-defaults';
import Navigation from '../lib/Navigation'; import Navigation from '../lib/Navigation';
import * as types from '../actions/actionsTypes'; import * as types from '../actions/actionsTypes';
import { selectServerRequest } from '../actions/server'; import { selectServerRequest } from '../actions/server';
import database from '../lib/realm'; import database from '../lib/database';
import RocketChat from '../lib/rocketchat'; import RocketChat from '../lib/rocketchat';
import EventEmitter from '../utils/events'; import EventEmitter from '../utils/events';
import { appStart } from '../actions'; import { appStart } from '../actions';
@ -66,12 +66,19 @@ const handleOpen = function* handleOpen({ params }) {
} }
} else { } else {
// search if deep link's server already exists // search if deep link's server already exists
const servers = yield database.databases.serversDB.objects('servers').filtered('id = $0', host); // TODO: need better test const serversDB = database.servers;
if (servers.length && user) { const serversCollection = serversDB.collections.get('servers');
try {
const servers = yield serversCollection.find(host);
if (servers && user) {
yield put(selectServerRequest(host)); yield put(selectServerRequest(host));
yield take(types.SERVER.SELECT_SUCCESS); yield take(types.SERVER.SELECT_SUCCESS);
yield navigate({ params }); yield navigate({ params });
} else { return;
}
} catch (e) {
// do nothing?
}
// if deep link is from a different server // if deep link is from a different server
const result = yield RocketChat.getServerInfo(server); const result = yield RocketChat.getServerInfo(server);
if (!result.success) { if (!result.success) {
@ -81,7 +88,6 @@ const handleOpen = function* handleOpen({ params }) {
yield delay(1000); yield delay(1000);
EventEmitter.emit('NewServer', { server: host }); EventEmitter.emit('NewServer', { server: host });
} }
}
}; };
const root = function* root() { const root = function* root() {

View File

@ -2,6 +2,7 @@ import { AsyncStorage } from 'react-native';
import { put, takeLatest, all } from 'redux-saga/effects'; import { put, takeLatest, all } from 'redux-saga/effects';
import SplashScreen from 'react-native-splash-screen'; import SplashScreen from 'react-native-splash-screen';
import RNUserDefaults from 'rn-user-defaults'; import RNUserDefaults from 'rn-user-defaults';
import { sanitizedRaw } from '@nozbe/watermelondb/RawRecord';
import * as actions from '../actions'; import * as actions from '../actions';
import { selectServerRequest } from '../actions/server'; import { selectServerRequest } from '../actions/server';
@ -12,11 +13,12 @@ import { APP } from '../actions/actionsTypes';
import RocketChat from '../lib/rocketchat'; import RocketChat from '../lib/rocketchat';
import log from '../utils/log'; import log from '../utils/log';
import Navigation from '../lib/Navigation'; import Navigation from '../lib/Navigation';
import database from '../lib/realm';
import { import {
SERVERS, SERVER_ICON, SERVER_NAME, SERVER_URL, TOKEN, USER_ID SERVERS, SERVER_ICON, SERVER_NAME, SERVER_URL, TOKEN, USER_ID
} from '../constants/userDefaults'; } from '../constants/userDefaults';
import { isIOS } from '../utils/deviceInfo'; import { isIOS } from '../utils/deviceInfo';
import database from '../lib/database';
import protectedFunction from '../lib/methods/helpers/protectedFunction';
const restore = function* restore() { const restore = function* restore() {
try { try {
@ -31,34 +33,47 @@ const restore = function* restore() {
server: RNUserDefaults.get('currentServer') server: RNUserDefaults.get('currentServer')
}); });
// get native credentials let servers = yield RNUserDefaults.objectForKey(SERVERS);
if (isIOS && !hasMigration) {
const { serversDB } = database.databases;
const servers = yield RNUserDefaults.objectForKey(SERVERS);
if (servers) {
serversDB.write(() => {
servers.forEach(async(serverItem) => {
const serverInfo = {
id: serverItem[SERVER_URL],
name: serverItem[SERVER_NAME],
iconURL: serverItem[SERVER_ICON]
};
try {
serversDB.create('servers', serverInfo, true);
await RNUserDefaults.set(`${ RocketChat.TOKEN_KEY }-${ serverInfo.id }`, serverItem[USER_ID]);
} catch (e) {
log(e);
}
});
});
yield AsyncStorage.setItem('hasMigration', '1');
}
// if not have current // if not have current
if (servers && servers.length !== 0 && (!token || !server)) { if (servers && servers.length !== 0 && (!token || !server)) {
server = servers[0][SERVER_URL]; server = servers[0][SERVER_URL];
token = servers[0][TOKEN]; token = servers[0][TOKEN];
} }
// get native credentials
if (servers && !hasMigration) {
// parse servers
servers = yield Promise.all(servers.map(async(s) => {
await RNUserDefaults.set(`${ RocketChat.TOKEN_KEY }-${ s[SERVER_URL] }`, s[USER_ID]);
return ({ id: s[SERVER_URL], name: s[SERVER_NAME], iconURL: s[SERVER_ICON] });
}));
try {
const serversDB = database.servers;
yield serversDB.action(async() => {
const serversCollection = serversDB.collections.get('servers');
const allServerRecords = await serversCollection.query().fetch();
// filter servers
let serversToCreate = servers.filter(i1 => !allServerRecords.find(i2 => i1.id === i2.id));
// Create
serversToCreate = serversToCreate.map(record => serversCollection.prepareCreate(protectedFunction((s) => {
s._raw = sanitizedRaw({ id: record.id }, serversCollection.schema);
Object.assign(s, record);
})));
const allRecords = serversToCreate;
try {
await serversDB.batch(...allRecords);
} catch (e) {
log(e);
}
return allRecords.length;
});
} catch (e) {
log(e);
}
} }
const sortPreferences = yield RocketChat.getSortPreferences(); const sortPreferences = yield RocketChat.getSortPreferences();
@ -77,13 +92,16 @@ const restore = function* restore() {
]); ]);
yield put(actions.appStart('outside')); yield put(actions.appStart('outside'));
} else if (server) { } else if (server) {
const serverObj = database.databases.serversDB.objectForPrimaryKey('servers', server); const serversDB = database.servers;
const serverCollections = serversDB.collections.get('servers');
const serverObj = yield serverCollections.find(server);
yield put(selectServerRequest(server, serverObj && serverObj.version)); yield put(selectServerRequest(server, serverObj && serverObj.version));
} }
yield put(actions.appReady({})); yield put(actions.appReady({}));
} catch (e) { } catch (e) {
log(e); log(e);
yield put(actions.appStart('outside'));
} }
}; };

View File

@ -2,6 +2,7 @@ import {
put, call, takeLatest, select, take, fork, cancel put, call, takeLatest, select, take, fork, cancel
} from 'redux-saga/effects'; } from 'redux-saga/effects';
import RNUserDefaults from 'rn-user-defaults'; import RNUserDefaults from 'rn-user-defaults';
import { sanitizedRaw } from '@nozbe/watermelondb/RawRecord';
import moment from 'moment'; import moment from 'moment';
import 'moment/min/locales'; import 'moment/min/locales';
@ -14,7 +15,7 @@ import { toMomentLocale } from '../utils/moment';
import RocketChat from '../lib/rocketchat'; import RocketChat from '../lib/rocketchat';
import log from '../utils/log'; import log from '../utils/log';
import I18n from '../i18n'; import I18n from '../i18n';
import database from '../lib/realm'; import database from '../lib/database';
import EventEmitter from '../utils/events'; import EventEmitter from '../utils/events';
const getServer = state => state.server.server; const getServer = state => state.server.server;
@ -77,12 +78,28 @@ const handleLoginSuccess = function* handleLoginSuccess({ user }) {
I18n.locale = user.language; I18n.locale = user.language;
moment.locale(toMomentLocale(user.language)); moment.locale(toMomentLocale(user.language));
const { serversDB } = database.databases; const serversDB = database.servers;
serversDB.write(() => { const usersCollection = serversDB.collections.get('users');
const u = {
token: user.token,
username: user.username,
name: user.name,
language: user.language,
status: user.status,
roles: user.roles
};
yield serversDB.action(async() => {
try { try {
serversDB.create('user', user, true); const userRecord = await usersCollection.find(user.id);
await userRecord.update((record) => {
record._raw = sanitizedRaw({ id: user.id, ...record._raw }, usersCollection.schema);
Object.assign(record, u);
});
} catch (e) { } catch (e) {
log(e); await usersCollection.create((record) => {
record._raw = sanitizedRaw({ id: user.id }, usersCollection.schema);
Object.assign(record, u);
});
} }
}); });
@ -108,14 +125,18 @@ const handleLogout = function* handleLogout() {
if (server) { if (server) {
try { try {
yield call(logoutCall, { server }); yield call(logoutCall, { server });
const { serversDB } = database.databases; const serversDB = database.servers;
// all servers // all servers
const servers = yield serversDB.objects('servers'); const serversCollection = serversDB.collections.get('servers');
// filter logging out server and delete it // filter logging out server and delete it
const serverRecord = servers.filtered('id = $0', server); yield serversDB.action(async() => {
serversDB.write(() => { const serverRecord = await serversCollection.find(server);
serversDB.delete(serverRecord); await serverRecord.destroyPermanently();
}); });
const servers = yield serversCollection.query().fetch();
// see if there's other logged in servers and selects first one // see if there's other logged in servers and selects first one
if (servers.length > 0) { if (servers.length > 0) {
const newServer = servers[0].id; const newServer = servers[0].id;

View File

@ -1,92 +1,37 @@
import { import { takeLatest } from 'redux-saga/effects';
takeLatest, put, call, delay import { Q } from '@nozbe/watermelondb';
} from 'redux-saga/effects';
import Navigation from '../lib/Navigation'; import Navigation from '../lib/Navigation';
import { MESSAGES } from '../actions/actionsTypes'; import { MESSAGES } from '../actions/actionsTypes';
import {
deleteSuccess,
deleteFailure,
editSuccess,
editFailure,
toggleStarSuccess,
toggleStarFailure,
togglePinSuccess,
togglePinFailure,
replyInit
} from '../actions/messages';
import RocketChat from '../lib/rocketchat'; import RocketChat from '../lib/rocketchat';
import database from '../lib/realm'; import database from '../lib/database';
import log from '../utils/log'; import log from '../utils/log';
const deleteMessage = message => RocketChat.deleteMessage(message); const goRoom = function goRoom({ rid, name, message }) {
const editMessage = message => RocketChat.editMessage(message);
const toggleStarMessage = message => RocketChat.toggleStarMessage(message);
const togglePinMessage = message => RocketChat.togglePinMessage(message);
const handleDeleteRequest = function* handleDeleteRequest({ message }) {
try {
yield call(deleteMessage, message);
yield put(deleteSuccess());
} catch (error) {
yield put(deleteFailure());
}
};
const handleEditRequest = function* handleEditRequest({ message }) {
try {
yield call(editMessage, message);
yield put(editSuccess());
} catch (error) {
yield put(editFailure());
}
};
const handleToggleStarRequest = function* handleToggleStarRequest({ message }) {
try {
yield call(toggleStarMessage, message);
yield put(toggleStarSuccess());
} catch (error) {
yield put(toggleStarFailure());
}
};
const handleTogglePinRequest = function* handleTogglePinRequest({ message }) {
try {
yield call(togglePinMessage, message);
yield put(togglePinSuccess());
} catch (error) {
yield put(togglePinFailure(error));
}
};
const goRoom = function goRoom({ rid, name }) {
Navigation.navigate('RoomsListView'); Navigation.navigate('RoomsListView');
Navigation.navigate('RoomView', { rid, name, t: 'd' }); Navigation.navigate('RoomView', {
rid, name, t: 'd', message
});
}; };
const handleReplyBroadcast = function* handleReplyBroadcast({ message }) { const handleReplyBroadcast = function* handleReplyBroadcast({ message }) {
try { try {
const db = database.active;
const { username } = message.u; const { username } = message.u;
const subscriptions = database.objects('subscriptions').filtered('name = $0', username); const subsCollection = db.collections.get('subscriptions');
const subscriptions = yield subsCollection.query(Q.where('name', username)).fetch();
if (subscriptions.length) { if (subscriptions.length) {
yield goRoom({ rid: subscriptions[0].rid, name: username }); yield goRoom({ rid: subscriptions[0].rid, name: username, message });
} else { } else {
const room = yield RocketChat.createDirectMessage(username); const room = yield RocketChat.createDirectMessage(username);
yield goRoom({ rid: room.rid, name: username }); yield goRoom({ rid: room.rid, name: username, message });
} }
yield delay(500);
yield put(replyInit(message, false));
} catch (e) { } catch (e) {
log(e); log(e);
} }
}; };
const root = function* root() { const root = function* root() {
yield takeLatest(MESSAGES.DELETE_REQUEST, handleDeleteRequest);
yield takeLatest(MESSAGES.EDIT_REQUEST, handleEditRequest);
yield takeLatest(MESSAGES.TOGGLE_STAR_REQUEST, handleToggleStarRequest);
yield takeLatest(MESSAGES.TOGGLE_PIN_REQUEST, handleTogglePinRequest);
yield takeLatest(MESSAGES.REPLY_BROADCAST, handleReplyBroadcast); yield takeLatest(MESSAGES.REPLY_BROADCAST, handleReplyBroadcast);
}; };
export default root; export default root;

View File

@ -1,6 +1,6 @@
import { Alert } from 'react-native'; import { Alert } from 'react-native';
import { import {
call, takeLatest, take, select, delay takeLatest, take, select, delay
} from 'redux-saga/effects'; } from 'redux-saga/effects';
import Navigation from '../lib/Navigation'; import Navigation from '../lib/Navigation';
@ -19,7 +19,7 @@ const watchUserTyping = function* watchUserTyping({ rid, status }) {
yield RocketChat.emitTyping(rid, status); yield RocketChat.emitTyping(rid, status);
if (status) { if (status) {
yield call(delay, 5000); yield delay(5000);
yield RocketChat.emitTyping(rid, false); yield RocketChat.emitTyping(rid, false);
} }
} catch (e) { } catch (e) {

View File

@ -2,36 +2,67 @@ import {
put, select, race, take, fork, cancel, delay put, select, race, take, fork, cancel, delay
} from 'redux-saga/effects'; } from 'redux-saga/effects';
import { BACKGROUND, INACTIVE } from 'redux-enhancer-react-native-appstate'; import { BACKGROUND, INACTIVE } from 'redux-enhancer-react-native-appstate';
import { Q } from '@nozbe/watermelondb';
import { sanitizedRaw } from '@nozbe/watermelondb/RawRecord';
import * as types from '../actions/actionsTypes'; import * as types from '../actions/actionsTypes';
import { roomsSuccess, roomsFailure } from '../actions/rooms'; import { roomsSuccess, roomsFailure } from '../actions/rooms';
import database from '../lib/realm'; import database from '../lib/database';
import log from '../utils/log'; import log from '../utils/log';
import mergeSubscriptionsRooms from '../lib/methods/helpers/mergeSubscriptionsRooms'; import mergeSubscriptionsRooms from '../lib/methods/helpers/mergeSubscriptionsRooms';
import RocketChat from '../lib/rocketchat'; import RocketChat from '../lib/rocketchat';
const handleRoomsRequest = function* handleRoomsRequest() { const handleRoomsRequest = function* handleRoomsRequest() {
try { try {
const serversDB = database.servers;
yield RocketChat.subscribeRooms(); yield RocketChat.subscribeRooms();
const newRoomsUpdatedAt = new Date(); const newRoomsUpdatedAt = new Date();
const server = yield select(state => state.server.server); const server = yield select(state => state.server.server);
const [serverRecord] = database.databases.serversDB.objects('servers').filtered('id = $0', server); const serversCollection = serversDB.collections.get('servers');
const serverRecord = yield serversCollection.find(server);
const { roomsUpdatedAt } = serverRecord; const { roomsUpdatedAt } = serverRecord;
const [subscriptionsResult, roomsResult] = yield RocketChat.getRooms(roomsUpdatedAt); const [subscriptionsResult, roomsResult] = yield RocketChat.getRooms(roomsUpdatedAt);
const { subscriptions } = mergeSubscriptionsRooms(subscriptionsResult, roomsResult); const { subscriptions } = mergeSubscriptionsRooms(subscriptionsResult, roomsResult);
database.write(() => { const db = database.active;
subscriptions.forEach((subscription) => { yield db.action(async() => {
const subCollection = db.collections.get('subscriptions');
if (!subscriptions.length) {
return;
}
const subsIds = subscriptions.map(sub => sub.rid);
const existingSubs = await subCollection.query(Q.where('id', Q.oneOf(subsIds))).fetch();
const subsToUpdate = existingSubs.filter(i1 => subscriptions.find(i2 => i1._id === i2._id));
const subsToCreate = subscriptions.filter(i1 => !existingSubs.find(i2 => i1._id === i2._id));
// TODO: subsToDelete?
const allRecords = [
...subsToCreate.map(subscription => subCollection.prepareCreate((s) => {
s._raw = sanitizedRaw({ id: subscription.rid }, subCollection.schema);
return Object.assign(s, subscription);
})),
...subsToUpdate.map((subscription) => {
const newSub = subscriptions.find(s => s._id === subscription._id);
return subscription.prepareUpdate(() => {
Object.assign(subscription, newSub);
});
})
];
try { try {
database.create('subscriptions', subscription, true); await db.batch(...allRecords);
} catch (e) { } catch (e) {
log(e); log(e);
} }
return allRecords.length;
}); });
});
database.databases.serversDB.write(() => { yield serversDB.action(async() => {
try { try {
database.databases.serversDB.create('servers', { id: server, roomsUpdatedAt: newRoomsUpdatedAt }, true); await serverRecord.update((record) => {
record.roomsUpdatedAt = newRoomsUpdatedAt;
});
} catch (e) { } catch (e) {
log(e); log(e);
} }

View File

@ -3,6 +3,7 @@ import {
} from 'redux-saga/effects'; } from 'redux-saga/effects';
import { Alert } from 'react-native'; import { Alert } from 'react-native';
import RNUserDefaults from 'rn-user-defaults'; import RNUserDefaults from 'rn-user-defaults';
import { sanitizedRaw } from '@nozbe/watermelondb/RawRecord';
import Navigation from '../lib/Navigation'; import Navigation from '../lib/Navigation';
import { SERVER } from '../actions/actionsTypes'; import { SERVER } from '../actions/actionsTypes';
@ -12,7 +13,7 @@ import {
} from '../actions/server'; } from '../actions/server';
import { setUser } from '../actions/login'; import { setUser } from '../actions/login';
import RocketChat from '../lib/rocketchat'; import RocketChat from '../lib/rocketchat';
import database from '../lib/realm'; import database from '../lib/database';
import log from '../utils/log'; import log from '../utils/log';
import { extractHostname } from '../utils/server'; import { extractHostname } from '../utils/server';
import I18n from '../i18n'; import I18n from '../i18n';
@ -29,8 +30,20 @@ const getServerInfo = function* getServerInfo({ server, raiseError = true }) {
return; return;
} }
database.databases.serversDB.write(() => { const serversDB = database.servers;
database.databases.serversDB.create('servers', { id: server, version: serverInfo.version }, true); const serversCollection = serversDB.collections.get('servers');
yield serversDB.action(async() => {
try {
const serverRecord = await serversCollection.find(server);
await serverRecord.update((record) => {
record.version = serverInfo.version;
});
} catch (e) {
await serversCollection.create((record) => {
record._raw = sanitizedRaw({ id: server }, serversCollection.schema);
record.version = serverInfo.version;
});
}
}); });
return serverInfo; return serverInfo;
@ -41,11 +54,27 @@ const getServerInfo = function* getServerInfo({ server, raiseError = true }) {
const handleSelectServer = function* handleSelectServer({ server, version, fetchVersion }) { const handleSelectServer = function* handleSelectServer({ server, version, fetchVersion }) {
try { try {
const { serversDB } = database.databases; const serversDB = database.servers;
yield RNUserDefaults.set('currentServer', server); yield RNUserDefaults.set('currentServer', server);
const userId = yield RNUserDefaults.get(`${ RocketChat.TOKEN_KEY }-${ server }`); const userId = yield RNUserDefaults.get(`${ RocketChat.TOKEN_KEY }-${ server }`);
const user = userId && serversDB.objectForPrimaryKey('user', userId); const userCollections = serversDB.collections.get('users');
let user = null;
if (userId) {
try {
user = yield userCollections.find(userId);
user = {
token: user.token,
username: user.username,
name: user.name,
language: user.language,
status: user.status,
roles: user.roles
};
user = { ...user, roles: JSON.parse(user.roles) };
} catch (e) {
// do nothing?
}
}
const servers = yield RNUserDefaults.objectForKey(SERVERS); const servers = yield RNUserDefaults.objectForKey(SERVERS);
const userCredentials = servers && servers.find(srv => srv[SERVER_URL] === server); const userCredentials = servers && servers.find(srv => srv[SERVER_URL] === server);
@ -62,9 +91,20 @@ const handleSelectServer = function* handleSelectServer({ server, version, fetch
yield put(actions.appStart('outside')); yield put(actions.appStart('outside'));
} }
const settings = database.objects('settings'); const db = database.active;
const serversCollection = db.collections.get('settings');
const settingsRecords = yield serversCollection.query().fetch();
const settings = Object.values(settingsRecords).map(item => ({
_id: item.id,
valueAsString: item.valueAsString,
valueAsBoolean: item.valueAsBoolean,
valueAsNumber: item.valueAsNumber,
_updatedAt: item._updatedAt
}));
yield put(actions.setAllSettings(RocketChat.parseSettings(settings.slice(0, settings.length)))); yield put(actions.setAllSettings(RocketChat.parseSettings(settings.slice(0, settings.length))));
yield RocketChat.setCustomEmojis();
let serverInfo; let serverInfo;
if (fetchVersion) { if (fetchVersion) {
serverInfo = yield getServerInfo({ server, raiseError: false }); serverInfo = yield getServerInfo({ server, raiseError: false });

View File

@ -9,7 +9,7 @@ export const loggerConfig = bugsnag.config;
export const { leaveBreadcrumb } = bugsnag; export const { leaveBreadcrumb } = bugsnag;
export default (e) => { export default (e) => {
if (e instanceof Error) { if (e instanceof Error && !__DEV__) {
bugsnag.notify(e); bugsnag.notify(e);
} else { } else {
console.log(e); console.log(e);

View File

@ -17,7 +17,6 @@ import {
SWITCH_TRACK_COLOR, COLOR_BACKGROUND_CONTAINER, COLOR_WHITE, COLOR_SEPARATOR SWITCH_TRACK_COLOR, COLOR_BACKGROUND_CONTAINER, COLOR_WHITE, COLOR_SEPARATOR
} from '../../constants/colors'; } from '../../constants/colors';
import scrollPersistTaps from '../../utils/scrollPersistTaps'; import scrollPersistTaps from '../../utils/scrollPersistTaps';
import database from '../../lib/realm';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
contentContainerStyle: { contentContainerStyle: {
@ -49,11 +48,19 @@ export default class AutoTranslateView extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.rid = props.navigation.getParam('rid'); this.rid = props.navigation.getParam('rid');
this.rooms = database.objects('subscriptions').filtered('rid = $0', this.rid); const room = props.navigation.getParam('room');
if (room && room.observe) {
this.roomObservable = room.observe();
this.subscription = this.roomObservable
.subscribe((changes) => {
this.room = changes;
});
}
this.state = { this.state = {
languages: [], languages: [],
selectedLanguage: this.rooms[0].autoTranslateLanguage, selectedLanguage: room.autoTranslateLanguage,
enableAutoTranslate: this.rooms[0].autoTranslate enableAutoTranslate: room.autoTranslate
}; };
} }
@ -66,6 +73,12 @@ export default class AutoTranslateView extends React.Component {
} }
} }
componentWillUnmount() {
if (this.subscription && this.subscription.unsubscribe) {
this.subscription.unsubscribe();
}
}
toggleAutoTranslate = async() => { toggleAutoTranslate = async() => {
const { enableAutoTranslate } = this.state; const { enableAutoTranslate } = this.state;
try { try {
@ -152,5 +165,3 @@ export default class AutoTranslateView extends React.Component {
); );
} }
} }
console.disableYellowBox = true;

View File

@ -26,7 +26,8 @@ class MessagesView extends React.Component {
static propTypes = { static propTypes = {
user: PropTypes.object, user: PropTypes.object,
baseUrl: PropTypes.string, baseUrl: PropTypes.string,
navigation: PropTypes.object navigation: PropTypes.object,
customEmojis: PropTypes.object
} }
constructor(props) { constructor(props) {
@ -80,7 +81,8 @@ class MessagesView extends React.Component {
isEdited: !!item.editedAt, isEdited: !!item.editedAt,
isHeader: true, isHeader: true,
attachments: item.attachments || [], attachments: item.attachments || [],
onOpenFileModal: this.onOpenFileModal onOpenFileModal: this.onOpenFileModal,
getCustomEmoji: this.getCustomEmoji
}); });
return ({ return ({
@ -145,7 +147,7 @@ class MessagesView extends React.Component {
/> />
), ),
actionTitle: I18n.t('Unstar'), actionTitle: I18n.t('Unstar'),
handleActionPress: message => RocketChat.toggleStarMessage(message) handleActionPress: message => RocketChat.toggleStarMessage(message._id, message.starred)
}, },
// Pinned Messages Screen // Pinned Messages Screen
Pinned: { Pinned: {
@ -161,7 +163,7 @@ class MessagesView extends React.Component {
/> />
), ),
actionTitle: I18n.t('Unpin'), actionTitle: I18n.t('Unpin'),
handleActionPress: message => RocketChat.togglePinMessage(message) handleActionPress: message => RocketChat.togglePinMessage(message._id, message.pinned)
} }
}[name]); }[name]);
} }
@ -191,6 +193,15 @@ class MessagesView extends React.Component {
} }
} }
getCustomEmoji = (name) => {
const { customEmojis } = this.props;
const emoji = customEmojis[name];
if (emoji) {
return emoji;
}
return null;
}
onOpenFileModal = (attachment) => { onOpenFileModal = (attachment) => {
this.setState({ selectedAttachment: attachment, photoModalVisible: true }); this.setState({ selectedAttachment: attachment, photoModalVisible: true });
} }
@ -285,7 +296,8 @@ const mapStateToProps = state => ({
id: state.login.user && state.login.user.id, id: state.login.user && state.login.user.id,
username: state.login.user && state.login.user.username, username: state.login.user && state.login.user.username,
token: state.login.user && state.login.user.token token: state.login.user && state.login.user.token
} },
customEmojis: state.customEmojis
}); });
export default connect(mapStateToProps)(MessagesView); export default connect(mapStateToProps)(MessagesView);

View File

@ -6,13 +6,15 @@ import {
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { SafeAreaView } from 'react-navigation'; import { SafeAreaView } from 'react-navigation';
import equal from 'deep-equal'; import equal from 'deep-equal';
import { orderBy } from 'lodash';
import { Q } from '@nozbe/watermelondb';
import database, { safeAddListener } from '../lib/realm'; import database from '../lib/database';
import RocketChat from '../lib/rocketchat'; import RocketChat from '../lib/rocketchat';
import UserItem from '../presentation/UserItem'; import UserItem from '../presentation/UserItem';
import debounce from '../utils/debounce';
import sharedStyles from './Styles'; import sharedStyles from './Styles';
import I18n from '../i18n'; import I18n from '../i18n';
import log from '../utils/log';
import Touch from '../utils/touch'; import Touch from '../utils/touch';
import { isIOS } from '../utils/deviceInfo'; import { isIOS } from '../utils/deviceInfo';
import SearchBox from '../containers/SearchBox'; import SearchBox from '../containers/SearchBox';
@ -67,24 +69,46 @@ class NewMessageView extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.data = database.objects('subscriptions').filtered('t = $0', 'd').sorted('roomUpdatedAt', true); this.init();
this.state = { this.state = {
search: [] search: [],
chats: []
}; };
safeAddListener(this.data, this.updateState);
} }
shouldComponentUpdate(nextProps, nextState) { shouldComponentUpdate(nextProps, nextState) {
const { search } = this.state; const { search, chats } = this.state;
if (!equal(nextState.search, search)) { if (!equal(nextState.search, search)) {
return true; return true;
} }
if (!equal(nextState.chats, chats)) {
return true;
}
return false; return false;
} }
componentWillUnmount() { componentWillUnmount() {
this.updateState.stop(); if (this.querySubscription && this.querySubscription.unsubscribe) {
this.data.removeAllListeners(); this.querySubscription.unsubscribe();
}
}
// eslint-disable-next-line react/sort-comp
init = async() => {
try {
const db = database.active;
const observable = await db.collections
.get('subscriptions')
.query(Q.where('t', 'd'))
.observeWithColumns(['room_updated_at']);
this.querySubscription = observable.subscribe((data) => {
const chats = orderBy(data, ['roomUpdatedAt'], ['desc']);
this.setState({ chats });
});
} catch (e) {
log(e);
}
} }
onSearchChangeText(text) { onSearchChangeText(text) {
@ -102,11 +126,6 @@ class NewMessageView extends React.Component {
return navigation.pop(); return navigation.pop();
} }
// eslint-disable-next-line react/sort-comp
updateState = debounce(() => {
this.forceUpdate();
}, 1000);
search = async(text) => { search = async(text) => {
const result = await RocketChat.search({ text, filterRooms: false }); const result = await RocketChat.search({ text, filterRooms: false });
this.setState({ this.setState({
@ -134,7 +153,7 @@ class NewMessageView extends React.Component {
renderSeparator = () => <View style={[sharedStyles.separator, styles.separator]} />; renderSeparator = () => <View style={[sharedStyles.separator, styles.separator]} />;
renderItem = ({ item, index }) => { renderItem = ({ item, index }) => {
const { search } = this.state; const { search, chats } = this.state;
const { baseUrl, user } = this.props; const { baseUrl, user } = this.props;
let style = {}; let style = {};
@ -144,7 +163,7 @@ class NewMessageView extends React.Component {
if (search.length > 0 && index === search.length - 1) { if (search.length > 0 && index === search.length - 1) {
style = { ...style, ...sharedStyles.separatorBottom }; style = { ...style, ...sharedStyles.separatorBottom };
} }
if (search.length === 0 && index === this.data.length - 1) { if (search.length === 0 && index === chats.length - 1) {
style = { ...style, ...sharedStyles.separatorBottom }; style = { ...style, ...sharedStyles.separatorBottom };
} }
return ( return (
@ -161,10 +180,10 @@ class NewMessageView extends React.Component {
} }
renderList = () => { renderList = () => {
const { search } = this.state; const { search, chats } = this.state;
return ( return (
<FlatList <FlatList
data={search.length > 0 ? search : this.data} data={search.length > 0 ? search : chats}
extraData={this.state} extraData={this.state}
keyExtractor={item => item._id} keyExtractor={item => item._id}
ListHeaderComponent={this.renderHeader} ListHeaderComponent={this.renderHeader}

View File

@ -14,7 +14,6 @@ import I18n from '../../i18n';
import scrollPersistTaps from '../../utils/scrollPersistTaps'; import scrollPersistTaps from '../../utils/scrollPersistTaps';
import styles from './styles'; import styles from './styles';
import sharedStyles from '../Styles'; import sharedStyles from '../Styles';
import database from '../../lib/realm';
import RocketChat from '../../lib/rocketchat'; import RocketChat from '../../lib/rocketchat';
import log from '../../utils/log'; import log from '../../utils/log';
@ -112,13 +111,37 @@ export default class NotificationPreferencesView extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.mounted = false;
this.rid = props.navigation.getParam('rid'); this.rid = props.navigation.getParam('rid');
this.rooms = database.objects('subscriptions').filtered('rid = $0', this.rid); const room = props.navigation.getParam('room');
if (room && room.observe) {
this.roomObservable = room.observe();
this.subscription = this.roomObservable
.subscribe((changes) => {
if (this.mounted) {
this.setState({ room: changes });
} else {
this.state.room = changes;
}
});
}
this.state = { this.state = {
room: JSON.parse(JSON.stringify(this.rooms[0] || {})) room: room || {}
}; };
} }
componentDidMount() {
this.mounted = true;
}
componentWillUnmount() {
if (this.subscription && this.subscription.unsubscribe) {
this.subscription.unsubscribe();
}
}
onValueChangeSwitch = async(key, value) => { onValueChangeSwitch = async(key, value) => {
const { room: newRoom } = this.state; const { room: newRoom } = this.state;
newRoom[key] = value; newRoom[key] = value;

View File

@ -5,7 +5,6 @@ import {
} from 'react-native'; } from 'react-native';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { SafeAreaView } from 'react-navigation'; import { SafeAreaView } from 'react-navigation';
import equal from 'deep-equal';
import { leaveRoom as leaveRoomAction } from '../../actions/room'; import { leaveRoom as leaveRoomAction } from '../../actions/room';
import styles from './styles'; import styles from './styles';
@ -13,7 +12,6 @@ import sharedStyles from '../Styles';
import Avatar from '../../containers/Avatar'; import Avatar from '../../containers/Avatar';
import Status from '../../containers/Status'; import Status from '../../containers/Status';
import Touch from '../../utils/touch'; import Touch from '../../utils/touch';
import database, { safeAddListener } from '../../lib/realm';
import RocketChat from '../../lib/rocketchat'; import RocketChat from '../../lib/rocketchat';
import log from '../../utils/log'; import log from '../../utils/log';
import RoomTypeIcon from '../../containers/RoomTypeIcon'; import RoomTypeIcon from '../../containers/RoomTypeIcon';
@ -43,22 +41,38 @@ class RoomActionsView extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.mounted = false;
const room = props.navigation.getParam('room');
if (room && room.observe) {
this.roomObservable = room.observe();
this.subscription = this.roomObservable
.subscribe((changes) => {
if (this.mounted) {
this.setState({ room: changes });
} else {
this.state.room = changes;
}
});
}
this.rid = props.navigation.getParam('rid'); this.rid = props.navigation.getParam('rid');
this.t = props.navigation.getParam('t'); this.t = props.navigation.getParam('t');
this.rooms = database.objects('subscriptions').filtered('rid = $0', this.rid);
this.state = { this.state = {
room: this.rooms[0] || { rid: this.rid, t: this.t }, room: room || { rid: this.rid, t: this.t },
membersCount: 0, membersCount: 0,
member: {}, member: {},
joined: this.rooms.length > 0, joined: !!room,
canViewMembers: false, canViewMembers: false,
canAutoTranslate: false canAutoTranslate: false,
canAddUser: false
}; };
} }
async componentDidMount() { async componentDidMount() {
this.mounted = true;
const { room } = this.state; const { room } = this.state;
if (!room._id) { if (!room.id) {
try { try {
const result = await RocketChat.getChannelInfo(room.rid); const result = await RocketChat.getChannelInfo(room.rid);
if (result.success) { if (result.success) {
@ -69,7 +83,7 @@ class RoomActionsView extends React.Component {
} }
} }
if (room && room.t !== 'd' && this.canViewMembers) { if (room && room.t !== 'd' && this.canViewMembers()) {
try { try {
const counters = await RocketChat.getRoomCounters(room.rid, room.t); const counters = await RocketChat.getRoomCounters(room.rid, room.t);
if (counters.success) { if (counters.success) {
@ -82,36 +96,16 @@ class RoomActionsView extends React.Component {
this.updateRoomMember(); this.updateRoomMember();
} }
const canAutoTranslate = RocketChat.canAutoTranslate(); const canAutoTranslate = await RocketChat.canAutoTranslate();
this.setState({ canAutoTranslate }); this.setState({ canAutoTranslate });
safeAddListener(this.rooms, this.updateRoom); this.canAddUser();
}
shouldComponentUpdate(nextProps, nextState) {
const {
room, membersCount, member, joined, canViewMembers
} = this.state;
if (nextState.membersCount !== membersCount) {
return true;
}
if (nextState.joined !== joined) {
return true;
}
if (nextState.canViewMembers !== canViewMembers) {
return true;
}
if (!equal(nextState.room, room)) {
return true;
}
if (!equal(nextState.member, member)) {
return true;
}
return false;
} }
componentWillUnmount() { componentWillUnmount() {
this.rooms.removeAllListeners(); if (this.subscription && this.subscription.unsubscribe) {
this.subscription.unsubscribe();
}
} }
onPressTouchable = (item) => { onPressTouchable = (item) => {
@ -125,32 +119,37 @@ class RoomActionsView extends React.Component {
} }
// TODO: move to componentDidMount // TODO: move to componentDidMount
get canAddUser() { // eslint-disable-next-line react/sort-comp
canAddUser = async() => {
const { room, joined } = this.state; const { room, joined } = this.state;
const { rid, t } = room; const { rid, t } = room;
let canAdd = false;
const userInRoom = joined; const userInRoom = joined;
const permissions = RocketChat.hasPermission(['add-user-to-joined-room', 'add-user-to-any-c-room', 'add-user-to-any-p-room'], rid); const permissions = await RocketChat.hasPermission(['add-user-to-joined-room', 'add-user-to-any-c-room', 'add-user-to-any-p-room'], rid);
if (permissions) {
if (userInRoom && permissions['add-user-to-joined-room']) { if (userInRoom && permissions['add-user-to-joined-room']) {
return true; canAdd = true;
} }
if (t === 'c' && permissions['add-user-to-any-c-room']) { if (t === 'c' && permissions['add-user-to-any-c-room']) {
return true; canAdd = true;
} }
if (t === 'p' && permissions['add-user-to-any-p-room']) { if (t === 'p' && permissions['add-user-to-any-p-room']) {
return true; canAdd = true;
} }
return false; }
this.setState({ canAddUser: canAdd });
} }
// TODO: move to componentDidMount // TODO: move to componentDidMount
get canViewMembers() { // eslint-disable-next-line react/sort-comp
canViewMembers = async() => {
const { room } = this.state; const { room } = this.state;
const { rid, t, broadcast } = room; const { rid, t, broadcast } = room;
if (broadcast) { if (broadcast) {
const viewBroadcastMemberListPermission = 'view-broadcast-member-list'; const viewBroadcastMemberListPermission = 'view-broadcast-member-list';
const permissions = RocketChat.hasPermission([viewBroadcastMemberListPermission], rid); const permissions = await RocketChat.hasPermission([viewBroadcastMemberListPermission], rid);
if (!permissions[viewBroadcastMemberListPermission]) { if (!permissions[viewBroadcastMemberListPermission]) {
return false; return false;
} }
@ -165,7 +164,7 @@ class RoomActionsView extends React.Component {
get sections() { get sections() {
const { const {
room, membersCount, canViewMembers, joined, canAutoTranslate room, membersCount, canViewMembers, canAddUser, joined, canAutoTranslate
} = this.state; } = this.state;
const { const {
rid, t, blocker rid, t, blocker
@ -175,7 +174,7 @@ class RoomActionsView extends React.Component {
icon: 'bell', icon: 'bell',
name: I18n.t('Notifications'), name: I18n.t('Notifications'),
route: 'NotificationPrefView', route: 'NotificationPrefView',
params: { rid }, params: { rid, room },
testID: 'room-actions-notifications' testID: 'room-actions-notifications'
}; };
@ -185,7 +184,7 @@ class RoomActionsView extends React.Component {
name: I18n.t('Room_Info'), name: I18n.t('Room_Info'),
route: 'RoomInfoView', route: 'RoomInfoView',
// forward room only if room isn't joined // forward room only if room isn't joined
params: { rid, t }, params: { rid, t, room },
testID: 'room-actions-info' testID: 'room-actions-info'
}], }],
renderItem: this.renderRoomInfo renderItem: this.renderRoomInfo
@ -257,7 +256,7 @@ class RoomActionsView extends React.Component {
icon: 'language', icon: 'language',
name: I18n.t('Auto_Translate'), name: I18n.t('Auto_Translate'),
route: 'AutoTranslateView', route: 'AutoTranslateView',
params: { rid }, params: { rid, room },
testID: 'room-actions-auto-translate' testID: 'room-actions-auto-translate'
}); });
} }
@ -285,12 +284,12 @@ class RoomActionsView extends React.Component {
name: I18n.t('Members'), name: I18n.t('Members'),
description: membersCount > 0 ? `${ membersCount } ${ I18n.t('members') }` : null, description: membersCount > 0 ? `${ membersCount } ${ I18n.t('members') }` : null,
route: 'RoomMembersView', route: 'RoomMembersView',
params: { rid }, params: { rid, room },
testID: 'room-actions-members' testID: 'room-actions-members'
}); });
} }
if (this.canAddUser) { if (canAddUser) {
actions.push({ actions.push({
icon: 'user-plus', icon: 'user-plus',
name: I18n.t('Add_user'), name: I18n.t('Add_user'),
@ -324,12 +323,6 @@ class RoomActionsView extends React.Component {
return sections; return sections;
} }
updateRoom = () => {
if (this.rooms.length > 0) {
this.setState({ room: JSON.parse(JSON.stringify(this.rooms[0])) });
}
}
updateRoomMember = async() => { updateRoomMember = async() => {
const { room } = this.state; const { room } = this.state;
const { rid } = room; const { rid } = room;

View File

@ -7,6 +7,7 @@ import { connect } from 'react-redux';
import { SafeAreaView } from 'react-navigation'; import { SafeAreaView } from 'react-navigation';
import equal from 'deep-equal'; import equal from 'deep-equal';
import database from '../../lib/database';
import { eraseRoom as eraseRoomAction } from '../../actions/room'; import { eraseRoom as eraseRoomAction } from '../../actions/room';
import KeyboardView from '../../presentation/KeyboardView'; import KeyboardView from '../../presentation/KeyboardView';
import sharedStyles from '../Styles'; import sharedStyles from '../Styles';
@ -15,7 +16,6 @@ import scrollPersistTaps from '../../utils/scrollPersistTaps';
import { showErrorAlert } from '../../utils/info'; import { showErrorAlert } from '../../utils/info';
import { LISTENER } from '../../containers/Toast'; import { LISTENER } from '../../containers/Toast';
import EventEmitter from '../../utils/events'; import EventEmitter from '../../utils/events';
import database, { safeAddListener } from '../../lib/realm';
import RocketChat from '../../lib/rocketchat'; import RocketChat from '../../lib/rocketchat';
import RCTextInput from '../../containers/TextInput'; import RCTextInput from '../../containers/TextInput';
import Loading from '../../containers/Loading'; import Loading from '../../containers/Loading';
@ -52,11 +52,9 @@ class RoomInfoEditView extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
const rid = props.navigation.getParam('rid');
this.rooms = database.objects('subscriptions').filtered('rid = $0', rid);
this.permissions = {};
this.state = { this.state = {
room: JSON.parse(JSON.stringify(this.rooms[0] || {})), room: {},
permissions: {},
name: '', name: '',
description: '', description: '',
topic: '', topic: '',
@ -66,27 +64,16 @@ class RoomInfoEditView extends React.Component {
saving: false, saving: false,
t: false, t: false,
ro: false, ro: false,
reactWhenReadOnly: false reactWhenReadOnly: false,
archived: false
}; };
} this.loadRoom();
componentDidMount() {
this.updateRoom();
this.init();
safeAddListener(this.rooms, this.updateRoom);
const { room } = this.state;
this.permissions = RocketChat.hasPermission(PERMISSIONS_ARRAY, room.rid);
} }
shouldComponentUpdate(nextProps, nextState) { shouldComponentUpdate(nextProps, nextState) {
const { room } = this.state;
if (!equal(nextState, this.state)) { if (!equal(nextState, this.state)) {
return true; return true;
} }
if (!equal(nextState.room, room)) {
return true;
}
if (!equal(nextProps, this.props)) { if (!equal(nextProps, this.props)) {
return true; return true;
} }
@ -94,21 +81,43 @@ class RoomInfoEditView extends React.Component {
} }
componentWillUnmount() { componentWillUnmount() {
this.rooms.removeAllListeners(); if (this.querySubscription && this.querySubscription.unsubscribe) {
this.querySubscription.unsubscribe();
}
} }
updateRoom = () => { // eslint-disable-next-line react/sort-comp
this.setState({ room: JSON.parse(JSON.stringify(this.rooms[0] || {})) }); loadRoom = async() => {
const { navigation } = this.props;
const rid = navigation.getParam('rid', null);
if (!rid) {
return;
}
try {
const db = database.active;
const sub = await db.collections.get('subscriptions').find(rid);
const observable = sub.observe();
this.querySubscription = observable.subscribe((data) => {
this.room = data;
this.init(this.room);
});
const permissions = await RocketChat.hasPermission(PERMISSIONS_ARRAY, rid);
this.setState({ permissions });
} catch (e) {
log(e);
}
} }
init = () => { init = (room) => {
const { room } = this.state;
const { const {
name, description, topic, announcement, t, ro, reactWhenReadOnly, joinCodeRequired name, description, topic, announcement, t, ro, reactWhenReadOnly, joinCodeRequired
} = room; } = room;
// fake password just to user knows about it // fake password just to user knows about it
this.randomValue = random(15); this.randomValue = random(15);
this.setState({ this.setState({
room,
name, name,
description, description,
topic, topic,
@ -116,7 +125,8 @@ class RoomInfoEditView extends React.Component {
t: t === 'p', t: t === 'p',
ro, ro,
reactWhenReadOnly, reactWhenReadOnly,
joinCode: joinCodeRequired ? this.randomValue : '' joinCode: joinCodeRequired ? this.randomValue : '',
archived: room.archived
}); });
} }
@ -128,7 +138,7 @@ class RoomInfoEditView extends React.Component {
reset = () => { reset = () => {
this.clearErrors(); this.clearErrors();
this.init(); this.init(this.room);
} }
formIsChanged = () => { formIsChanged = () => {
@ -271,19 +281,20 @@ class RoomInfoEditView extends React.Component {
} }
hasDeletePermission = () => { hasDeletePermission = () => {
const { room } = this.state; const { room, permissions } = this.state;
return ( return (
room.t === 'p' ? this.permissions[PERMISSION_DELETE_P] : this.permissions[PERMISSION_DELETE_C] room.t === 'p' ? permissions[PERMISSION_DELETE_P] : permissions[PERMISSION_DELETE_C]
); );
} }
hasArchivePermission = () => ( hasArchivePermission = () => {
this.permissions[PERMISSION_ARCHIVE] || this.permissions[PERMISSION_UNARCHIVE] const { permissions } = this.state;
); return (permissions[PERMISSION_ARCHIVE] || permissions[PERMISSION_UNARCHIVE]);
};
render() { render() {
const { const {
name, nameError, description, topic, announcement, t, ro, reactWhenReadOnly, room, joinCode, saving name, nameError, description, topic, announcement, t, ro, reactWhenReadOnly, room, joinCode, saving, permissions, archived
} = this.state; } = this.state;
return ( return (
<KeyboardView <KeyboardView
@ -355,7 +366,7 @@ class RoomInfoEditView extends React.Component {
rightLabelPrimary={I18n.t('Read_Only')} rightLabelPrimary={I18n.t('Read_Only')}
rightLabelSecondary={I18n.t('Only_authorized_users_can_write_new_messages')} rightLabelSecondary={I18n.t('Only_authorized_users_can_write_new_messages')}
onValueChange={value => this.setState({ ro: value })} onValueChange={value => this.setState({ ro: value })}
disabled={!this.permissions[PERMISSION_SET_READONLY] || room.broadcast} disabled={!permissions[PERMISSION_SET_READONLY] || room.broadcast}
testID='room-info-edit-view-ro' testID='room-info-edit-view-ro'
/> />
{ro && !room.broadcast {ro && !room.broadcast
@ -367,7 +378,7 @@ class RoomInfoEditView extends React.Component {
rightLabelPrimary={I18n.t('Allow_Reactions')} rightLabelPrimary={I18n.t('Allow_Reactions')}
rightLabelSecondary={I18n.t('Reactions_are_enabled')} rightLabelSecondary={I18n.t('Reactions_are_enabled')}
onValueChange={value => this.setState({ reactWhenReadOnly: value })} onValueChange={value => this.setState({ reactWhenReadOnly: value })}
disabled={!this.permissions[PERMISSION_SET_REACT_WHEN_READONLY]} disabled={!permissions[PERMISSION_SET_REACT_WHEN_READONLY]}
testID='room-info-edit-view-react-when-ro' testID='room-info-edit-view-react-when-ro'
/> />
) )
@ -408,7 +419,7 @@ class RoomInfoEditView extends React.Component {
testID='room-info-edit-view-archive' testID='room-info-edit-view-archive'
> >
<Text style={[sharedStyles.button_inverted, styles.colorDanger]} accessibilityTraits='button'> <Text style={[sharedStyles.button_inverted, styles.colorDanger]} accessibilityTraits='button'>
{ room.archived ? I18n.t('UNARCHIVE') : I18n.t('ARCHIVE') } { archived ? I18n.t('UNARCHIVE') : I18n.t('ARCHIVE') }
</Text> </Text>
</TouchableOpacity> </TouchableOpacity>
</View> </View>

View File

@ -9,7 +9,7 @@ import Status from '../../containers/Status';
import Avatar from '../../containers/Avatar'; import Avatar from '../../containers/Avatar';
import styles from './styles'; import styles from './styles';
import sharedStyles from '../Styles'; import sharedStyles from '../Styles';
import database, { safeAddListener } from '../../lib/realm'; import database from '../../lib/database';
import RocketChat from '../../lib/rocketchat'; import RocketChat from '../../lib/rocketchat';
import RoomTypeIcon from '../../containers/RoomTypeIcon'; import RoomTypeIcon from '../../containers/RoomTypeIcon';
import I18n from '../../i18n'; import I18n from '../../i18n';
@ -58,15 +58,13 @@ class RoomInfoView extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
const room = props.navigation.getParam('room');
this.rid = props.navigation.getParam('rid'); this.rid = props.navigation.getParam('rid');
this.t = props.navigation.getParam('t'); this.t = props.navigation.getParam('t');
this.roles = database.objects('roles');
this.sub = {
unsubscribe: () => {}
};
this.state = { this.state = {
room: {}, room: room || {},
roomUser: {} roomUser: {},
parsedRoles: []
}; };
} }
@ -77,19 +75,29 @@ class RoomInfoView extends React.Component {
try { try {
const result = await RocketChat.getUserInfo(roomUserId); const result = await RocketChat.getUserInfo(roomUserId);
if (result.success) { if (result.success) {
this.setState({ roomUser: result.user }); const { roles } = result.user;
let parsedRoles = [];
if (roles && roles.length) {
parsedRoles = await Promise.all(roles.map(async(role) => {
const description = await this.getRoleDescription(role);
return description;
}));
}
this.setState({ roomUser: result.user, parsedRoles });
} }
} catch (e) { } catch (e) {
log(e); log(e);
} }
return; return;
} }
this.rooms = database.objects('subscriptions').filtered('rid = $0', this.rid); const { navigation } = this.props;
safeAddListener(this.rooms, this.updateRoom); let room = navigation.getParam('room');
let room = {}; if (room && room.observe) {
if (this.rooms.length > 0) { this.roomObservable = room.observe();
this.setState({ room: this.rooms[0] }); this.subscription = this.roomObservable
[room] = this.rooms; .subscribe((changes) => {
this.setState({ room: changes });
});
} else { } else {
try { try {
const result = await RocketChat.getRoomInfo(this.rid); const result = await RocketChat.getRoomInfo(this.rid);
@ -102,29 +110,34 @@ class RoomInfoView extends React.Component {
log(e); log(e);
} }
} }
const permissions = RocketChat.hasPermission([PERMISSION_EDIT_ROOM], room.rid); const permissions = await RocketChat.hasPermission([PERMISSION_EDIT_ROOM], room.rid);
if (permissions[PERMISSION_EDIT_ROOM] && !room.prid) { if (permissions[PERMISSION_EDIT_ROOM] && !room.prid) {
const { navigation } = this.props;
navigation.setParams({ showEdit: true }); navigation.setParams({ showEdit: true });
} }
} }
getRoleDescription = (id) => { componentWillUnmount() {
const role = database.objectForPrimaryKey('roles', id); if (this.subscription && this.subscription.unsubscribe) {
this.subscription.unsubscribe();
}
}
getRoleDescription = async(id) => {
const db = database.active;
try {
const rolesCollection = db.collections.get('roles');
const role = await rolesCollection.find(id);
if (role) { if (role) {
return role.description; return role.description;
} }
return null; return null;
} catch (e) {
return null;
}
} }
isDirect = () => this.t === 'd' isDirect = () => this.t === 'd'
updateRoom = () => {
if (this.rooms.length > 0) {
this.setState({ room: JSON.parse(JSON.stringify(this.rooms[0])) });
}
}
renderItem = (key, room) => ( renderItem = (key, room) => (
<View style={styles.item}> <View style={styles.item}>
<Text style={styles.itemLabel}>{I18n.t(camelize(key))}</Text> <Text style={styles.itemLabel}>{I18n.t(camelize(key))}</Text>
@ -136,12 +149,11 @@ class RoomInfoView extends React.Component {
</View> </View>
); );
renderRole = (role) => { renderRole = (description) => {
const description = this.getRoleDescription(role);
if (description) { if (description) {
return ( return (
<View style={styles.roleBadge} key={role}> <View style={styles.roleBadge} key={description}>
<Text style={styles.role}>{ this.getRoleDescription(role) }</Text> <Text style={styles.role}>{ description }</Text>
</View> </View>
); );
} }
@ -149,13 +161,13 @@ class RoomInfoView extends React.Component {
} }
renderRoles = () => { renderRoles = () => {
const { roomUser } = this.state; const { parsedRoles } = this.state;
if (roomUser && roomUser.roles && roomUser.roles.length) { if (parsedRoles && parsedRoles.length) {
return ( return (
<View style={styles.item}> <View style={styles.item}>
<Text style={styles.itemLabel}>{I18n.t('Roles')}</Text> <Text style={styles.itemLabel}>{I18n.t('Roles')}</Text>
<View style={styles.rolesContainer}> <View style={styles.rolesContainer}>
{roomUser.roles.map(role => this.renderRole(role))} {parsedRoles.map(role => this.renderRole(role))}
</View> </View>
</View> </View>
); );

View File

@ -4,14 +4,14 @@ import { FlatList, View, ActivityIndicator } from 'react-native';
import ActionSheet from 'react-native-action-sheet'; import ActionSheet from 'react-native-action-sheet';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { SafeAreaView } from 'react-navigation'; import { SafeAreaView } from 'react-navigation';
import equal from 'deep-equal';
import * as Haptics from 'expo-haptics'; import * as Haptics from 'expo-haptics';
import { Q } from '@nozbe/watermelondb';
import styles from './styles'; import styles from './styles';
import UserItem from '../../presentation/UserItem'; import UserItem from '../../presentation/UserItem';
import scrollPersistTaps from '../../utils/scrollPersistTaps'; import scrollPersistTaps from '../../utils/scrollPersistTaps';
import RocketChat from '../../lib/rocketchat'; import RocketChat from '../../lib/rocketchat';
import database, { safeAddListener } from '../../lib/realm'; import database from '../../lib/database';
import { LISTENER } from '../../containers/Toast'; import { LISTENER } from '../../containers/Toast';
import EventEmitter from '../../utils/events'; import EventEmitter from '../../utils/events';
import log from '../../utils/log'; import log from '../../utils/log';
@ -52,13 +52,25 @@ class RoomMembersView extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.mounted = false;
this.CANCEL_INDEX = 0; this.CANCEL_INDEX = 0;
this.MUTE_INDEX = 1; this.MUTE_INDEX = 1;
this.actionSheetOptions = ['']; this.actionSheetOptions = [''];
const { rid } = props.navigation.state.params; const { rid } = props.navigation.state.params;
this.rooms = database.objects('subscriptions').filtered('rid = $0', rid);
this.permissions = RocketChat.hasPermission(['mute-user'], rid); const room = props.navigation.getParam('room');
if (room && room.observe) {
this.roomObservable = room.observe();
this.subscription = this.roomObservable
.subscribe((changes) => {
if (this.mounted) {
this.setState({ room: changes });
} else {
this.state.room = changes;
}
});
}
this.state = { this.state = {
isLoading: false, isLoading: false,
allUsers: false, allUsers: false,
@ -67,53 +79,25 @@ class RoomMembersView extends React.Component {
members: [], members: [],
membersFiltered: [], membersFiltered: [],
userLongPressed: {}, userLongPressed: {},
room: this.rooms[0] || {}, room: room || {},
options: [],
end: false end: false
}; };
} }
componentDidMount() { async componentDidMount() {
this.mounted = true;
this.fetchMembers(); this.fetchMembers();
safeAddListener(this.rooms, this.updateRoom);
const { navigation } = this.props; const { navigation } = this.props;
const { rid } = navigation.state.params;
navigation.setParams({ toggleStatus: this.toggleStatus }); navigation.setParams({ toggleStatus: this.toggleStatus });
} this.permissions = await RocketChat.hasPermission(['mute-user'], rid);
shouldComponentUpdate(nextProps, nextState) {
const {
allUsers, filtering, members, membersFiltered, userLongPressed, room, options, isLoading
} = this.state;
if (nextState.allUsers !== allUsers) {
return true;
}
if (nextState.filtering !== filtering) {
return true;
}
if (!equal(nextState.members, members)) {
return true;
}
if (!equal(nextState.options, options)) {
return true;
}
if (!equal(nextState.membersFiltered, membersFiltered)) {
return true;
}
if (!equal(nextState.userLongPressed, userLongPressed)) {
return true;
}
if (!equal(nextState.room.muted, room.muted)) {
return true;
}
if (isLoading !== nextState.isLoading) {
return true;
}
return false;
} }
componentWillUnmount() { componentWillUnmount() {
this.rooms.removeAllListeners(); if (this.subscription && this.subscription.unsubscribe) {
this.subscription.unsubscribe();
}
} }
onSearchChangeText = protectedFunction((text) => { onSearchChangeText = protectedFunction((text) => {
@ -128,9 +112,12 @@ class RoomMembersView extends React.Component {
onPressUser = async(item) => { onPressUser = async(item) => {
try { try {
const subscriptions = database.objects('subscriptions').filtered('name = $0', item.username); const db = database.active;
if (subscriptions.length) { const subsCollection = db.collections.get('subscriptions');
this.goRoom({ rid: subscriptions[0].rid, name: item.username }); const query = await subsCollection.query(Q.where('name', item.username)).fetch();
if (query) {
const [room] = query;
this.goRoom({ rid: room.rid, name: item.username, room });
} else { } else {
const result = await RocketChat.createDirectMessage(item.username); const result = await RocketChat.createDirectMessage(item.username);
if (result.success) { if (result.success) {
@ -150,7 +137,7 @@ class RoomMembersView extends React.Component {
const { muted } = room; const { muted } = room;
this.actionSheetOptions = [I18n.t('Cancel')]; this.actionSheetOptions = [I18n.t('Cancel')];
const userIsMuted = !!muted.find(m => m === user.username); const userIsMuted = !!(muted || []).find(m => m === user.username);
user.muted = userIsMuted; user.muted = userIsMuted;
if (userIsMuted) { if (userIsMuted) {
this.actionSheetOptions.push(I18n.t('Unmute')); this.actionSheetOptions.push(I18n.t('Unmute'));
@ -209,17 +196,12 @@ class RoomMembersView extends React.Component {
} }
} }
updateRoom = () => { goRoom = async({ rid, name, room }) => {
if (this.rooms.length > 0) {
const [room] = this.rooms;
this.setState({ room });
}
}
goRoom = async({ rid, name }) => {
const { navigation } = this.props; const { navigation } = this.props;
await navigation.popToTop(); await navigation.popToTop();
navigation.navigate('RoomView', { rid, name, t: 'd' }); navigation.navigate('RoomView', {
rid, name, t: 'd', room
});
} }
handleMute = async() => { handleMute = async() => {

View File

@ -10,14 +10,15 @@ const styles = StyleSheet.create({
} }
}); });
const EmptyRoom = React.memo(({ length }) => { const EmptyRoom = React.memo(({ length, mounted }) => {
if (length === 0) { if (length === 0 && mounted) {
return <ImageBackground source={{ uri: 'message_empty' }} style={styles.image} />; return <ImageBackground source={{ uri: 'message_empty' }} style={styles.image} />;
} }
return null; return null;
}); });
EmptyRoom.propTypes = { EmptyRoom.propTypes = {
length: PropTypes.number.isRequired length: PropTypes.number.isRequired,
mounted: PropTypes.bool
}; };
export default EmptyRoom; export default EmptyRoom;

View File

@ -47,19 +47,18 @@ const styles = StyleSheet.create({
}); });
const Typing = React.memo(({ usersTyping }) => { const Typing = React.memo(({ usersTyping }) => {
const users = usersTyping.map(item => item.username);
let usersText; let usersText;
if (!users.length) { if (!usersTyping.length) {
return null; return null;
} else if (users.length === 2) { } else if (usersTyping.length === 2) {
usersText = users.join(` ${ I18n.t('and') } `); usersText = usersTyping.join(` ${ I18n.t('and') } `);
} else { } else {
usersText = users.join(', '); usersText = usersTyping.join(', ');
} }
return ( return (
<Text style={styles.typing} numberOfLines={1}> <Text style={styles.typing} numberOfLines={1}>
<Text style={styles.typingUsers}>{usersText} </Text> <Text style={styles.typingUsers}>{usersText} </Text>
{ users.length > 1 ? I18n.t('are_typing') : I18n.t('is_typing') }... { usersTyping.length > 1 ? I18n.t('are_typing') : I18n.t('is_typing') }...
</Text> </Text>
); );
}); });

View File

@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { CustomHeaderButtons, Item } from '../../../containers/HeaderButton'; import { CustomHeaderButtons, Item } from '../../../containers/HeaderButton';
import database, { safeAddListener } from '../../../lib/realm'; import database from '../../../lib/database';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
more: { more: {
@ -27,29 +27,33 @@ class RightButtonsContainer extends React.PureComponent {
t: PropTypes.string, t: PropTypes.string,
tmid: PropTypes.string, tmid: PropTypes.string,
navigation: PropTypes.object, navigation: PropTypes.object,
toggleFollowThread: PropTypes.func toggleFollowThread: PropTypes.func,
room: PropTypes.object
}; };
constructor(props) { constructor(props) {
super(props); super(props);
if (props.tmid) {
// FIXME: it may be empty if the thread header isn't fetched yet
this.thread = database.objectForPrimaryKey('messages', props.tmid);
}
this.state = { this.state = {
isFollowingThread: true isFollowingThread: true
}; };
} }
componentDidMount() { async componentDidMount() {
if (this.thread) { const { tmid, userId } = this.props;
safeAddListener(this.thread, this.updateThread); if (tmid) {
const db = database.active;
const threadObservable = await db.collections.get('messages').findAndObserve(tmid);
this.threadSubscription = threadObservable.subscribe((thread) => {
this.setState({
isFollowingThread: thread.replies && !!thread.replies.find(t => t === userId)
});
});
} }
} }
componentWillUnmount() { componentWillUnmount() {
if (this.thread && this.thread.removeAllListeners) { if (this.threadSubscription && this.threadSubscription.unsubscribe) {
this.thread.removeAllListeners(); this.threadSubscription.unsubscribe();
} }
} }
@ -66,8 +70,10 @@ class RightButtonsContainer extends React.PureComponent {
} }
goRoomActionsView = () => { goRoomActionsView = () => {
const { rid, t, navigation } = this.props; const {
navigation.navigate('RoomActionsView', { rid, t }); rid, t, navigation, room
} = this.props;
navigation.navigate('RoomActionsView', { rid, t, room });
} }
toggleFollowThread = () => { toggleFollowThread = () => {

View File

@ -4,7 +4,6 @@ import { connect } from 'react-redux';
import { responsive } from 'react-native-responsive-ui'; import { responsive } from 'react-native-responsive-ui';
import equal from 'deep-equal'; import equal from 'deep-equal';
import database, { safeAddListener } from '../../../lib/realm';
import Header from './Header'; import Header from './Header';
import RightButtons from './RightButtons'; import RightButtons from './RightButtons';
@ -14,32 +13,15 @@ class RoomHeaderView extends Component {
type: PropTypes.string, type: PropTypes.string,
prid: PropTypes.string, prid: PropTypes.string,
tmid: PropTypes.string, tmid: PropTypes.string,
rid: PropTypes.string, usersTyping: PropTypes.string,
window: PropTypes.object, window: PropTypes.object,
status: PropTypes.string, status: PropTypes.string,
connecting: PropTypes.bool, connecting: PropTypes.bool,
widthOffset: PropTypes.number, widthOffset: PropTypes.number
isLoggedUser: PropTypes.bool,
userId: PropTypes.string
}; };
constructor(props) { shouldComponentUpdate(nextProps) {
super(props); const { usersTyping } = this.props;
this.usersTyping = database.memoryDatabase.objects('usersTyping').filtered('rid = $0', props.rid);
this.user = [];
if (props.type === 'd' && !props.isLoggedUser) {
this.user = database.memoryDatabase.objects('activeUsers').filtered('id == $0', props.userId);
safeAddListener(this.user, this.updateUser);
}
this.state = {
usersTyping: this.usersTyping.slice() || [],
user: this.user[0] || {}
};
this.usersTyping.addListener(this.updateState);
}
shouldComponentUpdate(nextProps, nextState) {
const { usersTyping, user } = this.state;
const { const {
type, title, status, window, connecting type, title, status, window, connecting
} = this.props; } = this.props;
@ -61,46 +43,16 @@ class RoomHeaderView extends Component {
if (nextProps.window.height !== window.height) { if (nextProps.window.height !== window.height) {
return true; return true;
} }
if (!equal(nextState.usersTyping, usersTyping)) { if (!equal(nextProps.usersTyping, usersTyping)) {
return true;
}
if (!equal(nextState.user, user)) {
return true; return true;
} }
return false; return false;
} }
componentWillUnmount() {
this.usersTyping.removeAllListeners();
if (this.user && this.user.removeAllListeners) {
this.user.removeAllListeners();
}
}
updateState = () => {
this.setState({ usersTyping: this.usersTyping.slice() });
}
updateUser = () => {
if (this.user.length) {
this.setState({ user: this.user[0] });
}
}
render() { render() {
const { usersTyping, user } = this.state;
const { const {
window, title, type, prid, tmid, widthOffset, isLoggedUser, status: userStatus, connecting window, title, type, prid, tmid, widthOffset, status = 'offline', connecting, usersTyping
} = this.props; } = this.props;
let status = 'offline';
if (type === 'd') {
if (isLoggedUser) {
status = userStatus;
} else {
status = user.status || 'offline';
}
}
return ( return (
<Header <Header
@ -121,24 +73,18 @@ class RoomHeaderView extends Component {
const mapStateToProps = (state, ownProps) => { const mapStateToProps = (state, ownProps) => {
let status; let status;
let userId;
let isLoggedUser = false;
const { rid, type } = ownProps; const { rid, type } = ownProps;
if (type === 'd') { if (type === 'd') {
if (state.login.user && state.login.user.id) { if (state.login.user && state.login.user.id) {
const { id: loggedUserId } = state.login.user; const { id: loggedUserId } = state.login.user;
userId = rid.replace(loggedUserId, '').trim(); const userId = rid.replace(loggedUserId, '').trim();
isLoggedUser = userId === loggedUserId; status = state.activeUsers[userId];
if (isLoggedUser) {
status = state.login.user.status; // eslint-disable-line
}
} }
} }
return { return {
connecting: state.meteor.connecting, connecting: state.meteor.connecting,
userId, usersTyping: state.usersTyping,
isLoggedUser,
status status
}; };
}; };

View File

@ -1,88 +1,134 @@
import React from 'react'; import React from 'react';
import { ActivityIndicator, FlatList, InteractionManager } from 'react-native'; import {
ActivityIndicator, FlatList, InteractionManager, LayoutAnimation
} from 'react-native';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import debounce from 'lodash/debounce'; import debounce from 'lodash/debounce';
import orderBy from 'lodash/orderBy';
import { Q } from '@nozbe/watermelondb';
import isEqual from 'lodash/isEqual';
import styles from './styles'; import styles from './styles';
import database, { safeAddListener } from '../../lib/realm'; import database from '../../lib/database';
import scrollPersistTaps from '../../utils/scrollPersistTaps'; import scrollPersistTaps from '../../utils/scrollPersistTaps';
import RocketChat from '../../lib/rocketchat'; import RocketChat from '../../lib/rocketchat';
import log from '../../utils/log'; import log from '../../utils/log';
import EmptyRoom from './EmptyRoom'; import EmptyRoom from './EmptyRoom';
import { isIOS } from '../../utils/deviceInfo';
export class List extends React.PureComponent { export class List extends React.Component {
static propTypes = { static propTypes = {
onEndReached: PropTypes.func, onEndReached: PropTypes.func,
renderFooter: PropTypes.func, renderFooter: PropTypes.func,
renderRow: PropTypes.func, renderRow: PropTypes.func,
rid: PropTypes.string, rid: PropTypes.string,
t: PropTypes.string, t: PropTypes.string,
tmid: PropTypes.string tmid: PropTypes.string,
animated: PropTypes.bool
}; };
constructor(props) { constructor(props) {
super(props); super(props);
console.time(`${ this.constructor.name } init`); console.time(`${ this.constructor.name } init`);
console.time(`${ this.constructor.name } mount`); console.time(`${ this.constructor.name } mount`);
if (props.tmid) {
this.data = database
.objects('threadMessages')
.filtered('rid = $0', props.tmid)
.sorted('ts', true);
this.threads = database.objects('threads').filtered('_id = $0', props.tmid);
} else {
this.data = database
.objects('messages')
.filtered('rid = $0', props.rid)
.sorted('ts', true);
this.threads = database.objects('threads').filtered('rid = $0', props.rid);
}
this.mounted = false;
this.state = { this.state = {
loading: true, loading: true,
end: false, end: false,
messages: this.data.slice(), messages: []
threads: this.threads.slice()
}; };
this.init();
safeAddListener(this.data, this.updateState);
console.timeEnd(`${ this.constructor.name } init`); console.timeEnd(`${ this.constructor.name } init`);
} }
componentDidMount() { componentDidMount() {
this.mounted = true;
console.timeEnd(`${ this.constructor.name } mount`); console.timeEnd(`${ this.constructor.name } mount`);
} }
componentWillUnmount() { // eslint-disable-next-line react/sort-comp
this.data.removeAllListeners(); async init() {
this.threads.removeAllListeners(); const { rid, tmid } = this.props;
if (this.updateState && this.updateState.stop) { const db = database.active;
this.updateState.stop();
if (tmid) {
try {
this.thread = await db.collections
.get('threads')
.find(tmid);
} catch (e) {
console.log(e);
} }
if (this.interactionManagerState && this.interactionManagerState.cancel) { this.messagesObservable = db.collections
this.interactionManagerState.cancel(); .get('thread_messages')
.query(
Q.where('rid', tmid)
)
.observeWithColumns(['_updated_at']);
} else {
this.messagesObservable = db.collections
.get('messages')
.query(
Q.where('rid', rid)
)
.observeWithColumns(['_updated_at']);
}
this.messagesSubscription = this.messagesObservable
.subscribe((data) => {
this.interaction = InteractionManager.runAfterInteractions(() => {
if (tmid) {
data = [this.thread, ...data];
}
const messages = orderBy(data, ['ts'], ['desc']);
if (this.mounted) {
LayoutAnimation.easeInEaseOut();
this.setState({ messages });
} else {
this.state.messages = messages;
}
});
});
}
// this.state.loading works for this.onEndReached and RoomView.init
static getDerivedStateFromProps(props, state) {
if (props.loading !== state.loading) {
return {
loading: props.loading
};
}
return null;
}
shouldComponentUpdate(nextProps, nextState) {
const { messages, loading, end } = this.state;
if (loading !== nextState.loading) {
return true;
}
if (end !== nextState.end) {
return true;
}
if (!isEqual(messages, nextState.messages)) {
return true;
}
return false;
}
componentWillUnmount() {
if (this.messagesSubscription && this.messagesSubscription.unsubscribe) {
this.messagesSubscription.unsubscribe();
}
if (this.interaction && this.interaction.cancel) {
this.interaction.cancel();
}
if (this.onEndReached && this.onEndReached.stop) {
this.onEndReached.stop();
} }
console.countReset(`${ this.constructor.name }.render calls`); console.countReset(`${ this.constructor.name }.render calls`);
} }
// eslint-disable-next-line react/sort-comp
updateState = debounce(() => {
this.interactionManagerState = InteractionManager.runAfterInteractions(() => {
const { tmid } = this.props;
let messages = this.data;
if (tmid && this.threads[0]) {
const thread = { ...this.threads[0] };
thread.tlm = null;
messages = [...messages, thread];
}
this.setState({
messages: messages.slice(),
threads: this.threads.slice(),
loading: false
});
});
}, 300, { leading: true });
onEndReached = debounce(async() => { onEndReached = debounce(async() => {
const { const {
loading, end, messages loading, end, messages
@ -97,7 +143,7 @@ export class List extends React.PureComponent {
let result; let result;
if (tmid) { if (tmid) {
// `offset` is `messages.length - 1` because we append thread start to `messages` obj // `offset` is `messages.length - 1` because we append thread start to `messages` obj
result = await RocketChat.loadThreadMessages({ tmid, offset: messages.length - 1 }); result = await RocketChat.loadThreadMessages({ tmid, rid, offset: messages.length - 1 });
} else { } else {
result = await RocketChat.loadMessagesForRoom({ rid, t, latest: messages[messages.length - 1].ts }); result = await RocketChat.loadMessagesForRoom({ rid, t, latest: messages[messages.length - 1].ts });
} }
@ -118,15 +164,8 @@ export class List extends React.PureComponent {
} }
renderItem = ({ item, index }) => { renderItem = ({ item, index }) => {
const { messages, threads } = this.state; const { messages } = this.state;
const { renderRow } = this.props; const { renderRow } = this.props;
if (item.tmid) {
const thread = threads.find(t => t._id === item.tmid);
if (thread) {
const tmsg = thread.msg || (thread.attachments && thread.attachments.length && thread.attachments[0].title);
item = { ...item, tmsg };
}
}
return renderRow(item, messages[index + 1]); return renderRow(item, messages[index + 1]);
} }
@ -134,19 +173,19 @@ export class List extends React.PureComponent {
console.count(`${ this.constructor.name }.render calls`); console.count(`${ this.constructor.name }.render calls`);
const { messages } = this.state; const { messages } = this.state;
return ( return (
<React.Fragment> <>
<EmptyRoom length={messages.length} /> <EmptyRoom length={messages.length} mounted={this.mounted} />
<FlatList <FlatList
testID='room-view-messages' testID='room-view-messages'
ref={ref => this.list = ref} ref={ref => this.list = ref}
keyExtractor={item => item._id} keyExtractor={item => item.id}
data={messages} data={messages}
extraData={this.state} extraData={this.state}
renderItem={this.renderItem} renderItem={this.renderItem}
contentContainerStyle={styles.contentContainer} contentContainerStyle={styles.contentContainer}
style={styles.list} style={styles.list}
inverted inverted
removeClippedSubviews removeClippedSubviews={isIOS}
initialNumToRender={7} initialNumToRender={7}
onEndReached={this.onEndReached} onEndReached={this.onEndReached}
onEndReachedThreshold={5} onEndReachedThreshold={5}
@ -155,7 +194,7 @@ export class List extends React.PureComponent {
ListFooterComponent={this.renderFooter} ListFooterComponent={this.renderFooter}
{...scrollPersistTaps} {...scrollPersistTaps}
/> />
</React.Fragment> </>
); );
} }
} }

View File

@ -6,7 +6,6 @@ import Modal from 'react-native-modal';
import { responsive } from 'react-native-responsive-ui'; import { responsive } from 'react-native-responsive-ui';
import EmojiPicker from '../../containers/EmojiPicker'; import EmojiPicker from '../../containers/EmojiPicker';
import { toggleReactionPicker as toggleReactionPickerAction } from '../../actions/messages';
import styles from './styles'; import styles from './styles';
import { isAndroid } from '../../utils/deviceInfo'; import { isAndroid } from '../../utils/deviceInfo';
@ -17,36 +16,37 @@ class ReactionPicker extends React.Component {
static propTypes = { static propTypes = {
baseUrl: PropTypes.string.isRequired, baseUrl: PropTypes.string.isRequired,
window: PropTypes.any, window: PropTypes.any,
showReactionPicker: PropTypes.bool, message: PropTypes.object,
toggleReactionPicker: PropTypes.func, show: PropTypes.bool,
reactionClose: PropTypes.func,
onEmojiSelected: PropTypes.func onEmojiSelected: PropTypes.func
}; };
shouldComponentUpdate(nextProps) { shouldComponentUpdate(nextProps) {
const { showReactionPicker, window } = this.props; const { show, window } = this.props;
return nextProps.showReactionPicker !== showReactionPicker || window.width !== nextProps.window.width; return nextProps.show !== show || window.width !== nextProps.window.width;
} }
onEmojiSelected(emoji, shortname) { onEmojiSelected(emoji, shortname) {
// standard emojis: `emoji` is unicode and `shortname` is :joy: // standard emojis: `emoji` is unicode and `shortname` is :joy:
// custom emojis: only `emoji` is returned with shortname type (:joy:) // custom emojis: only `emoji` is returned with shortname type (:joy:)
// to set reactions, we need shortname type // to set reactions, we need shortname type
const { onEmojiSelected } = this.props; const { onEmojiSelected, message } = this.props;
onEmojiSelected(shortname || emoji); onEmojiSelected(shortname || emoji, message.id);
} }
render() { render() {
const { const {
window: { width, height }, showReactionPicker, baseUrl, toggleReactionPicker window: { width, height }, show, baseUrl, reactionClose
} = this.props; } = this.props;
return (showReactionPicker return (show
? ( ? (
<Modal <Modal
isVisible={showReactionPicker} isVisible={show}
style={{ alignItems: 'center' }} style={{ alignItems: 'center' }}
onBackdropPress={() => toggleReactionPicker()} onBackdropPress={reactionClose}
onBackButtonPress={() => toggleReactionPicker()} onBackButtonPress={reactionClose}
animationIn='fadeIn' animationIn='fadeIn'
animationOut='fadeOut' animationOut='fadeOut'
> >
@ -69,12 +69,7 @@ class ReactionPicker extends React.Component {
} }
const mapStateToProps = state => ({ const mapStateToProps = state => ({
showReactionPicker: state.messages.showReactionPicker,
baseUrl: state.settings.Site_Url || state.server ? state.server.server : '' baseUrl: state.settings.Site_Url || state.server ? state.server.server : ''
}); });
const mapDispatchToProps = dispatch => ({ export default responsive(connect(mapStateToProps)(ReactionPicker));
toggleReactionPicker: message => dispatch(toggleReactionPickerAction(message))
});
export default responsive(connect(mapStateToProps, mapDispatchToProps)(ReactionPicker));

View File

@ -4,9 +4,9 @@ import {
} from 'react-native'; } from 'react-native';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { responsive } from 'react-native-responsive-ui'; import { responsive } from 'react-native-responsive-ui';
import equal from 'deep-equal'; import { Q } from '@nozbe/watermelondb';
import database, { safeAddListener } from '../../lib/realm'; import database from '../../lib/database';
import RocketChat from '../../lib/rocketchat'; import RocketChat from '../../lib/rocketchat';
import log from '../../utils/log'; import log from '../../utils/log';
import I18n from '../../i18n'; import I18n from '../../i18n';
@ -74,47 +74,72 @@ class UploadProgress extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.mounted = false;
this.ranInitialUploadCheck = false;
this.init();
this.state = { this.state = {
uploads: [] uploads: []
}; };
const { rid } = this.props;
this.uploads = database.objects('uploads').filtered('rid = $0', rid);
safeAddListener(this.uploads, this.updateUploads);
} }
componentDidMount() { componentDidMount() {
this.uploads.forEach((u) => { this.mounted = true;
if (!RocketChat.isUploadActive(u.path)) {
database.write(() => {
const [upload] = database.objects('uploads').filtered('path = $0', u.path);
if (upload) {
upload.error = true;
}
});
}
});
}
shouldComponentUpdate(nextProps, nextState) {
const { uploads } = this.state;
const { window } = this.props;
if (nextProps.window.width !== window.width) {
return true;
}
if (!equal(nextState.uploads, uploads)) {
return true;
}
return false;
} }
componentWillUnmount() { componentWillUnmount() {
this.uploads.removeAllListeners(); if (this.uploadsSubscription && this.uploadsSubscription.unsubscribe) {
this.uploadsSubscription.unsubscribe();
}
} }
deleteUpload = (item) => { init = () => {
const uploadItem = this.uploads.filtered('path = $0', item.path); const { rid } = this.props;
const db = database.active;
this.uploadsObservable = db.collections
.get('uploads')
.query(
Q.where('rid', rid)
)
.observeWithColumns(['progress', 'error']);
this.uploadsSubscription = this.uploadsObservable
.subscribe((uploads) => {
if (this.mounted) {
this.setState({ uploads });
} else {
this.state.uploads = uploads;
}
if (!this.ranInitialUploadCheck) {
this.uploadCheck();
}
});
}
uploadCheck = () => {
this.ranInitialUploadCheck = true;
const { uploads } = this.state;
uploads.forEach(async(u) => {
if (!RocketChat.isUploadActive(u.path)) {
try { try {
database.write(() => database.delete(uploadItem[0])); await database.database.action(async() => {
await u.update(() => {
u.error = true;
});
});
} catch (e) {
log(e);
}
}
});
}
deleteUpload = async(item) => {
try {
const db = database.active;
await db.action(async() => {
await item.destroyPermanently();
});
} catch (e) { } catch (e) {
log(e); log(e);
} }
@ -122,7 +147,7 @@ class UploadProgress extends Component {
cancelUpload = async(item) => { cancelUpload = async(item) => {
try { try {
await RocketChat.cancelUpload(item.path); await RocketChat.cancelUpload(item);
} catch (e) { } catch (e) {
log(e); log(e);
} }
@ -132,20 +157,18 @@ class UploadProgress extends Component {
const { rid, baseUrl: server, user } = this.props; const { rid, baseUrl: server, user } = this.props;
try { try {
database.write(() => { const db = database.active;
await db.action(async() => {
await item.update(() => {
item.error = false; item.error = false;
}); });
});
await RocketChat.sendFileMessage(rid, item, undefined, server, user); await RocketChat.sendFileMessage(rid, item, undefined, server, user);
} catch (e) { } catch (e) {
log(e); log(e);
} }
} }
updateUploads = () => {
const uploads = this.uploads.map(item => JSON.parse(JSON.stringify(item)));
this.setState({ uploads });
}
renderItemContent = (item) => { renderItemContent = (item) => {
const { window } = this.props; const { window } = this.props;
@ -177,6 +200,7 @@ class UploadProgress extends Component {
); );
} }
// TODO: transform into stateless and update based on its own observable changes
renderItem = (item, index) => ( renderItem = (item, index) => (
<View key={item.path} style={[styles.item, index !== 0 ? { marginTop: 10 } : {}]}> <View key={item.path} style={[styles.item, index !== 0 ? { marginTop: 10 } : {}]}>
{this.renderItemContent(item)} {this.renderItemContent(item)}

View File

@ -1,26 +1,22 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { import {
Text, View, LayoutAnimation, InteractionManager Text, View, InteractionManager, LayoutAnimation
} from 'react-native'; } from 'react-native';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { RectButton } from 'react-native-gesture-handler'; import { RectButton } from 'react-native-gesture-handler';
import { SafeAreaView, HeaderBackButton } from 'react-navigation'; import { SafeAreaView, HeaderBackButton } from 'react-navigation';
import equal from 'deep-equal'; import { sanitizedRaw } from '@nozbe/watermelondb/RawRecord';
import moment from 'moment'; import moment from 'moment';
import EJSON from 'ejson';
import * as Haptics from 'expo-haptics'; import * as Haptics from 'expo-haptics';
import { Q } from '@nozbe/watermelondb';
import isEqual from 'lodash/isEqual';
import { import {
toggleReactionPicker as toggleReactionPickerAction,
actionsShow as actionsShowAction,
errorActionsShow as errorActionsShowAction,
editCancel as editCancelAction,
replyCancel as replyCancelAction,
replyBroadcast as replyBroadcastAction replyBroadcast as replyBroadcastAction
} from '../../actions/messages'; } from '../../actions/messages';
import { List } from './List'; import { List } from './List';
import database, { safeAddListener } from '../../lib/realm'; import database from '../../lib/database';
import RocketChat from '../../lib/rocketchat'; import RocketChat from '../../lib/rocketchat';
import Message from '../../containers/message'; import Message from '../../containers/message';
import MessageActions from '../../containers/MessageActions'; import MessageActions from '../../containers/MessageActions';
@ -30,7 +26,6 @@ import ReactionPicker from './ReactionPicker';
import UploadProgress from './UploadProgress'; import UploadProgress from './UploadProgress';
import styles from './styles'; import styles from './styles';
import log from '../../utils/log'; import log from '../../utils/log';
import { isIOS } from '../../utils/deviceInfo';
import EventEmitter from '../../utils/events'; import EventEmitter from '../../utils/events';
import I18n from '../../i18n'; import I18n from '../../i18n';
import RoomHeaderView, { RightButtons } from './Header'; import RoomHeaderView, { RightButtons } from './Header';
@ -38,11 +33,26 @@ import StatusBar from '../../containers/StatusBar';
import Separator from './Separator'; import Separator from './Separator';
import { COLOR_WHITE, HEADER_BACK } from '../../constants/colors'; import { COLOR_WHITE, HEADER_BACK } from '../../constants/colors';
import debounce from '../../utils/debounce'; import debounce from '../../utils/debounce';
import buildMessage from '../../lib/methods/helpers/buildMessage';
import FileModal from '../../containers/FileModal'; import FileModal from '../../containers/FileModal';
import ReactionsModal from '../../containers/ReactionsModal'; import ReactionsModal from '../../containers/ReactionsModal';
import { LISTENER } from '../../containers/Toast'; import { LISTENER } from '../../containers/Toast';
import { isReadOnly, isBlocked } from '../../utils/room'; import { isReadOnly, isBlocked } from '../../utils/room';
import { isIOS } from '../../utils/deviceInfo';
const stateAttrsUpdate = [
'joined',
'lastOpen',
'photoModalVisible',
'reactionsModalVisible',
'canAutoTranslate',
'showActions',
'showErrorActions',
'loading',
'editing',
'replying',
'reacting'
];
const roomAttrsUpdate = ['f', 'ro', 'blocked', 'blocker', 'archived', 'muted'];
class RoomView extends React.Component { class RoomView extends React.Component {
static navigationOptions = ({ navigation }) => { static navigationOptions = ({ navigation }) => {
@ -51,6 +61,7 @@ class RoomView extends React.Component {
const title = navigation.getParam('name'); const title = navigation.getParam('name');
const t = navigation.getParam('t'); const t = navigation.getParam('t');
const tmid = navigation.getParam('tmid'); const tmid = navigation.getParam('tmid');
const room = navigation.getParam('room');
const toggleFollowThread = navigation.getParam('toggleFollowThread', () => {}); const toggleFollowThread = navigation.getParam('toggleFollowThread', () => {});
const unreadsCount = navigation.getParam('unreadsCount', null); const unreadsCount = navigation.getParam('unreadsCount', null);
return { return {
@ -68,6 +79,7 @@ class RoomView extends React.Component {
<RightButtons <RightButtons
rid={rid} rid={rid}
tmid={tmid} tmid={tmid}
room={room}
t={t} t={t}
navigation={navigation} navigation={navigation}
toggleFollowThread={toggleFollowThread} toggleFollowThread={toggleFollowThread}
@ -91,25 +103,16 @@ class RoomView extends React.Component {
username: PropTypes.string.isRequired, username: PropTypes.string.isRequired,
token: PropTypes.string.isRequired token: PropTypes.string.isRequired
}), }),
showActions: PropTypes.bool,
showErrorActions: PropTypes.bool,
actionMessage: PropTypes.object,
appState: PropTypes.string, appState: PropTypes.string,
useRealName: PropTypes.bool, useRealName: PropTypes.bool,
isAuthenticated: PropTypes.bool, isAuthenticated: PropTypes.bool,
Message_GroupingPeriod: PropTypes.number, Message_GroupingPeriod: PropTypes.number,
Message_TimeFormat: PropTypes.string, Message_TimeFormat: PropTypes.string,
Message_Read_Receipt_Enabled: PropTypes.bool, Message_Read_Receipt_Enabled: PropTypes.bool,
editing: PropTypes.bool,
replying: PropTypes.bool,
baseUrl: PropTypes.string, baseUrl: PropTypes.string,
customEmojis: PropTypes.object,
useMarkdown: PropTypes.bool, useMarkdown: PropTypes.bool,
toggleReactionPicker: PropTypes.func, replyBroadcast: PropTypes.func
actionsShow: PropTypes.func,
editCancel: PropTypes.func,
replyCancel: PropTypes.func,
replyBroadcast: PropTypes.func,
errorActionsShow: PropTypes.func
}; };
constructor(props) { constructor(props) {
@ -118,22 +121,36 @@ class RoomView extends React.Component {
console.time(`${ this.constructor.name } mount`); console.time(`${ this.constructor.name } mount`);
this.rid = props.navigation.getParam('rid'); this.rid = props.navigation.getParam('rid');
this.t = props.navigation.getParam('t'); this.t = props.navigation.getParam('t');
this.tmid = props.navigation.getParam('tmid'); this.tmid = props.navigation.getParam('tmid', null);
this.rooms = database.objects('subscriptions').filtered('rid = $0', this.rid); const room = props.navigation.getParam('room');
this.chats = database.objects('subscriptions').filtered('rid != $0', this.rid); const selectedMessage = props.navigation.getParam('message');
const canAutoTranslate = RocketChat.canAutoTranslate();
this.state = { this.state = {
joined: this.rooms.length > 0, joined: true,
room: this.rooms[0] || { rid: this.rid, t: this.t }, room: room || { rid: this.rid, t: this.t },
roomUpdate: {},
lastOpen: null, lastOpen: null,
photoModalVisible: false, photoModalVisible: false,
reactionsModalVisible: false, reactionsModalVisible: false,
selectedAttachment: {}, selectedAttachment: {},
selectedMessage: {}, selectedMessage: selectedMessage || {},
canAutoTranslate canAutoTranslate: false,
loading: true,
showActions: false,
showErrorActions: false,
editing: false,
replying: !!selectedMessage,
replyWithMention: false,
reacting: false
}; };
if (room && room.observe) {
this.observeRoom(room);
} else {
this.findAndObserveRoom(this.rid);
}
this.beginAnimating = false; this.beginAnimating = false;
this.beginAnimatingTimeout = setTimeout(() => this.beginAnimating = true, 300); this.didFocusListener = props.navigation.addListener('didFocus', () => this.beginAnimating = true);
this.messagebox = React.createRef(); this.messagebox = React.createRef();
this.willBlurListener = props.navigation.addListener('willBlur', () => this.mounted = false); this.willBlurListener = props.navigation.addListener('willBlur', () => this.mounted = false);
this.mounted = false; this.mounted = false;
@ -141,111 +158,85 @@ class RoomView extends React.Component {
} }
componentDidMount() { componentDidMount() {
this.mounted = true;
this.didMountInteraction = InteractionManager.runAfterInteractions(() => { this.didMountInteraction = InteractionManager.runAfterInteractions(() => {
const { room } = this.state; const { room } = this.state;
const { navigation, isAuthenticated } = this.props; const { navigation, isAuthenticated } = this.props;
if (room.id && !this.tmid) {
if (room._id && !this.tmid) {
navigation.setParams({ name: this.getRoomTitle(room), t: room.t }); navigation.setParams({ name: this.getRoomTitle(room), t: room.t });
} }
if (this.tmid) { if (this.tmid) {
navigation.setParams({ toggleFollowThread: this.toggleFollowThread }); navigation.setParams({ toggleFollowThread: this.toggleFollowThread });
} }
if (isAuthenticated) { if (isAuthenticated) {
this.init(); this.init();
} else { } else {
EventEmitter.addEventListener('connected', this.handleConnected); EventEmitter.addEventListener('connected', this.handleConnected);
} }
safeAddListener(this.rooms, this.updateRoom); this.updateUnreadCount();
safeAddListener(this.chats, this.updateUnreadCount);
this.mounted = true;
}); });
console.timeEnd(`${ this.constructor.name } mount`); console.timeEnd(`${ this.constructor.name } mount`);
} }
shouldComponentUpdate(nextProps, nextState) { shouldComponentUpdate(nextProps, nextState) {
const { const { state } = this;
room, joined, lastOpen, photoModalVisible, reactionsModalVisible, canAutoTranslate const { roomUpdate } = state;
} = this.state; const { appState } = this.props;
const { showActions, showErrorActions, appState } = this.props; if (appState !== nextProps.appState) {
if (lastOpen !== nextState.lastOpen) {
return true;
} else if (photoModalVisible !== nextState.photoModalVisible) {
return true;
} else if (reactionsModalVisible !== nextState.reactionsModalVisible) {
return true;
} else if (room.ro !== nextState.room.ro) {
return true;
} else if (room.f !== nextState.room.f) {
return true;
} else if (room.blocked !== nextState.room.blocked) {
return true;
} else if (room.blocker !== nextState.room.blocker) {
return true;
} else if (room.archived !== nextState.room.archived) {
return true;
} else if (joined !== nextState.joined) {
return true;
} else if (canAutoTranslate !== nextState.canAutoTranslate) {
return true;
} else if (showActions !== nextProps.showActions) {
return true;
} else if (showErrorActions !== nextProps.showErrorActions) {
return true;
} else if (appState !== nextProps.appState) {
return true;
} else if (!equal(room.muted, nextState.room.muted)) {
return true; return true;
} }
return false; const stateUpdated = stateAttrsUpdate.some(key => nextState[key] !== state[key]);
if (stateUpdated) {
return true;
}
return roomAttrsUpdate.some(key => !isEqual(nextState.roomUpdate[key], roomUpdate[key]));
} }
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
const { room } = this.state;
const { appState } = this.props; const { appState } = this.props;
if (appState === 'foreground' && appState !== prevProps.appState) { if (appState === 'foreground' && appState !== prevProps.appState) {
this.onForegroundInteraction = InteractionManager.runAfterInteractions(() => { this.onForegroundInteraction = InteractionManager.runAfterInteractions(() => {
RocketChat.loadMissedMessages(room).catch(e => console.log(e)); this.init();
RocketChat.readMessages(room.rid).catch(e => console.log(e));
}); });
} }
} }
componentWillUnmount() { async componentWillUnmount() {
const { editing, room } = this.state;
const db = database.active;
this.mounted = false; this.mounted = false;
const { editing, replying } = this.props;
if (!editing && this.messagebox && this.messagebox.current) { if (!editing && this.messagebox && this.messagebox.current) {
const { text } = this.messagebox.current; const { text } = this.messagebox.current;
let obj; let obj;
if (this.tmid) { if (this.tmid) {
obj = database.objectForPrimaryKey('threads', this.tmid); try {
const threadsCollection = db.collections.get('threads');
obj = await threadsCollection.find(this.tmid);
} catch (e) {
// Do nothing
}
} else { } else {
[obj] = this.rooms; obj = room;
} }
if (obj) { if (obj) {
database.write(() => { try {
obj.draftMessage = text; await db.action(async() => {
await obj.update((r) => {
r.draftMessage = text;
}); });
});
} catch (error) {
// Do nothing
}
} }
} }
this.rooms.removeAllListeners();
this.chats.removeAllListeners();
if (this.sub && this.sub.stop) { if (this.sub && this.sub.stop) {
this.sub.stop(); this.sub.stop();
} }
if (this.beginAnimatingTimeout) { if (this.didFocusListener && this.didFocusListener.remove) {
clearTimeout(this.beginAnimatingTimeout); this.didFocusListener.remove();
}
if (editing) {
const { editCancel } = this.props;
editCancel();
}
if (replying) {
const { replyCancel } = this.props;
replyCancel();
} }
if (this.didMountInteraction && this.didMountInteraction.cancel) { if (this.didMountInteraction && this.didMountInteraction.cancel) {
this.didMountInteraction.cancel(); this.didMountInteraction.cancel();
@ -253,15 +244,18 @@ class RoomView extends React.Component {
if (this.onForegroundInteraction && this.onForegroundInteraction.cancel) { if (this.onForegroundInteraction && this.onForegroundInteraction.cancel) {
this.onForegroundInteraction.cancel(); this.onForegroundInteraction.cancel();
} }
if (this.updateStateInteraction && this.updateStateInteraction.cancel) {
this.updateStateInteraction.cancel();
}
if (this.initInteraction && this.initInteraction.cancel) { if (this.initInteraction && this.initInteraction.cancel) {
this.initInteraction.cancel(); this.initInteraction.cancel();
} }
if (this.willBlurListener && this.willBlurListener.remove) { if (this.willBlurListener && this.willBlurListener.remove) {
this.willBlurListener.remove(); this.willBlurListener.remove();
} }
if (this.subSubscription && this.subSubscription.unsubscribe) {
this.subSubscription.unsubscribe();
}
if (this.queryUnreads && this.queryUnreads.unsubscribe) {
this.queryUnreads.unsubscribe();
}
EventEmitter.removeListener('connected', this.handleConnected); EventEmitter.removeListener('connected', this.handleConnected);
console.countReset(`${ this.constructor.name }.render calls`); console.countReset(`${ this.constructor.name }.render calls`);
} }
@ -269,38 +263,135 @@ class RoomView extends React.Component {
// eslint-disable-next-line react/sort-comp // eslint-disable-next-line react/sort-comp
init = () => { init = () => {
try { try {
this.setState({ loading: true });
this.initInteraction = InteractionManager.runAfterInteractions(async() => { this.initInteraction = InteractionManager.runAfterInteractions(async() => {
const { room } = this.state; const { room, joined } = this.state;
if (this.tmid) { if (this.tmid) {
await this.getThreadMessages(); await this.getThreadMessages();
} else { } else {
const newLastOpen = new Date();
await this.getMessages(room); await this.getMessages(room);
// if room is joined // if room is joined
if (room._id) { if (joined) {
if (room.alert || room.unread || room.userMentions) { if (room.alert || room.unread || room.userMentions) {
this.setLastOpen(room.ls); this.setLastOpen(room.ls);
} else { } else {
this.setLastOpen(null); this.setLastOpen(null);
} }
RocketChat.readMessages(room.rid).catch(e => console.log(e)); RocketChat.readMessages(room.rid, newLastOpen).catch(e => console.log(e));
this.sub = await RocketChat.subscribeRoom(room); this.sub = await RocketChat.subscribeRoom(room);
} }
} }
// We run `canAutoTranslate` again in order to refetch auto translate permission // We run `canAutoTranslate` again in order to refetch auto translate permission
// in case of a missing connection or poor connection on room open // in case of a missing connection or poor connection on room open
const canAutoTranslate = RocketChat.canAutoTranslate(); const canAutoTranslate = await RocketChat.canAutoTranslate();
this.setState({ canAutoTranslate }); this.setState({ canAutoTranslate, loading: false });
}); });
} catch (e) {
this.setState({ loading: false });
log(e);
}
}
findAndObserveRoom = async(rid) => {
try {
const db = database.active;
const { navigation } = this.props;
const subCollection = await db.collections.get('subscriptions');
const room = await subCollection.find(rid);
this.setState({ room });
navigation.setParams({ room });
this.observeRoom(room);
} catch (error) {
if (this.t !== 'd') {
console.log('Room not found');
this.internalSetState({ joined: false });
} else {
// We navigate to RoomView before the DM is inserted to the local db
// So we retry just to make sure we have the right content
this.retryFindCount = this.retryFindCount + 1 || 1;
if (this.retryFindCount <= 3) {
this.retryFindTimeout = setTimeout(() => {
this.findAndObserveRoom(rid);
this.init();
}, 300);
}
}
}
}
observeRoom = (room) => {
const observable = room.observe();
this.subSubscription = observable
.subscribe((changes) => {
const roomUpdate = roomAttrsUpdate.reduce((ret, attr) => {
ret[attr] = changes[attr];
return ret;
}, {});
if (this.mounted) {
this.internalSetState({ room: changes, roomUpdate });
} else {
this.state.room = changes;
this.state.roomUpdate = roomUpdate;
}
});
}
errorActionsShow = (message) => {
this.setState({ selectedMessage: message, showErrorActions: true });
}
onActionsHide = () => {
const { editing, replying, reacting } = this.state;
if (editing || replying || reacting) {
return;
}
this.setState({ selectedMessage: {}, showActions: false });
}
onErrorActionsHide = () => {
this.setState({ selectedMessage: {}, showErrorActions: false });
}
onEditInit = (message) => {
this.setState({ selectedMessage: message, editing: true, showActions: false });
}
onEditCancel = () => {
this.setState({ selectedMessage: {}, editing: false });
}
onEditRequest = async(message) => {
this.setState({ selectedMessage: {}, editing: false });
try {
await RocketChat.editMessage(message);
} catch (e) { } catch (e) {
log(e); log(e);
} }
} }
onReplyInit = (message, mention) => {
this.setState({
selectedMessage: message, replying: true, showActions: false, replyWithMention: mention
});
}
onReplyCancel = () => {
this.setState({ selectedMessage: {}, replying: false });
}
onReactionInit = (message) => {
this.setState({ selectedMessage: message, reacting: true, showActions: false });
}
onReactionClose = () => {
this.setState({ selectedMessage: {}, reacting: false });
}
onMessageLongPress = (message) => { onMessageLongPress = (message) => {
const { actionsShow } = this.props; this.setState({ selectedMessage: message, showActions: true });
actionsShow({ ...message, rid: this.rid });
} }
onOpenFileModal = (attachment) => { onOpenFileModal = (attachment) => {
@ -311,14 +402,10 @@ class RoomView extends React.Component {
this.setState({ selectedAttachment: {}, photoModalVisible: false }); this.setState({ selectedAttachment: {}, photoModalVisible: false });
} }
onReactionPress = (shortname, messageId) => { onReactionPress = async(shortname, messageId) => {
const { actionMessage, toggleReactionPicker } = this.props;
try { try {
if (!messageId) { await RocketChat.setReaction(shortname, messageId);
RocketChat.setReaction(shortname, actionMessage._id); this.onReactionClose();
return toggleReactionPicker();
}
RocketChat.setReaction(shortname, messageId);
} catch (e) { } catch (e) {
log(e); log(e);
} }
@ -341,45 +428,49 @@ class RoomView extends React.Component {
}, 1000, true) }, 1000, true)
// eslint-disable-next-line react/sort-comp // eslint-disable-next-line react/sort-comp
updateUnreadCount = debounce(() => { updateUnreadCount = async() => {
const db = database.active;
const observable = await db.collections
.get('subscriptions')
.query(
Q.where('archived', false),
Q.where('open', true),
Q.where('rid', Q.notEq(this.rid))
)
.observeWithColumns(['unread']);
this.queryUnreads = observable.subscribe((data) => {
const { navigation } = this.props; const { navigation } = this.props;
const unreadsCount = this.chats.filtered('archived != true && open == true && unread > 0').reduce((a, b) => a + (b.unread || 0), 0); const unreadsCount = data.filter(s => s.unread > 0).reduce((a, b) => a + (b.unread || 0), 0);
if (unreadsCount !== navigation.getParam('unreadsCount')) { if (unreadsCount !== navigation.getParam('unreadsCount')) {
navigation.setParams({ navigation.setParams({
unreadsCount unreadsCount
}); });
} }
}, 300, false) });
};
onThreadPress = debounce((item) => { onThreadPress = debounce(async(item) => {
const { navigation } = this.props; const { navigation } = this.props;
if (item.tmid) { if (item.tmid) {
if (!item.tmsg) {
await this.fetchThreadName(item.tmid, item.id);
}
navigation.push('RoomView', { navigation.push('RoomView', {
rid: item.rid, tmid: item.tmid, name: item.tmsg, t: 'thread' rid: item.subscription.id, tmid: item.tmid, name: item.tmsg, t: 'thread'
}); });
} else if (item.tlm) { } else if (item.tlm) {
const title = item.msg || (item.attachments && item.attachments.length && item.attachments[0].title);
navigation.push('RoomView', { navigation.push('RoomView', {
rid: item.rid, tmid: item._id, name: title, t: 'thread' rid: item.subscription.id, tmid: item.id, name: item.msg, t: 'thread'
}); });
} }
}, 1000, true) }, 1000, true)
toggleReactionPicker = (message) => {
const { toggleReactionPicker } = this.props;
toggleReactionPicker(message);
}
replyBroadcast = (message) => { replyBroadcast = (message) => {
const { replyBroadcast } = this.props; const { replyBroadcast } = this.props;
replyBroadcast(message); replyBroadcast(message);
} }
errorActionsShow = (message) => {
const { errorActionsShow } = this.props;
errorActionsShow(message);
}
handleConnected = () => { handleConnected = () => {
this.init(); this.init();
EventEmitter.removeListener('connected', this.handleConnected); EventEmitter.removeListener('connected', this.handleConnected);
@ -395,15 +486,6 @@ class RoomView extends React.Component {
this.setState(...args); this.setState(...args);
} }
updateRoom = () => {
this.updateStateInteraction = InteractionManager.runAfterInteractions(() => {
if (this.rooms[0]) {
const room = JSON.parse(JSON.stringify(this.rooms[0] || {}));
this.internalSetState({ room });
}
});
}
sendMessage = (message, tmid) => { sendMessage = (message, tmid) => {
const { user } = this.props; const { user } = this.props;
LayoutAnimation.easeInEaseOut(); LayoutAnimation.easeInEaseOut();
@ -433,12 +515,21 @@ class RoomView extends React.Component {
getThreadMessages = () => { getThreadMessages = () => {
try { try {
return RocketChat.loadThreadMessages({ tmid: this.tmid }); return RocketChat.loadThreadMessages({ tmid: this.tmid, rid: this.rid });
} catch (e) { } catch (e) {
log(e); log(e);
} }
} }
getCustomEmoji = (name) => {
const { customEmojis } = this.props;
const emoji = customEmojis[name];
if (emoji) {
return emoji;
}
return null;
}
setLastOpen = lastOpen => this.setState({ lastOpen }); setLastOpen = lastOpen => this.setState({ lastOpen });
joinRoom = async() => { joinRoom = async() => {
@ -450,16 +541,43 @@ class RoomView extends React.Component {
} catch (e) { } catch (e) {
log(e); log(e);
} }
}; }
// eslint-disable-next-line react/sort-comp // eslint-disable-next-line react/sort-comp
fetchThreadName = async(tmid) => { fetchThreadName = async(tmid, messageId) => {
try { try {
// TODO: we should build a tmid queue here in order to search for a single tmid only once const { room } = this.state;
const thread = await RocketChat.getSingleMessage(tmid); const db = database.active;
database.write(() => { const threadCollection = db.collections.get('threads');
database.create('threads', buildMessage(EJSON.fromJSONValue(thread)), true); const messageCollection = db.collections.get('messages');
const messageRecord = await messageCollection.find(messageId);
let threadRecord;
try {
threadRecord = await threadCollection.find(tmid);
} catch (error) {
console.log('Thread not found. We have to search for it.');
}
if (threadRecord) {
await db.action(async() => {
await messageRecord.update((m) => {
m.tmsg = threadRecord.msg || (threadRecord.attachments && threadRecord.attachments.length && threadRecord.attachments[0].title);
}); });
});
} else {
const thread = await RocketChat.getSingleMessage(tmid);
await db.action(async() => {
await db.batch(
threadCollection.prepareCreate((t) => {
t._raw = sanitizedRaw({ id: thread._id }, threadCollection.schema);
t.subscription.set(room);
Object.assign(t, thread);
}),
messageRecord.prepareUpdate((m) => {
m.tmsg = thread.msg || (thread.attachments && thread.attachments.length && thread.attachments[0].title);
})
);
});
}
} catch (e) { } catch (e) {
log(e); log(e);
} }
@ -482,6 +600,12 @@ class RoomView extends React.Component {
navigation.navigate('RoomInfoView', navParam); navigation.navigate('RoomInfoView', navParam);
} }
get isReadOnly() {
const { room } = this.state;
const { user } = this.props;
return isReadOnly(room, user);
}
renderItem = (item, previousItem) => { renderItem = (item, previousItem) => {
const { room, lastOpen, canAutoTranslate } = this.state; const { room, lastOpen, canAutoTranslate } = this.state;
const { const {
@ -504,13 +628,12 @@ class RoomView extends React.Component {
const message = ( const message = (
<Message <Message
key={item._id}
item={item} item={item}
user={user} user={user}
archived={room.archived} archived={room.archived}
broadcast={room.broadcast} broadcast={room.broadcast}
status={item.status} status={item.status}
_updatedAt={item._updatedAt} isThreadRoom={!!this.tmid}
previousItem={previousItem} previousItem={previousItem}
fetchThreadName={this.fetchThreadName} fetchThreadName={this.fetchThreadName}
onReactionPress={this.onReactionPress} onReactionPress={this.onReactionPress}
@ -519,7 +642,7 @@ class RoomView extends React.Component {
onDiscussionPress={this.onDiscussionPress} onDiscussionPress={this.onDiscussionPress}
onThreadPress={this.onThreadPress} onThreadPress={this.onThreadPress}
onOpenFileModal={this.onOpenFileModal} onOpenFileModal={this.onOpenFileModal}
toggleReactionPicker={this.toggleReactionPicker} reactionInit={this.onReactionInit}
replyBroadcast={this.replyBroadcast} replyBroadcast={this.replyBroadcast}
errorActionsShow={this.errorActionsShow} errorActionsShow={this.errorActionsShow}
baseUrl={baseUrl} baseUrl={baseUrl}
@ -531,6 +654,7 @@ class RoomView extends React.Component {
autoTranslateRoom={canAutoTranslate && room.autoTranslate} autoTranslateRoom={canAutoTranslate && room.autoTranslate}
autoTranslateLanguage={room.autoTranslateLanguage} autoTranslateLanguage={room.autoTranslateLanguage}
navToRoomInfo={this.navToRoomInfo} navToRoomInfo={this.navToRoomInfo}
getCustomEmoji={this.getCustomEmoji}
/> />
); );
@ -550,8 +674,10 @@ class RoomView extends React.Component {
} }
renderFooter = () => { renderFooter = () => {
const { joined, room } = this.state; const {
const { navigation, user } = this.props; joined, room, selectedMessage, editing, replying, replyWithMention
} = this.state;
const { navigation } = this.props;
if (!joined && !this.tmid) { if (!joined && !this.tmid) {
return ( return (
@ -568,7 +694,7 @@ class RoomView extends React.Component {
</View> </View>
); );
} }
if (isReadOnly(room, user)) { if (this.isReadOnly) {
return ( return (
<View style={styles.readOnly}> <View style={styles.readOnly}>
<Text style={styles.previewMode}>{I18n.t('This_room_is_read_only')}</Text> <Text style={styles.previewMode}>{I18n.t('This_room_is_read_only')}</Text>
@ -590,33 +716,60 @@ class RoomView extends React.Component {
tmid={this.tmid} tmid={this.tmid}
roomType={room.t} roomType={room.t}
isFocused={navigation.isFocused()} isFocused={navigation.isFocused()}
message={selectedMessage}
editing={editing}
editRequest={this.onEditRequest}
editCancel={this.onEditCancel}
replying={replying}
replyWithMention={replyWithMention}
replyCancel={this.onReplyCancel}
getCustomEmoji={this.getCustomEmoji}
/> />
); );
}; };
renderActions = () => { renderActions = () => {
const { room } = this.state;
const { const {
user, showActions, showErrorActions, navigation room, selectedMessage, showActions, showErrorActions, joined
} = this.state;
const {
user, navigation
} = this.props; } = this.props;
if (!navigation.isFocused()) { if (!navigation.isFocused()) {
return null; return null;
} }
return ( return (
<React.Fragment> <>
{room._id && showActions {joined && showActions
? <MessageActions room={room} tmid={this.tmid} user={user} /> ? (
<MessageActions
tmid={this.tmid}
room={room}
user={user}
message={selectedMessage}
actionsHide={this.onActionsHide}
editInit={this.onEditInit}
replyInit={this.onReplyInit}
reactionInit={this.onReactionInit}
isReadOnly={this.isReadOnly}
/>
)
: null : null
} }
{showErrorActions ? <MessageErrorActions /> : null} {showErrorActions ? (
</React.Fragment> <MessageErrorActions
message={selectedMessage}
actionsHide={this.onErrorActionsHide}
/>
) : null}
</>
); );
} }
render() { render() {
console.count(`${ this.constructor.name }.render calls`); console.count(`${ this.constructor.name }.render calls`);
const { const {
room, photoModalVisible, reactionsModalVisible, selectedAttachment, selectedMessage room, photoModalVisible, reactionsModalVisible, selectedAttachment, selectedMessage, loading, reacting
} = this.state; } = this.state;
const { user, baseUrl } = this.props; const { user, baseUrl } = this.props;
const { rid, t } = room; const { rid, t } = room;
@ -624,10 +777,23 @@ class RoomView extends React.Component {
return ( return (
<SafeAreaView style={styles.container} testID='room-view' forceInset={{ vertical: 'never' }}> <SafeAreaView style={styles.container} testID='room-view' forceInset={{ vertical: 'never' }}>
<StatusBar /> <StatusBar />
<List rid={rid} t={t} tmid={this.tmid} renderRow={this.renderItem} /> <List
rid={rid}
t={t}
tmid={this.tmid}
room={room}
renderRow={this.renderItem}
loading={loading}
animated={this.beginAnimating}
/>
{this.renderFooter()} {this.renderFooter()}
{this.renderActions()} {this.renderActions()}
<ReactionPicker onEmojiSelected={this.onReactionPress} /> <ReactionPicker
show={reacting}
message={selectedMessage}
onEmojiSelected={this.onReactionPress}
reactionClose={this.onReactionClose}
/>
<UploadProgress rid={this.rid} user={user} baseUrl={baseUrl} /> <UploadProgress rid={this.rid} user={user} baseUrl={baseUrl} />
<FileModal <FileModal
attachment={selectedAttachment} attachment={selectedAttachment}
@ -639,9 +805,10 @@ class RoomView extends React.Component {
<ReactionsModal <ReactionsModal
message={selectedMessage} message={selectedMessage}
isVisible={reactionsModalVisible} isVisible={reactionsModalVisible}
onClose={this.onCloseReactionsModal}
user={user} user={user}
baseUrl={baseUrl} baseUrl={baseUrl}
onClose={this.onCloseReactionsModal}
getCustomEmoji={this.getCustomEmoji}
/> />
</SafeAreaView> </SafeAreaView>
); );
@ -654,27 +821,18 @@ const mapStateToProps = state => ({
username: state.login.user && state.login.user.username, username: state.login.user && state.login.user.username,
token: state.login.user && state.login.user.token token: state.login.user && state.login.user.token
}, },
actionMessage: state.messages.actionMessage,
editing: state.messages.editing,
replying: state.messages.replying,
showActions: state.messages.showActions,
showErrorActions: state.messages.showErrorActions,
appState: state.app.ready && state.app.foreground ? 'foreground' : 'background', appState: state.app.ready && state.app.foreground ? 'foreground' : 'background',
useRealName: state.settings.UI_Use_Real_Name, useRealName: state.settings.UI_Use_Real_Name,
isAuthenticated: state.login.isAuthenticated, isAuthenticated: state.login.isAuthenticated,
Message_GroupingPeriod: state.settings.Message_GroupingPeriod, Message_GroupingPeriod: state.settings.Message_GroupingPeriod,
Message_TimeFormat: state.settings.Message_TimeFormat, Message_TimeFormat: state.settings.Message_TimeFormat,
useMarkdown: state.markdown.useMarkdown, useMarkdown: state.markdown.useMarkdown,
customEmojis: state.customEmojis,
baseUrl: state.settings.baseUrl || state.server ? state.server.server : '', baseUrl: state.settings.baseUrl || state.server ? state.server.server : '',
Message_Read_Receipt_Enabled: state.settings.Message_Read_Receipt_Enabled Message_Read_Receipt_Enabled: state.settings.Message_Read_Receipt_Enabled
}); });
const mapDispatchToProps = dispatch => ({ const mapDispatchToProps = dispatch => ({
editCancel: () => dispatch(editCancelAction()),
replyCancel: () => dispatch(replyCancelAction()),
toggleReactionPicker: message => dispatch(toggleReactionPickerAction(message)),
errorActionsShow: actionMessage => dispatch(errorActionsShowAction(actionMessage)),
actionsShow: actionMessage => dispatch(actionsShowAction(actionMessage)),
replyBroadcast: message => dispatch(replyBroadcastAction(message)) replyBroadcast: message => dispatch(replyBroadcastAction(message))
}); });

View File

@ -12,12 +12,12 @@ import { toggleServerDropdown as toggleServerDropdownAction } from '../../action
import { selectServerRequest as selectServerRequestAction } from '../../actions/server'; import { selectServerRequest as selectServerRequestAction } from '../../actions/server';
import { appStart as appStartAction } from '../../actions'; import { appStart as appStartAction } from '../../actions';
import styles from './styles'; import styles from './styles';
import database, { safeAddListener } from '../../lib/realm';
import Touch from '../../utils/touch'; import Touch from '../../utils/touch';
import RocketChat from '../../lib/rocketchat'; import RocketChat from '../../lib/rocketchat';
import I18n from '../../i18n'; import I18n from '../../i18n';
import EventEmitter from '../../utils/events'; import EventEmitter from '../../utils/events';
import Check from '../../containers/Check'; import Check from '../../containers/Check';
import database from '../../lib/database';
const ROW_HEIGHT = 68; const ROW_HEIGHT = 68;
const ANIMATION_DURATION = 200; const ANIMATION_DURATION = 200;
@ -34,15 +34,21 @@ class ServerDropdown extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.servers = database.databases.serversDB.objects('servers'); this.state = { servers: [] };
this.state = {
servers: this.servers
};
this.animatedValue = new Animated.Value(0); this.animatedValue = new Animated.Value(0);
safeAddListener(this.servers, this.updateState);
} }
componentDidMount() { async componentDidMount() {
const serversDB = database.servers;
const observable = await serversDB.collections
.get('servers')
.query()
.observeWithColumns(['name']);
this.subscription = observable.subscribe((data) => {
this.setState({ servers: data });
});
Animated.timing( Animated.timing(
this.animatedValue, this.animatedValue,
{ {
@ -81,11 +87,9 @@ class ServerDropdown extends Component {
clearTimeout(this.newServerTimeout); clearTimeout(this.newServerTimeout);
this.newServerTimeout = false; this.newServerTimeout = false;
} }
if (this.subscription && this.subscription.unsubscribe) {
this.subscription.unsubscribe();
} }
updateState = () => {
const { servers } = this;
this.setState({ servers });
} }
close = () => { close = () => {

View File

@ -1,14 +1,23 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { import {
View, FlatList, BackHandler, ActivityIndicator, Text, ScrollView, Keyboard, LayoutAnimation, InteractionManager, Dimensions View,
FlatList,
BackHandler,
ActivityIndicator,
Text,
ScrollView,
Keyboard,
LayoutAnimation,
Dimensions
} from 'react-native'; } from 'react-native';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { isEqual } from 'lodash'; import { isEqual, orderBy } from 'lodash';
import { SafeAreaView } from 'react-navigation'; import { SafeAreaView } from 'react-navigation';
import Orientation from 'react-native-orientation-locker'; import Orientation from 'react-native-orientation-locker';
import { Q } from '@nozbe/watermelondb';
import database, { safeAddListener } from '../../lib/realm'; import database from '../../lib/database';
import RocketChat from '../../lib/rocketchat'; import RocketChat from '../../lib/rocketchat';
import RoomItem, { ROW_HEIGHT } from '../../presentation/RoomItem'; import RoomItem, { ROW_HEIGHT } from '../../presentation/RoomItem';
import styles from './styles'; import styles from './styles';
@ -26,47 +35,87 @@ import { appStart as appStartAction } from '../../actions';
import debounce from '../../utils/debounce'; import debounce from '../../utils/debounce';
import { isIOS, isAndroid } from '../../utils/deviceInfo'; import { isIOS, isAndroid } from '../../utils/deviceInfo';
import RoomsListHeaderView from './Header'; import RoomsListHeaderView from './Header';
import { DrawerButton, CustomHeaderButtons, Item } from '../../containers/HeaderButton'; import {
DrawerButton,
CustomHeaderButtons,
Item
} from '../../containers/HeaderButton';
import StatusBar from '../../containers/StatusBar'; import StatusBar from '../../containers/StatusBar';
import ListHeader from './ListHeader'; import ListHeader from './ListHeader';
import { selectServerRequest as selectServerRequestAction } from '../../actions/server'; import { selectServerRequest as selectServerRequestAction } from '../../actions/server';
const SCROLL_OFFSET = 56; const SCROLL_OFFSET = 56;
const shouldUpdateProps = ['searchText', 'loadingServer', 'showServerDropdown', 'showSortDropdown', 'sortBy', 'groupByType', 'showFavorites', 'showUnread', 'useRealName', 'StoreLastMessage', 'appState']; const shouldUpdateProps = [
const getItemLayout = (data, index) => ({ length: ROW_HEIGHT, offset: ROW_HEIGHT * index, index }); 'searchText',
'loadingServer',
'showServerDropdown',
'showSortDropdown',
'sortBy',
'groupByType',
'showFavorites',
'showUnread',
'useRealName',
'StoreLastMessage',
'appState',
'isAuthenticated'
];
const getItemLayout = (data, index) => ({
length: ROW_HEIGHT,
offset: ROW_HEIGHT * index,
index
});
const keyExtractor = item => item.rid; const keyExtractor = item => item.rid;
class RoomsListView extends React.Component { class RoomsListView extends React.Component {
static navigationOptions = ({ navigation }) => { static navigationOptions = ({ navigation }) => {
const searching = navigation.getParam('searching'); const searching = navigation.getParam('searching');
const cancelSearchingAndroid = navigation.getParam('cancelSearchingAndroid'); const cancelSearchingAndroid = navigation.getParam(
'cancelSearchingAndroid'
);
const onPressItem = navigation.getParam('onPressItem', () => {}); const onPressItem = navigation.getParam('onPressItem', () => {});
const initSearchingAndroid = navigation.getParam('initSearchingAndroid', () => {}); const initSearchingAndroid = navigation.getParam(
'initSearchingAndroid',
() => {}
);
return { return {
headerLeft: ( headerLeft: searching ? (
searching
? (
<CustomHeaderButtons left> <CustomHeaderButtons left>
<Item title='cancel' iconName='cross' onPress={cancelSearchingAndroid} /> <Item
title='cancel'
iconName='cross'
onPress={cancelSearchingAndroid}
/>
</CustomHeaderButtons> </CustomHeaderButtons>
) ) : (
: <DrawerButton navigation={navigation} testID='rooms-list-view-sidebar' /> <DrawerButton
navigation={navigation}
testID='rooms-list-view-sidebar'
/>
), ),
headerTitle: <RoomsListHeaderView />, headerTitle: <RoomsListHeaderView />,
headerRight: ( headerRight: searching ? null : (
searching
? null
: (
<CustomHeaderButtons> <CustomHeaderButtons>
{isAndroid ? <Item title='search' iconName='magnifier' onPress={initSearchingAndroid} /> : null} {isAndroid ? (
<Item title='new' iconName='edit-rounded' onPress={() => navigation.navigate('NewMessageView', { onPressItem })} testID='rooms-list-view-create-channel' /> <Item
title='search'
iconName='magnifier'
onPress={initSearchingAndroid}
/>
) : null}
<Item
title='new'
iconName='edit-rounded'
onPress={() => navigation.navigate('NewMessageView', {
onPressItem
})}
testID='rooms-list-view-create-channel'
/>
</CustomHeaderButtons> </CustomHeaderButtons>
) )
)
}; };
} };
static propTypes = { static propTypes = {
navigation: PropTypes.object, navigation: PropTypes.object,
@ -92,7 +141,7 @@ class RoomsListView extends React.Component {
appStart: PropTypes.func, appStart: PropTypes.func,
roomsRequest: PropTypes.func, roomsRequest: PropTypes.func,
isAuthenticated: PropTypes.bool isAuthenticated: PropTypes.bool
} };
constructor(props) { constructor(props) {
super(props); super(props);
@ -100,11 +149,11 @@ class RoomsListView extends React.Component {
console.time(`${ this.constructor.name } mount`); console.time(`${ this.constructor.name } mount`);
const { width } = Dimensions.get('window'); const { width } = Dimensions.get('window');
this.data = [];
this.state = { this.state = {
searching: false, searching: false,
search: [], search: [],
loading: true, loading: true,
allChats: [],
chats: [], chats: [],
unread: [], unread: [],
favorites: [], favorites: [],
@ -112,12 +161,20 @@ class RoomsListView extends React.Component {
channels: [], channels: [],
privateGroup: [], privateGroup: [],
direct: [], direct: [],
livechat: [],
width width
}; };
Orientation.unlockAllOrientations(); Orientation.unlockAllOrientations();
this.didFocusListener = props.navigation.addListener('didFocus', () => BackHandler.addEventListener('hardwareBackPress', this.handleBackPress)); this.didFocusListener = props.navigation.addListener('didFocus', () => {
this.willBlurListener = props.navigation.addListener('willBlur', () => BackHandler.addEventListener('hardwareBackPress', this.handleBackPress)); BackHandler.addEventListener(
'hardwareBackPress',
this.handleBackPress
);
this.forceUpdate();
});
this.willBlurListener = props.navigation.addListener('willBlur', () => BackHandler.addEventListener(
'hardwareBackPress',
this.handleBackPress
));
} }
componentDidMount() { componentDidMount() {
@ -153,51 +210,70 @@ class RoomsListView extends React.Component {
return true; return true;
} }
const { loading, searching, width } = this.state; if (!nextProps.navigation.isFocused()) {
return false;
}
const {
loading,
searching,
width,
allChats,
search
} = this.state;
if (nextState.loading !== loading) { if (nextState.loading !== loading) {
return true; return true;
} }
if (nextState.searching !== searching) { if (nextState.searching !== searching) {
return true; return true;
} }
if (nextState.width !== width) { if (nextState.width !== width) {
return true; return true;
} }
const { search } = this.state;
if (!isEqual(nextState.search, search)) { if (!isEqual(nextState.search, search)) {
return true; return true;
} }
if (!isEqual(nextState.allChats, allChats)) {
return true;
}
return false; return false;
} }
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
const { const {
sortBy, groupByType, showFavorites, showUnread, appState, roomsRequest, isAuthenticated sortBy,
groupByType,
showFavorites,
showUnread,
appState,
roomsRequest,
isAuthenticated
} = this.props; } = this.props;
if (!( if (
(prevProps.sortBy === sortBy) !(
&& (prevProps.groupByType === groupByType) prevProps.sortBy === sortBy
&& (prevProps.showFavorites === showFavorites) && prevProps.groupByType === groupByType
&& (prevProps.showUnread === showUnread) && prevProps.showFavorites === showFavorites
)) { && prevProps.showUnread === showUnread
)
) {
this.getSubscriptions(); this.getSubscriptions();
} else if (appState === 'foreground' && appState !== prevProps.appState && isAuthenticated) { } else if (
appState === 'foreground'
&& appState !== prevProps.appState
&& isAuthenticated
) {
roomsRequest(); roomsRequest();
} }
} }
componentWillUnmount() { componentWillUnmount() {
if (this.data && this.data.removeAllListeners) {
this.data.removeAllListeners();
}
if (this.getSubscriptions && this.getSubscriptions.stop) { if (this.getSubscriptions && this.getSubscriptions.stop) {
this.getSubscriptions.stop(); this.getSubscriptions.stop();
} }
if (this.updateStateInteraction && this.updateStateInteraction.cancel) { if (this.querySubscription && this.querySubscription.unsubscribe) {
this.updateStateInteraction.cancel(); this.querySubscription.unsubscribe();
} }
if (this.didFocusListener && this.didFocusListener.remove) { if (this.didFocusListener && this.didFocusListener.remove) {
this.didFocusListener.remove(); this.didFocusListener.remove();
@ -209,7 +285,7 @@ class RoomsListView extends React.Component {
console.countReset(`${ this.constructor.name }.render calls`); console.countReset(`${ this.constructor.name }.render calls`);
} }
onDimensionsChange = ({ window: { width } }) => this.setState({ width }) onDimensionsChange = ({ window: { width } }) => this.setState({ width });
// eslint-disable-next-line react/sort-comp // eslint-disable-next-line react/sort-comp
internalSetState = (...args) => { internalSetState = (...args) => {
@ -218,77 +294,104 @@ class RoomsListView extends React.Component {
LayoutAnimation.easeInEaseOut(); LayoutAnimation.easeInEaseOut();
} }
this.setState(...args); this.setState(...args);
} };
getSubscriptions = debounce(() => { getSubscriptions = debounce(async() => {
if (this.data && this.data.removeAllListeners) { if (this.querySubscription && this.querySubscription.unsubscribe) {
this.data.removeAllListeners(); this.querySubscription.unsubscribe();
} }
const { const {
server, sortBy, showUnread, showFavorites, groupByType sortBy,
showUnread,
showFavorites,
groupByType
} = this.props; } = this.props;
if (server && this.hasActiveDB()) { const db = database.active;
this.data = database.objects('subscriptions').filtered('archived != true && open == true && t != $0', 'l'); const observable = await db.collections
.get('subscriptions')
.query(
Q.where('archived', false),
Q.where('open', true),
Q.where('t', Q.notEq('l'))
)
.observeWithColumns(['room_updated_at', 'unread', 'alert', 'user_mentions', 'f', 't']);
this.querySubscription = observable.subscribe((data) => {
let chats = [];
let unread = [];
let favorites = [];
let discussions = [];
let channels = [];
let privateGroup = [];
let direct = [];
if (sortBy === 'alphabetical') { if (sortBy === 'alphabetical') {
this.data = this.data.sorted('name', false); chats = orderBy(data, ['name'], ['asc']);
} else { } else {
this.data = this.data.sorted('roomUpdatedAt', true); chats = orderBy(data, ['roomUpdatedAt'], ['desc']);
} }
// it's better to map and test all subs altogether then testing them individually
const allChats = data.map(item => ({
alert: item.alert,
unread: item.unread,
userMentions: item.userMentions,
isRead: this.getIsRead(item),
favorite: item.f,
lastMessage: item.lastMessage,
name: this.getRoomTitle(item),
_updatedAt: item.roomUpdatedAt,
key: item._id,
rid: item.rid,
type: item.t,
prid: item.prid
}));
// unread // unread
if (showUnread) { if (showUnread) {
this.unread = this.data.filtered('(unread > 0 || alert == true)'); unread = chats.filter(s => s.unread > 0 || s.alert);
} else { } else {
this.unread = []; unread = [];
} }
// favorites // favorites
if (showFavorites) { if (showFavorites) {
this.favorites = this.data.filtered('f == true'); favorites = chats.filter(s => s.f);
} else { } else {
this.favorites = []; favorites = [];
} }
// type // type
if (groupByType) { if (groupByType) {
this.discussions = this.data.filtered('prid != null'); discussions = chats.filter(s => s.prid);
this.channels = this.data.filtered('t == $0 AND prid == null', 'c'); channels = chats.filter(s => s.t === 'c' && !s.prid);
this.privateGroup = this.data.filtered('t == $0 AND prid == null', 'p'); privateGroup = chats.filter(s => s.t === 'p' && !s.prid);
this.direct = this.data.filtered('t == $0 AND prid == null', 'd'); direct = chats.filter(s => s.t === 'd' && !s.prid);
this.livechat = this.data.filtered('t == $0 AND prid == null', 'l');
} else if (showUnread) { } else if (showUnread) {
this.chats = this.data.filtered('(unread == 0 && alert == false)'); chats = chats.filter(s => !s.unread && !s.alert);
} else {
this.chats = this.data;
} }
safeAddListener(this.data, this.updateState);
}
}, 300);
// eslint-disable-next-line react/sort-comp
updateState = debounce(() => {
this.updateStateInteraction = InteractionManager.runAfterInteractions(() => {
this.internalSetState({ this.internalSetState({
chats: this.chats ? this.chats.slice() : [], allChats,
unread: this.unread ? this.unread.slice() : [], chats,
favorites: this.favorites ? this.favorites.slice() : [], unread,
discussions: this.discussions ? this.discussions.slice() : [], favorites,
channels: this.channels ? this.channels.slice() : [], discussions,
privateGroup: this.privateGroup ? this.privateGroup.slice() : [], channels,
direct: this.direct ? this.direct.slice() : [], privateGroup,
livechat: this.livechat ? this.livechat.slice() : [], direct,
loading: false loading: false
}); });
this.forceUpdate();
}); });
}, 300); }, 300, true);
initSearchingAndroid = () => { initSearchingAndroid = () => {
const { openSearchHeader, navigation } = this.props; const { openSearchHeader, navigation } = this.props;
this.setState({ searching: true }); this.setState({ searching: true });
navigation.setParams({ searching: true }); navigation.setParams({ searching: true });
openSearchHeader(); openSearchHeader();
} };
cancelSearchingAndroid = () => { cancelSearchingAndroid = () => {
if (isAndroid) { if (isAndroid) {
@ -299,10 +402,7 @@ class RoomsListView extends React.Component {
this.internalSetState({ search: [] }); this.internalSetState({ search: [] });
Keyboard.dismiss(); Keyboard.dismiss();
} }
} };
// this is necessary during development (enables Cmd + r)
hasActiveDB = () => database && database.databases && database.databases.activeDB;
handleBackPress = () => { handleBackPress = () => {
const { searching } = this.state; const { searching } = this.state;
@ -313,29 +413,32 @@ class RoomsListView extends React.Component {
} }
appStart('background'); appStart('background');
return false; return false;
} };
_isUnread = item => item.unread > 0 || item.alert // eslint-disable-next-line react/sort-comp
search = debounce(async(text) => {
search = async(text) => {
const result = await RocketChat.search({ text }); const result = await RocketChat.search({ text });
this.internalSetState({ this.internalSetState({
search: result search: result
}); });
} }, 300);
getRoomTitle = (item) => { getRoomTitle = (item) => {
const { useRealName } = this.props; const { useRealName } = this.props;
return ((item.prid || useRealName) && item.fname) || item.name; return ((item.prid || useRealName) && item.fname) || item.name;
} };
goRoom = (item) => { goRoom = (item) => {
this.cancelSearchingAndroid(); this.cancelSearchingAndroid();
const { navigation } = this.props; const { navigation } = this.props;
navigation.navigate('RoomView', { navigation.navigate('RoomView', {
rid: item.rid, name: this.getRoomTitle(item), t: item.t, prid: item.prid rid: item.rid,
name: this.getRoomTitle(item),
t: item.t,
prid: item.prid,
room: item
}); });
} };
_onPressItem = async(item = {}) => { _onPressItem = async(item = {}) => {
if (!item.search) { if (!item.search) {
@ -347,7 +450,11 @@ class RoomsListView extends React.Component {
const { username } = item; const { username } = item;
const result = await RocketChat.createDirectMessage(username); const result = await RocketChat.createDirectMessage(username);
if (result.success) { if (result.success) {
return this.goRoom({ rid: result.room._id, name: username, t: 'd' }); return this.goRoom({
rid: result.room._id,
name: username,
t: 'd'
});
} }
} catch (e) { } catch (e) {
log(e); log(e);
@ -355,7 +462,7 @@ class RoomsListView extends React.Component {
} else { } else {
return this.goRoom(item); return this.goRoom(item);
} }
} };
toggleSort = () => { toggleSort = () => {
const { toggleSortDropdown } = this.props; const { toggleSortDropdown } = this.props;
@ -369,60 +476,78 @@ class RoomsListView extends React.Component {
setTimeout(() => { setTimeout(() => {
toggleSortDropdown(); toggleSortDropdown();
}, 100); }, 100);
} };
toggleFav = async(rid, favorite) => { toggleFav = async(rid, favorite) => {
try { try {
const db = database.active;
const result = await RocketChat.toggleFavorite(rid, !favorite); const result = await RocketChat.toggleFavorite(rid, !favorite);
if (result.success) { if (result.success) {
database.write(() => { const subCollection = db.collections.get('subscriptions');
const sub = database.objects('subscriptions').filtered('rid == $0', rid)[0]; await db.action(async() => {
if (sub) { try {
const subRecord = await subCollection.find(rid);
await subRecord.update((sub) => {
sub.f = !favorite; sub.f = !favorite;
});
} catch (e) {
log(e);
} }
}); });
} }
} catch (e) { } catch (e) {
log(e); log(e);
} }
} };
toggleRead = async(rid, isRead) => { toggleRead = async(rid, isRead) => {
try { try {
const db = database.active;
const result = await RocketChat.toggleRead(isRead, rid); const result = await RocketChat.toggleRead(isRead, rid);
if (result.success) { if (result.success) {
database.write(() => { const subCollection = db.collections.get('subscriptions');
const sub = database.objects('subscriptions').filtered('rid == $0', rid)[0]; await db.action(async() => {
if (sub) { try {
const subRecord = await subCollection.find(rid);
await subRecord.update((sub) => {
sub.alert = isRead; sub.alert = isRead;
});
} catch (e) {
log(e);
} }
}); });
} }
} catch (e) { } catch (e) {
log(e); log(e);
} }
} };
hideChannel = async(rid, type) => { hideChannel = async(rid, type) => {
try { try {
const db = database.active;
const result = await RocketChat.hideRoom(rid, type); const result = await RocketChat.hideRoom(rid, type);
if (result.success) { if (result.success) {
database.write(() => { const subCollection = db.collections.get('subscriptions');
const sub = database.objects('subscriptions').filtered('rid == $0', rid)[0]; await db.action(async() => {
database.delete(sub); try {
const subRecord = await subCollection.find(rid);
await subRecord.destroyPermanently();
} catch (e) {
log(e);
}
}); });
} }
} catch (e) { } catch (e) {
log(e); log(e);
} }
} };
goDirectory = () => { goDirectory = () => {
const { navigation } = this.props; const { navigation } = this.props;
navigation.navigate('DirectoryView'); navigation.navigate('DirectoryView');
} };
getScrollRef = ref => this.scroll = ref getScrollRef = ref => (this.scroll = ref);
renderListHeader = () => { renderListHeader = () => {
const { search } = this.state; const { search } = this.state;
@ -436,22 +561,25 @@ class RoomsListView extends React.Component {
goDirectory={this.goDirectory} goDirectory={this.goDirectory}
/> />
); );
} };
getIsRead = (item) => { getIsRead = (item) => {
let isUnread = (item.archived !== true && item.open === true); // item is not archived and not opened let isUnread = item.archived !== true && item.open === true; // item is not archived and not opened
isUnread = isUnread && (item.unread > 0 || item.alert === true); // either its unread count > 0 or its alert isUnread = isUnread && (item.unread > 0 || item.alert === true); // either its unread count > 0 or its alert
return !isUnread; return !isUnread;
} };
renderItem = ({ item }) => { renderItem = ({ item }) => {
const { width } = this.state; const { width } = this.state;
const { const {
userId, username, token, baseUrl, StoreLastMessage userId,
username,
token,
baseUrl,
StoreLastMessage
} = this.props; } = this.props;
const id = item.rid.replace(userId, '').trim(); const id = item.rid.replace(userId, '').trim();
if (item.search || (item.isValid && item.isValid())) {
return ( return (
<RoomItem <RoomItem
alert={item.alert} alert={item.alert}
@ -459,7 +587,7 @@ class RoomsListView extends React.Component {
userMentions={item.userMentions} userMentions={item.userMentions}
isRead={this.getIsRead(item)} isRead={this.getIsRead(item)}
favorite={item.f} favorite={item.f}
lastMessage={item.lastMessage ? JSON.parse(JSON.stringify(item.lastMessage)) : null} lastMessage={item.lastMessage}
name={this.getRoomTitle(item)} name={this.getRoomTitle(item)}
_updatedAt={item.roomUpdatedAt} _updatedAt={item.roomUpdatedAt}
key={item._id} key={item._id}
@ -480,15 +608,13 @@ class RoomsListView extends React.Component {
hideChannel={this.hideChannel} hideChannel={this.hideChannel}
/> />
); );
} };
return null;
}
renderSectionHeader = header => ( renderSectionHeader = header => (
<View style={styles.groupTitleContainer}> <View style={styles.groupTitleContainer}>
<Text style={styles.groupTitle}>{I18n.t(header)}</Text> <Text style={styles.groupTitle}>{I18n.t(header)}</Text>
</View> </View>
) );
renderSection = (data, header) => { renderSection = (data, header) => {
const { showUnread, showFavorites, groupByType } = this.props; const { showUnread, showFavorites, groupByType } = this.props;
@ -497,7 +623,15 @@ class RoomsListView extends React.Component {
return null; return null;
} else if (header === 'Favorites' && !showFavorites) { } else if (header === 'Favorites' && !showFavorites) {
return null; return null;
} else if (['Discussions', 'Channels', 'Direct_Messages', 'Private_Groups', 'Livechat'].includes(header) && !groupByType) { } else if (
[
'Discussions',
'Channels',
'Direct_Messages',
'Private_Groups'
].includes(header)
&& !groupByType
) {
return null; return null;
} else if (header === 'Chats' && groupByType) { } else if (header === 'Chats' && groupByType) {
return null; return null;
@ -506,13 +640,14 @@ class RoomsListView extends React.Component {
return ( return (
<FlatList <FlatList
data={data} data={data}
extraData={data}
keyExtractor={keyExtractor} keyExtractor={keyExtractor}
style={styles.list} style={styles.list}
renderItem={this.renderItem} renderItem={this.renderItem}
ListHeaderComponent={() => this.renderSectionHeader(header)} ListHeaderComponent={() => this.renderSectionHeader(header)}
getItemLayout={getItemLayout} getItemLayout={getItemLayout}
enableEmptySections enableEmptySections
removeClippedSubviews removeClippedSubviews={isIOS}
keyboardShouldPersistTaps='always' keyboardShouldPersistTaps='always'
initialNumToRender={12} initialNumToRender={12}
windowSize={7} windowSize={7}
@ -520,11 +655,18 @@ class RoomsListView extends React.Component {
); );
} }
return null; return null;
} };
renderList = () => { renderList = () => {
const { const {
search, chats, unread, favorites, discussions, channels, direct, privateGroup, livechat search,
chats,
unread,
favorites,
discussions,
channels,
direct,
privateGroup
} = this.state; } = this.state;
if (search.length > 0) { if (search.length > 0) {
@ -537,7 +679,7 @@ class RoomsListView extends React.Component {
renderItem={this.renderItem} renderItem={this.renderItem}
getItemLayout={getItemLayout} getItemLayout={getItemLayout}
enableEmptySections enableEmptySections
removeClippedSubviews removeClippedSubviews={isIOS}
keyboardShouldPersistTaps='always' keyboardShouldPersistTaps='always'
initialNumToRender={12} initialNumToRender={12}
windowSize={7} windowSize={7}
@ -553,11 +695,10 @@ class RoomsListView extends React.Component {
{this.renderSection(channels, 'Channels')} {this.renderSection(channels, 'Channels')}
{this.renderSection(direct, 'Direct_Messages')} {this.renderSection(direct, 'Direct_Messages')}
{this.renderSection(privateGroup, 'Private_Groups')} {this.renderSection(privateGroup, 'Private_Groups')}
{this.renderSection(livechat, 'Livechat')}
{this.renderSection(chats, 'Chats')} {this.renderSection(chats, 'Chats')}
</View> </View>
); );
} };
renderScroll = () => { renderScroll = () => {
const { loading } = this.state; const { loading } = this.state;
@ -573,13 +714,14 @@ class RoomsListView extends React.Component {
<FlatList <FlatList
ref={this.getScrollRef} ref={this.getScrollRef}
data={search.length ? search : chats} data={search.length ? search : chats}
extraData={search.length ? search : chats}
contentOffset={isIOS ? { x: 0, y: SCROLL_OFFSET } : {}} contentOffset={isIOS ? { x: 0, y: SCROLL_OFFSET } : {}}
keyExtractor={keyExtractor} keyExtractor={keyExtractor}
style={styles.list} style={styles.list}
renderItem={this.renderItem} renderItem={this.renderItem}
ListHeaderComponent={this.renderListHeader} ListHeaderComponent={this.renderListHeader}
getItemLayout={getItemLayout} getItemLayout={getItemLayout}
// removeClippedSubviews removeClippedSubviews={isIOS}
keyboardShouldPersistTaps='always' keyboardShouldPersistTaps='always'
initialNumToRender={9} initialNumToRender={9}
windowSize={9} windowSize={9}
@ -598,20 +740,28 @@ class RoomsListView extends React.Component {
{this.renderList()} {this.renderList()}
</ScrollView> </ScrollView>
); );
} };
render = () => { render = () => {
console.count(`${ this.constructor.name }.render calls`); console.count(`${ this.constructor.name }.render calls`);
const { const {
sortBy, groupByType, showFavorites, showUnread, showServerDropdown, showSortDropdown sortBy,
groupByType,
showFavorites,
showUnread,
showServerDropdown,
showSortDropdown
} = this.props; } = this.props;
return ( return (
<SafeAreaView style={styles.container} testID='rooms-list-view' forceInset={{ vertical: 'never' }}> <SafeAreaView
style={styles.container}
testID='rooms-list-view'
forceInset={{ vertical: 'never' }}
>
<StatusBar /> <StatusBar />
{this.renderScroll()} {this.renderScroll()}
{showSortDropdown {showSortDropdown ? (
? (
<SortDropdown <SortDropdown
close={this.toggleSort} close={this.toggleSort}
sortBy={sortBy} sortBy={sortBy}
@ -619,13 +769,11 @@ class RoomsListView extends React.Component {
showFavorites={showFavorites} showFavorites={showFavorites}
showUnread={showUnread} showUnread={showUnread}
/> />
) ) : null}
: null
}
{showServerDropdown ? <ServerDropdown /> : null} {showServerDropdown ? <ServerDropdown /> : null}
</SafeAreaView> </SafeAreaView>
); );
} };
} }
const mapStateToProps = state => ({ const mapStateToProps = state => ({

View File

@ -25,7 +25,8 @@ class SearchMessagesView extends React.Component {
static propTypes = { static propTypes = {
navigation: PropTypes.object, navigation: PropTypes.object,
user: PropTypes.object, user: PropTypes.object,
baseUrl: PropTypes.string baseUrl: PropTypes.string,
customEmojis: PropTypes.object
} }
constructor(props) { constructor(props) {
@ -74,6 +75,15 @@ class SearchMessagesView extends React.Component {
} }
}, 1000) }, 1000)
getCustomEmoji = (name) => {
const { customEmojis } = this.props;
const emoji = customEmojis[name];
if (emoji) {
return emoji;
}
return null;
}
renderEmpty = () => ( renderEmpty = () => (
<View style={styles.listEmptyContainer}> <View style={styles.listEmptyContainer}>
<Text style={styles.noDataFound}>{I18n.t('No_results_found')}</Text> <Text style={styles.noDataFound}>{I18n.t('No_results_found')}</Text>
@ -94,6 +104,7 @@ class SearchMessagesView extends React.Component {
isEdited={!!item.editedAt} isEdited={!!item.editedAt}
isHeader isHeader
onOpenFileModal={() => {}} onOpenFileModal={() => {}}
getCustomEmoji={this.getCustomEmoji}
/> />
); );
} }
@ -145,7 +156,8 @@ const mapStateToProps = state => ({
id: state.login.user && state.login.user.id, id: state.login.user && state.login.user.id,
username: state.login.user && state.login.user.username, username: state.login.user && state.login.user.username,
token: state.login.user && state.login.user.token token: state.login.user && state.login.user.token
} },
customEmojis: state.customEmojis
}); });
export default connect(mapStateToProps)(SearchMessagesView); export default connect(mapStateToProps)(SearchMessagesView);

View File

@ -7,7 +7,6 @@ import { connect } from 'react-redux';
import { SafeAreaView } from 'react-navigation'; import { SafeAreaView } from 'react-navigation';
import I18n from '../i18n'; import I18n from '../i18n';
import database from '../lib/realm';
import StatusBar from '../containers/StatusBar'; import StatusBar from '../containers/StatusBar';
import { COLOR_BACKGROUND_CONTAINER } from '../constants/colors'; import { COLOR_BACKGROUND_CONTAINER } from '../constants/colors';
import Navigation from '../lib/ShareNavigation'; import Navigation from '../lib/ShareNavigation';
@ -39,13 +38,14 @@ class SelectServerView extends React.Component {
}) })
static propTypes = { static propTypes = {
server: PropTypes.string server: PropTypes.string,
navigation: PropTypes.object
} }
constructor(props) { constructor(props) {
super(props); super(props);
const { serversDB } = database.databases; const { navigation } = this.props;
const servers = serversDB.objects('servers'); const servers = navigation.getParam('servers', []);
const filteredServers = servers.filter(server => server.roomsUpdatedAt); const filteredServers = servers.filter(server => server.roomsUpdatedAt);
this.state = { this.state = {
servers: filteredServers servers: filteredServers

View File

@ -6,11 +6,13 @@ import {
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { SafeAreaView } from 'react-navigation'; import { SafeAreaView } from 'react-navigation';
import equal from 'deep-equal'; import equal from 'deep-equal';
import { orderBy } from 'lodash';
import { Q } from '@nozbe/watermelondb';
import { import {
addUser as addUserAction, removeUser as removeUserAction, reset as resetAction, setLoading as setLoadingAction addUser as addUserAction, removeUser as removeUserAction, reset as resetAction, setLoading as setLoadingAction
} from '../actions/selectedUsers'; } from '../actions/selectedUsers';
import database, { safeAddListener } from '../lib/realm'; import database from '../lib/database';
import RocketChat from '../lib/rocketchat'; import RocketChat from '../lib/rocketchat';
import UserItem from '../presentation/UserItem'; import UserItem from '../presentation/UserItem';
import Loading from '../containers/Loading'; import Loading from '../containers/Loading';
@ -68,11 +70,11 @@ class SelectedUsersView extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.data = database.objects('subscriptions').filtered('t = $0', 'd').sorted('roomUpdatedAt', true); this.init();
this.state = { this.state = {
search: [] search: [],
chats: []
}; };
safeAddListener(this.data, this.updateState);
} }
componentDidMount() { componentDidMount() {
@ -81,7 +83,7 @@ class SelectedUsersView extends React.Component {
} }
shouldComponentUpdate(nextProps, nextState) { shouldComponentUpdate(nextProps, nextState) {
const { search } = this.state; const { search, chats } = this.state;
const { users, loading } = this.props; const { users, loading } = this.props;
if (nextProps.loading !== loading) { if (nextProps.loading !== loading) {
return true; return true;
@ -92,14 +94,36 @@ class SelectedUsersView extends React.Component {
if (!equal(nextState.search, search)) { if (!equal(nextState.search, search)) {
return true; return true;
} }
if (!equal(nextState.chats, chats)) {
return true;
}
return false; return false;
} }
componentWillUnmount() { componentWillUnmount() {
const { reset } = this.props; const { reset } = this.props;
this.updateState.stop();
this.data.removeAllListeners();
reset(); reset();
if (this.querySubscription && this.querySubscription.unsubscribe) {
this.querySubscription.unsubscribe();
}
}
// eslint-disable-next-line react/sort-comp
init = async() => {
try {
const db = database.active;
const observable = await db.collections
.get('subscriptions')
.query(Q.where('t', 'd'))
.observeWithColumns(['room_updated_at']);
this.querySubscription = observable.subscribe((data) => {
const chats = orderBy(data, ['roomUpdatedAt'], ['desc']);
this.setState({ chats });
});
} catch (e) {
log(e);
}
} }
onSearchChangeText(text) { onSearchChangeText(text) {
@ -208,7 +232,7 @@ class SelectedUsersView extends React.Component {
renderSeparator = () => <View style={[sharedStyles.separator, styles.separator]} /> renderSeparator = () => <View style={[sharedStyles.separator, styles.separator]} />
renderItem = ({ item, index }) => { renderItem = ({ item, index }) => {
const { search } = this.state; const { search, chats } = this.state;
const { baseUrl, user } = this.props; const { baseUrl, user } = this.props;
const name = item.search ? item.name : item.fname; const name = item.search ? item.name : item.fname;
@ -220,7 +244,7 @@ class SelectedUsersView extends React.Component {
if (search.length > 0 && index === search.length - 1) { if (search.length > 0 && index === search.length - 1) {
style = { ...style, ...sharedStyles.separatorBottom }; style = { ...style, ...sharedStyles.separatorBottom };
} }
if (search.length === 0 && index === this.data.length - 1) { if (search.length === 0 && index === chats.length - 1) {
style = { ...style, ...sharedStyles.separatorBottom }; style = { ...style, ...sharedStyles.separatorBottom };
} }
return ( return (
@ -238,10 +262,10 @@ class SelectedUsersView extends React.Component {
} }
renderList = () => { renderList = () => {
const { search } = this.state; const { search, chats } = this.state;
return ( return (
<FlatList <FlatList
data={search.length > 0 ? search : this.data} data={search.length > 0 ? search : chats}
extraData={this.props} extraData={this.props}
keyExtractor={item => item._id} keyExtractor={item => item._id}
renderItem={this.renderItem} renderItem={this.renderItem}

Some files were not shown because too many files have changed in this diff Show More