[RELEASE] Merge beta into master (#1282)
This commit is contained in:
parent
d524ccdb72
commit
0f3cdda77b
|
@ -0,0 +1,7 @@
|
||||||
|
# Reporting Security Issues
|
||||||
|
|
||||||
|
Please report any security issues you discovered to security[at]rocket[dot]chat
|
||||||
|
|
||||||
|
We will assess the risk, plus make a fix available before we create a GitHub issue.
|
||||||
|
|
||||||
|
Thank you for your contribution.
|
|
@ -1,7 +0,0 @@
|
||||||
// @flow
|
|
||||||
/* eslint-disable */
|
|
||||||
import I18nJs from 'i18n-js';
|
|
||||||
|
|
||||||
I18nJs.locale = 'en'; // a locale from your available translations
|
|
||||||
export const getLanguages = (): Promise<string[]> => Promise.resolve(['en']);
|
|
||||||
export default I18nJs;
|
|
|
@ -1 +0,0 @@
|
||||||
export const CachedImage = 'CachedImage';
|
|
|
@ -1,17 +0,0 @@
|
||||||
class Events {
|
|
||||||
registerAppLaunchedListener = () => {}
|
|
||||||
}
|
|
||||||
const events = new Events();
|
|
||||||
class NavigationClass {
|
|
||||||
registerComponent = () => {}
|
|
||||||
|
|
||||||
setRoot = () => {}
|
|
||||||
|
|
||||||
events = () => events
|
|
||||||
}
|
|
||||||
|
|
||||||
const Navigation = new NavigationClass();
|
|
||||||
|
|
||||||
export {
|
|
||||||
Navigation
|
|
||||||
};
|
|
|
@ -1,3 +0,0 @@
|
||||||
export default {
|
|
||||||
realmPath: ''
|
|
||||||
};
|
|
|
@ -1,5 +0,0 @@
|
||||||
export default function() {
|
|
||||||
return {
|
|
||||||
show: () => {}
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1 +0,0 @@
|
||||||
export default () => 'View';
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
export default {
|
||||||
|
NavigationActions: () => {}
|
||||||
|
};
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load Diff
|
@ -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'
|
||||||
|
@ -80,6 +81,7 @@ import com.android.build.OutputFile
|
||||||
|
|
||||||
project.ext.react = [
|
project.ext.react = [
|
||||||
entryFile: "index.js",
|
entryFile: "index.js",
|
||||||
|
bundleAssetName: "app.bundle",
|
||||||
iconFontNames: [ 'custom.ttf' ],
|
iconFontNames: [ 'custom.ttf' ],
|
||||||
enableHermes: false, // clean and rebuild if changing
|
enableHermes: false, // clean and rebuild if changing
|
||||||
]
|
]
|
||||||
|
@ -136,7 +138,7 @@ android {
|
||||||
minSdkVersion rootProject.ext.minSdkVersion
|
minSdkVersion rootProject.ext.minSdkVersion
|
||||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||||
versionCode VERSIONCODE as Integer
|
versionCode VERSIONCODE as Integer
|
||||||
versionName "1.19.0"
|
versionName "1.20.0"
|
||||||
vectorDrawables.useSupportLibrary = true
|
vectorDrawables.useSupportLibrary = true
|
||||||
manifestPlaceholders = [BugsnagAPIKey: BugsnagAPIKey as String]
|
manifestPlaceholders = [BugsnagAPIKey: BugsnagAPIKey as String]
|
||||||
}
|
}
|
||||||
|
@ -180,15 +182,6 @@ android {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
packagingOptions {
|
|
||||||
pickFirst '**/armeabi-v7a/libc++_shared.so'
|
|
||||||
pickFirst '**/x86/libc++_shared.so'
|
|
||||||
pickFirst '**/arm64-v8a/libc++_shared.so'
|
|
||||||
pickFirst '**/x86_64/libc++_shared.so'
|
|
||||||
pickFirst '**/x86/libjsc.so'
|
|
||||||
pickFirst '**/armeabi-v7a/libjsc.so'
|
|
||||||
}
|
|
||||||
|
|
||||||
bundle {
|
bundle {
|
||||||
language {
|
language {
|
||||||
enableSplit = false
|
enableSplit = false
|
||||||
|
@ -204,8 +197,10 @@ android {
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
addUnimodulesDependencies()
|
addUnimodulesDependencies()
|
||||||
|
implementation project(':watermelondb')
|
||||||
implementation project(':reactnativenotifications')
|
implementation project(':reactnativenotifications')
|
||||||
implementation project(":reactnativekeyboardinput")
|
implementation project(":reactnativekeyboardinput")
|
||||||
|
implementation project(':@react-native-community_viewpager')
|
||||||
implementation fileTree(dir: "libs", include: ["*.jar"])
|
implementation fileTree(dir: "libs", include: ["*.jar"])
|
||||||
implementation "com.facebook.react:react-native:+" // From node_modules
|
implementation "com.facebook.react:react-native:+" // From node_modules
|
||||||
implementation "com.google.firebase:firebase-messaging:18.0.0"
|
implementation "com.google.firebase:firebase-messaging:18.0.0"
|
||||||
|
@ -216,7 +211,7 @@ dependencies {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (enableHermes) {
|
if (enableHermes) {
|
||||||
def hermesPath = "../../node_modules/hermesvm/android/";
|
def hermesPath = "../../node_modules/hermes-engine/android/";
|
||||||
debugImplementation files(hermesPath + "hermes-debug.aar")
|
debugImplementation files(hermesPath + "hermes-debug.aar")
|
||||||
releaseImplementation files(hermesPath + "hermes-release.aar")
|
releaseImplementation files(hermesPath + "hermes-release.aar")
|
||||||
} else {
|
} else {
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -1 +0,0 @@
|
||||||
ÖŠ<EFBFBD>tùþ춥Z'ŸFöà.â°'
|
|
|
@ -18,8 +18,8 @@ public class MainActivity extends ReactFragmentActivity {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the name of the main component registered from JavaScript.
|
* Returns the name of the main component registered from JavaScript. This is used to schedule
|
||||||
* This is used to schedule rendering of the component.
|
* rendering of the component.
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
protected String getMainComponentName() {
|
protected String getMainComponentName() {
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
package chat.rocket.reactnative;
|
package chat.rocket.reactnative;
|
||||||
|
|
||||||
import android.app.Application;
|
import android.app.Application;
|
||||||
import android.util.Log;
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import com.facebook.react.PackageList;
|
import com.facebook.react.PackageList;
|
||||||
import com.facebook.hermes.reactexecutor.HermesExecutorFactory;
|
import com.facebook.hermes.reactexecutor.HermesExecutorFactory;
|
||||||
|
@ -31,6 +31,9 @@ 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 com.reactnativecommunity.viewpager.RNCViewPagerPackage;
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
@ -53,6 +56,8 @@ 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 RNCViewPagerPackage());
|
||||||
packages.add(new ModuleRegistryAdapter(mModuleRegistryProvider));
|
packages.add(new ModuleRegistryAdapter(mModuleRegistryProvider));
|
||||||
return packages;
|
return packages;
|
||||||
}
|
}
|
||||||
|
@ -61,6 +66,11 @@ public class MainApplication extends Application implements ReactApplication, IN
|
||||||
protected String getJSMainModuleName() {
|
protected String getJSMainModuleName() {
|
||||||
return "index";
|
return "index";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected @Nullable String getBundleAssetName() {
|
||||||
|
return "app.bundle";
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -6,10 +6,8 @@ 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"
|
supportLibVersion = "28.0.0"
|
||||||
// mediaCompatVersion = '1.0.1'
|
|
||||||
// supportV4Version = '1.0.0'
|
|
||||||
}
|
}
|
||||||
repositories {
|
repositories {
|
||||||
mavenLocal()
|
mavenLocal()
|
||||||
|
@ -20,10 +18,11 @@ buildscript {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:3.4.1'
|
classpath 'com.android.tools.build:gradle:3.4.2'
|
||||||
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
|
||||||
|
@ -42,6 +41,11 @@ allprojects {
|
||||||
// Android JSC is installed from npm
|
// Android JSC is installed from npm
|
||||||
url("$rootDir/../node_modules/jsc-android/dist")
|
url("$rootDir/../node_modules/jsc-android/dist")
|
||||||
}
|
}
|
||||||
|
maven {
|
||||||
|
// We should change it when Jitsi-SDK release v2.4
|
||||||
|
url("$rootDir/../node_modules/react-native-jitsi-meet/jitsi-sdk")
|
||||||
|
// url "https://github.com/jitsi/jitsi-maven-repository/raw/master/releases"
|
||||||
|
}
|
||||||
|
|
||||||
google()
|
google()
|
||||||
jcenter()
|
jcenter()
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-5.5-all.zip
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
|
@ -2,9 +2,13 @@ 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'
|
||||||
project(':reactnativekeyboardinput').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-keyboard-input/lib/android')
|
project(':reactnativekeyboardinput').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-keyboard-input/lib/android')
|
||||||
|
include ':@react-native-community_viewpager'
|
||||||
|
project(':@react-native-community_viewpager').projectDir = new File(rootProject.projectDir, '../node_modules/@react-native-community/viewpager/android')
|
||||||
apply from: file("../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesSettingsGradle(settings)
|
apply from: file("../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesSettingsGradle(settings)
|
||||||
include ':app'
|
include ':app'
|
||||||
|
|
|
@ -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']);
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
import { SET_ACTIVE_USERS } from './actionsTypes';
|
||||||
|
|
||||||
|
export function setActiveUsers(activeUsers) {
|
||||||
|
return {
|
||||||
|
type: SET_ACTIVE_USERS,
|
||||||
|
activeUsers
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
import * as types from './actionsTypes';
|
||||||
|
|
||||||
|
export function setCustomEmojis(emojis) {
|
||||||
|
return {
|
||||||
|
type: types.SET_CUSTOM_EMOJIS,
|
||||||
|
emojis
|
||||||
|
};
|
||||||
|
}
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
};
|
||||||
|
}
|
|
@ -1,77 +0,0 @@
|
||||||
import { View, Animated } from 'react-native';
|
|
||||||
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
export default class Panel extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
open: PropTypes.bool.isRequired,
|
|
||||||
children: PropTypes.node.isRequired,
|
|
||||||
style: PropTypes.object
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this.state = {
|
|
||||||
animation: new Animated.Value()
|
|
||||||
};
|
|
||||||
this.first = true;
|
|
||||||
this.open = false;
|
|
||||||
this.opacity = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
const { animation } = this.state;
|
|
||||||
const { open } = this.props;
|
|
||||||
const initialValue = !open ? this.height : 0;
|
|
||||||
animation.setValue(initialValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillReceiveProps(nextProps) {
|
|
||||||
const { animation } = this.state;
|
|
||||||
const { open } = this.props;
|
|
||||||
|
|
||||||
if (this.first) {
|
|
||||||
this.first = false;
|
|
||||||
if (!open) {
|
|
||||||
animation.setValue(0);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (this.open === nextProps.open) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.open = nextProps.open;
|
|
||||||
const initialValue = !nextProps.open ? this.height : 0;
|
|
||||||
const finalValue = !nextProps.open ? 0 : this.height;
|
|
||||||
|
|
||||||
animation.setValue(initialValue);
|
|
||||||
Animated.timing(
|
|
||||||
animation,
|
|
||||||
{
|
|
||||||
toValue: finalValue,
|
|
||||||
duration: 150,
|
|
||||||
useNativeDriver: true
|
|
||||||
}
|
|
||||||
).start();
|
|
||||||
}
|
|
||||||
|
|
||||||
set _height(h) {
|
|
||||||
this.height = h || this.height;
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { animation } = this.state;
|
|
||||||
const { style, children } = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Animated.View
|
|
||||||
style={[{ height: animation }, style]}
|
|
||||||
>
|
|
||||||
<View onLayout={({ nativeEvent }) => this._height = nativeEvent.layout.height} style={{ position: !this.first ? 'relative' : 'absolute' }}>
|
|
||||||
{children}
|
|
||||||
</View>
|
|
||||||
</Animated.View>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,63 +0,0 @@
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import { Animated, Text } from 'react-native';
|
|
||||||
|
|
||||||
export default class Fade extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
visible: PropTypes.bool.isRequired,
|
|
||||||
style: Animated.View.propTypes.style,
|
|
||||||
children: PropTypes.oneOfType([
|
|
||||||
PropTypes.arrayOf(PropTypes.node),
|
|
||||||
PropTypes.node
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
const { visible } = this.props;
|
|
||||||
this.state = {
|
|
||||||
visible
|
|
||||||
};
|
|
||||||
this._visibility = new Animated.Value(visible ? 1 : 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillReceiveProps(nextProps) {
|
|
||||||
if (nextProps.visible) {
|
|
||||||
this.setState({ visible: true });
|
|
||||||
}
|
|
||||||
Animated.timing(this._visibility, {
|
|
||||||
toValue: nextProps.visible ? 1 : 0,
|
|
||||||
duration: 300,
|
|
||||||
useNativeDriver: true
|
|
||||||
}).start(() => {
|
|
||||||
this.setState({ visible: nextProps.visible });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { visible } = this.state;
|
|
||||||
const { style, children, ...rest } = this.props;
|
|
||||||
|
|
||||||
const containerStyle = {
|
|
||||||
opacity: this._visibility.interpolate({
|
|
||||||
inputRange: [0, 1],
|
|
||||||
outputRange: [0, 1]
|
|
||||||
}),
|
|
||||||
transform: [
|
|
||||||
{
|
|
||||||
scale: this._visibility.interpolate({
|
|
||||||
inputRange: [0, 1],
|
|
||||||
outputRange: [1.1, 1]
|
|
||||||
})
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
const combinedStyle = [containerStyle, style];
|
|
||||||
return (
|
|
||||||
<Animated.View style={visible ? combinedStyle : containerStyle} {...rest}>
|
|
||||||
<Text>{visible ? children : null}</Text>
|
|
||||||
</Animated.View>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -23,6 +23,18 @@ export default {
|
||||||
LDAP_Enable: {
|
LDAP_Enable: {
|
||||||
type: 'valueAsBoolean'
|
type: 'valueAsBoolean'
|
||||||
},
|
},
|
||||||
|
Jitsi_Enabled: {
|
||||||
|
type: 'valueAsBoolean'
|
||||||
|
},
|
||||||
|
Jitsi_SSL: {
|
||||||
|
type: 'valueAsBoolean'
|
||||||
|
},
|
||||||
|
Jitsi_Domain: {
|
||||||
|
type: 'valueAsString'
|
||||||
|
},
|
||||||
|
Jitsi_URL_Room_Prefix: {
|
||||||
|
type: 'valueAsString'
|
||||||
|
},
|
||||||
Message_AllowDeleting: {
|
Message_AllowDeleting: {
|
||||||
type: 'valueAsBoolean'
|
type: 'valueAsBoolean'
|
||||||
},
|
},
|
||||||
|
@ -56,6 +68,9 @@ export default {
|
||||||
Store_Last_Message: {
|
Store_Last_Message: {
|
||||||
type: 'valueAsBoolean'
|
type: 'valueAsBoolean'
|
||||||
},
|
},
|
||||||
|
uniqueID: {
|
||||||
|
type: 'valueAsString'
|
||||||
|
},
|
||||||
UI_Use_Real_Name: {
|
UI_Use_Real_Name: {
|
||||||
type: 'valueAsBoolean'
|
type: 'valueAsBoolean'
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,25 +1,26 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Image } from 'react-native';
|
import FastImage from 'react-native-fast-image';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
export default class CustomEmoji extends React.Component {
|
const CustomEmoji = React.memo(({ baseUrl, emoji, style }) => (
|
||||||
static propTypes = {
|
<FastImage
|
||||||
|
style={style}
|
||||||
|
source={{
|
||||||
|
uri: `${ baseUrl }/emoji-custom/${ encodeURIComponent(emoji.content || emoji.name) }.${ emoji.extension }`,
|
||||||
|
priority: FastImage.priority.high
|
||||||
|
}}
|
||||||
|
resizeMode={FastImage.resizeMode.contain}
|
||||||
|
/>
|
||||||
|
), (prevProps, nextProps) => {
|
||||||
|
const prevEmoji = prevProps.emoji.content || prevProps.emoji.name;
|
||||||
|
const nextEmoji = nextProps.emoji.content || nextProps.emoji.name;
|
||||||
|
return prevEmoji === nextEmoji;
|
||||||
|
});
|
||||||
|
|
||||||
|
CustomEmoji.propTypes = {
|
||||||
baseUrl: PropTypes.string.isRequired,
|
baseUrl: PropTypes.string.isRequired,
|
||||||
emoji: PropTypes.object.isRequired,
|
emoji: PropTypes.object.isRequired,
|
||||||
style: PropTypes.any
|
style: PropTypes.any
|
||||||
}
|
};
|
||||||
|
|
||||||
shouldComponentUpdate() {
|
export default CustomEmoji;
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { baseUrl, emoji, style } = this.props;
|
|
||||||
return (
|
|
||||||
<Image
|
|
||||||
style={style}
|
|
||||||
source={{ uri: `${ baseUrl }/emoji-custom/${ encodeURIComponent(emoji.content || emoji.name) }.${ emoji.extension }` }}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Text, TouchableOpacity } from 'react-native';
|
import { Text, TouchableOpacity } from 'react-native';
|
||||||
import { emojify } from 'react-emojione';
|
import { shortnameToUnicode } from 'emoji-toolkit';
|
||||||
import { responsive } from 'react-native-responsive-ui';
|
import { responsive } from 'react-native-responsive-ui';
|
||||||
import { OptimizedFlatList } from 'react-native-optimized-flatlist';
|
import { OptimizedFlatList } from 'react-native-optimized-flatlist';
|
||||||
|
|
||||||
|
@ -18,7 +18,7 @@ const renderEmoji = (emoji, size, baseUrl) => {
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Text style={[styles.categoryEmoji, { height: size, width: size, fontSize: size - 14 }]}>
|
<Text style={[styles.categoryEmoji, { height: size, width: size, fontSize: size - 14 }]}>
|
||||||
{emojify(`:${ emoji }:`, { output: 'unicode' })}
|
{shortnameToUnicode(`:${ emoji }:`)}
|
||||||
</Text>
|
</Text>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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 { shortnameToUnicode } from 'emoji-toolkit';
|
||||||
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(shortnameToUnicode(shortname), 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 shortnameToUnicode(`${ item.content }`);
|
||||||
});
|
});
|
||||||
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);
|
||||||
|
|
|
@ -93,7 +93,7 @@ const ModalContent = React.memo(({
|
||||||
rate={1.0}
|
rate={1.0}
|
||||||
volume={1.0}
|
volume={1.0}
|
||||||
isMuted={false}
|
isMuted={false}
|
||||||
resizeMode='cover'
|
resizeMode={Video.RESIZE_MODE_CONTAIN}
|
||||||
shouldPlay
|
shouldPlay
|
||||||
isLooping={false}
|
isLooping={false}
|
||||||
style={styles.video}
|
style={styles.video}
|
||||||
|
|
|
@ -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.subscription.id);
|
||||||
|
} 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);
|
|
||||||
|
|
|
@ -5,6 +5,7 @@ import {
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { AudioRecorder, AudioUtils } from 'react-native-audio';
|
import { AudioRecorder, AudioUtils } from 'react-native-audio';
|
||||||
import { BorderlessButton } from 'react-native-gesture-handler';
|
import { BorderlessButton } from 'react-native-gesture-handler';
|
||||||
|
import RNFetchBlob from 'rn-fetch-blob';
|
||||||
|
|
||||||
import styles from './styles';
|
import styles from './styles';
|
||||||
import I18n from '../../i18n';
|
import I18n from '../../i18n';
|
||||||
|
@ -68,7 +69,7 @@ export default class extends React.PureComponent {
|
||||||
//
|
//
|
||||||
AudioRecorder.onFinished = (data) => {
|
AudioRecorder.onFinished = (data) => {
|
||||||
if (!this.recordingCanceled && isIOS) {
|
if (!this.recordingCanceled && isIOS) {
|
||||||
this.finishRecording(data.status === 'OK', data.audioFileURL);
|
this.finishRecording(data.status === 'OK', data.audioFileURL, data.audioFileSize);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
AudioRecorder.startRecording();
|
AudioRecorder.startRecording();
|
||||||
|
@ -80,7 +81,7 @@ export default class extends React.PureComponent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
finishRecording = (didSucceed, filePath) => {
|
finishRecording = (didSucceed, filePath, size) => {
|
||||||
const { onFinish } = this.props;
|
const { onFinish } = this.props;
|
||||||
if (!didSucceed) {
|
if (!didSucceed) {
|
||||||
return onFinish && onFinish(didSucceed);
|
return onFinish && onFinish(didSucceed);
|
||||||
|
@ -90,9 +91,11 @@ export default class extends React.PureComponent {
|
||||||
}
|
}
|
||||||
const fileInfo = {
|
const fileInfo = {
|
||||||
name: this.name,
|
name: this.name,
|
||||||
|
mime: 'audio/aac',
|
||||||
type: 'audio/aac',
|
type: 'audio/aac',
|
||||||
store: 'Uploads',
|
store: 'Uploads',
|
||||||
path: filePath
|
path: filePath,
|
||||||
|
size
|
||||||
};
|
};
|
||||||
return onFinish && onFinish(fileInfo);
|
return onFinish && onFinish(fileInfo);
|
||||||
}
|
}
|
||||||
|
@ -102,7 +105,8 @@ export default class extends React.PureComponent {
|
||||||
this.recording = false;
|
this.recording = false;
|
||||||
const filePath = await AudioRecorder.stopRecording();
|
const filePath = await AudioRecorder.stopRecording();
|
||||||
if (isAndroid) {
|
if (isAndroid) {
|
||||||
this.finishRecording(true, filePath);
|
const data = await RNFetchBlob.fs.stat(decodeURIComponent(filePath));
|
||||||
|
this.finishRecording(true, filePath, data.size);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.finishRecording(false);
|
this.finishRecording(false);
|
||||||
|
|
|
@ -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 (
|
||||||
|
@ -79,7 +79,7 @@ class ReplyPreview extends Component {
|
||||||
<Text style={styles.username}>{message.u.username}</Text>
|
<Text style={styles.username}>{message.u.username}</Text>
|
||||||
<Text style={styles.time}>{time}</Text>
|
<Text style={styles.time}>{time}</Text>
|
||||||
</View>
|
</View>
|
||||||
<Markdown msg={message.msg} baseUrl={baseUrl} username={username} getCustomEmoji={getCustomEmoji} numberOfLines={1} useMarkdown={useMarkdown} />
|
<Markdown msg={message.msg} baseUrl={baseUrl} username={username} getCustomEmoji={getCustomEmoji} numberOfLines={1} useMarkdown={useMarkdown} preview />
|
||||||
</View>
|
</View>
|
||||||
<CustomIcon name='cross' color={COLOR_TEXT_DESCRIPTION} size={20} style={styles.close} onPress={this.close} />
|
<CustomIcon name='cross' color={COLOR_TEXT_DESCRIPTION} size={20} style={styles.close} onPress={this.close} />
|
||||||
</View>
|
</View>
|
||||||
|
|
|
@ -10,10 +10,10 @@ const RightButtons = React.memo(({
|
||||||
return <SendButton onPress={submit} />;
|
return <SendButton onPress={submit} />;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<>
|
||||||
<AudioButton onPress={recordAudioMessage} />
|
<AudioButton onPress={recordAudioMessage} />
|
||||||
<FileButton onPress={showFileActions} />
|
<FileButton onPress={showFileActions} />
|
||||||
</React.Fragment>
|
</>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,6 @@ import React, { Component } from 'react';
|
||||||
import {
|
import {
|
||||||
View, Text, StyleSheet, Image, ScrollView, TouchableHighlight
|
View, Text, StyleSheet, Image, ScrollView, TouchableHighlight
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import Modal from 'react-native-modal';
|
import Modal from 'react-native-modal';
|
||||||
import { responsive } from 'react-native-responsive-ui';
|
import { responsive } from 'react-native-responsive-ui';
|
||||||
|
@ -13,9 +12,8 @@ import Button from '../Button';
|
||||||
import I18n from '../../i18n';
|
import I18n from '../../i18n';
|
||||||
import sharedStyles from '../../views/Styles';
|
import sharedStyles from '../../views/Styles';
|
||||||
import { isIOS } from '../../utils/deviceInfo';
|
import { isIOS } from '../../utils/deviceInfo';
|
||||||
import { canUploadFile } from '../../utils/media';
|
|
||||||
import {
|
import {
|
||||||
COLOR_PRIMARY, COLOR_BACKGROUND_CONTAINER, COLOR_WHITE, COLOR_DANGER
|
COLOR_PRIMARY, COLOR_BACKGROUND_CONTAINER, COLOR_WHITE
|
||||||
} from '../../constants/colors';
|
} from '../../constants/colors';
|
||||||
import { CustomIcon } from '../../lib/Icons';
|
import { CustomIcon } from '../../lib/Icons';
|
||||||
|
|
||||||
|
@ -75,23 +73,6 @@ const styles = StyleSheet.create({
|
||||||
flex: 1,
|
flex: 1,
|
||||||
textAlign: 'center'
|
textAlign: 'center'
|
||||||
},
|
},
|
||||||
errorIcon: {
|
|
||||||
color: COLOR_DANGER
|
|
||||||
},
|
|
||||||
fileMime: {
|
|
||||||
...sharedStyles.textColorTitle,
|
|
||||||
...sharedStyles.textBold,
|
|
||||||
textAlign: 'center',
|
|
||||||
fontSize: 20,
|
|
||||||
marginBottom: 20
|
|
||||||
},
|
|
||||||
errorContainer: {
|
|
||||||
margin: 20,
|
|
||||||
flex: 1,
|
|
||||||
textAlign: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center'
|
|
||||||
},
|
|
||||||
video: {
|
video: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
borderRadius: 4,
|
borderRadius: 4,
|
||||||
|
@ -110,9 +91,7 @@ class UploadModal extends Component {
|
||||||
file: PropTypes.object,
|
file: PropTypes.object,
|
||||||
close: PropTypes.func,
|
close: PropTypes.func,
|
||||||
submit: PropTypes.func,
|
submit: PropTypes.func,
|
||||||
window: PropTypes.object,
|
window: PropTypes.object
|
||||||
FileUpload_MediaTypeWhiteList: PropTypes.string,
|
|
||||||
FileUpload_MaxFileSize: PropTypes.number
|
|
||||||
}
|
}
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
|
@ -154,79 +133,12 @@ class UploadModal extends Component {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
canUploadFile = () => {
|
|
||||||
const { FileUpload_MediaTypeWhiteList, FileUpload_MaxFileSize, file } = this.props;
|
|
||||||
if (!(file && file.path)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (file.size > FileUpload_MaxFileSize) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
// if white list is empty, all media types are enabled
|
|
||||||
if (!FileUpload_MediaTypeWhiteList) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
const allowedMime = FileUpload_MediaTypeWhiteList.split(',');
|
|
||||||
if (allowedMime.includes(file.mime)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
const wildCardGlob = '/*';
|
|
||||||
const wildCards = allowedMime.filter(item => item.indexOf(wildCardGlob) > 0);
|
|
||||||
if (wildCards.includes(file.mime.replace(/(\/.*)$/, wildCardGlob))) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
submit = () => {
|
submit = () => {
|
||||||
const { file, submit } = this.props;
|
const { file, submit } = this.props;
|
||||||
const { name, description } = this.state;
|
const { name, description } = this.state;
|
||||||
submit({ ...file, name, description });
|
submit({ ...file, name, description });
|
||||||
}
|
}
|
||||||
|
|
||||||
renderError = () => {
|
|
||||||
const { file, FileUpload_MaxFileSize, close } = this.props;
|
|
||||||
const { window: { width } } = this.props;
|
|
||||||
const errorMessage = (FileUpload_MaxFileSize < file.size)
|
|
||||||
? 'error-file-too-large'
|
|
||||||
: 'error-invalid-file-type';
|
|
||||||
return (
|
|
||||||
<View style={[styles.container, { width: width - 32 }]}>
|
|
||||||
<View style={styles.titleContainer}>
|
|
||||||
<Text style={styles.title}>{I18n.t(errorMessage)}</Text>
|
|
||||||
</View>
|
|
||||||
<View style={styles.errorContainer}>
|
|
||||||
<CustomIcon name='circle-cross' size={120} style={styles.errorIcon} />
|
|
||||||
</View>
|
|
||||||
<Text style={styles.fileMime}>{ file.mime }</Text>
|
|
||||||
<View style={styles.buttonContainer}>
|
|
||||||
{
|
|
||||||
(isIOS)
|
|
||||||
? (
|
|
||||||
<Button
|
|
||||||
title={I18n.t('Cancel')}
|
|
||||||
type='secondary'
|
|
||||||
backgroundColor={cancelButtonColor}
|
|
||||||
style={styles.button}
|
|
||||||
onPress={close}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
: (
|
|
||||||
<TouchableHighlight
|
|
||||||
onPress={close}
|
|
||||||
style={[styles.androidButton, { backgroundColor: cancelButtonColor }]}
|
|
||||||
underlayColor={cancelButtonColor}
|
|
||||||
activeOpacity={0.5}
|
|
||||||
>
|
|
||||||
<Text style={[styles.androidButtonText, { ...sharedStyles.textBold, color: COLOR_PRIMARY }]}>{I18n.t('Cancel')}</Text>
|
|
||||||
</TouchableHighlight>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
renderButtons = () => {
|
renderButtons = () => {
|
||||||
const { close } = this.props;
|
const { close } = this.props;
|
||||||
if (isIOS) {
|
if (isIOS) {
|
||||||
|
@ -288,10 +200,9 @@ class UploadModal extends Component {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
window: { width }, isVisible, close, file, FileUpload_MediaTypeWhiteList, FileUpload_MaxFileSize
|
window: { width }, isVisible, close
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const { name, description } = this.state;
|
const { name, description } = this.state;
|
||||||
const showError = !canUploadFile(file, { FileUpload_MediaTypeWhiteList, FileUpload_MaxFileSize });
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
isVisible={isVisible}
|
isVisible={isVisible}
|
||||||
|
@ -304,8 +215,6 @@ class UploadModal extends Component {
|
||||||
hideModalContentWhileAnimating
|
hideModalContentWhileAnimating
|
||||||
avoidKeyboard
|
avoidKeyboard
|
||||||
>
|
>
|
||||||
{(showError) ? this.renderError()
|
|
||||||
: (
|
|
||||||
<View style={[styles.container, { width: width - 32 }]}>
|
<View style={[styles.container, { width: width - 32 }]}>
|
||||||
<View style={styles.titleContainer}>
|
<View style={styles.titleContainer}>
|
||||||
<Text style={styles.title}>{I18n.t('Upload_file_question_mark')}</Text>
|
<Text style={styles.title}>{I18n.t('Upload_file_question_mark')}</Text>
|
||||||
|
@ -326,15 +235,9 @@ class UploadModal extends Component {
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
{this.renderButtons()}
|
{this.renderButtons()}
|
||||||
</View>
|
</View>
|
||||||
)}
|
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
export default responsive(UploadModal);
|
||||||
FileUpload_MediaTypeWhiteList: state.settings.FileUpload_MediaTypeWhiteList,
|
|
||||||
FileUpload_MaxFileSize: state.settings.FileUpload_MaxFileSize
|
|
||||||
});
|
|
||||||
|
|
||||||
export default responsive(connect(mapStateToProps)(UploadModal));
|
|
||||||
|
|
|
@ -4,22 +4,18 @@ import {
|
||||||
View, TextInput, FlatList, Text, TouchableOpacity, Alert, ScrollView
|
View, TextInput, FlatList, Text, TouchableOpacity, Alert, ScrollView
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { emojify } from 'react-emojione';
|
import { shortnameToUnicode } from 'emoji-toolkit';
|
||||||
import { KeyboardAccessoryView } from 'react-native-keyboard-input';
|
import { KeyboardAccessoryView } from 'react-native-keyboard-input';
|
||||||
import ImagePicker from 'react-native-image-crop-picker';
|
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';
|
||||||
|
@ -34,16 +30,13 @@ import LeftButtons from './LeftButtons';
|
||||||
import RightButtons from './RightButtons';
|
import RightButtons from './RightButtons';
|
||||||
import { isAndroid } from '../../utils/deviceInfo';
|
import { isAndroid } from '../../utils/deviceInfo';
|
||||||
import CommandPreview from './CommandPreview';
|
import CommandPreview from './CommandPreview';
|
||||||
|
import { canUploadFile } from '../../utils/media';
|
||||||
|
|
||||||
const MENTIONS_TRACKING_TYPE_USERS = '@';
|
const MENTIONS_TRACKING_TYPE_USERS = '@';
|
||||||
const MENTIONS_TRACKING_TYPE_EMOJIS = ':';
|
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 +62,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 +73,15 @@ class MessageBox extends Component {
|
||||||
}),
|
}),
|
||||||
roomType: PropTypes.string,
|
roomType: PropTypes.string,
|
||||||
tmid: PropTypes.string,
|
tmid: PropTypes.string,
|
||||||
|
replyWithMention: PropTypes.bool,
|
||||||
|
FileUpload_MediaTypeWhiteList: PropTypes.string,
|
||||||
|
FileUpload_MaxFileSize: PropTypes.number,
|
||||||
|
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) {
|
||||||
|
@ -99,14 +95,9 @@ class MessageBox extends Component {
|
||||||
file: {
|
file: {
|
||||||
isVisible: false
|
isVisible: false
|
||||||
},
|
},
|
||||||
commandPreview: []
|
commandPreview: [],
|
||||||
|
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 +126,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 +165,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 +185,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,29 +222,61 @@ class MessageBox extends Component {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
onChangeText = debounce((text) => {
|
componentWillUnmount() {
|
||||||
|
console.countReset(`${ this.constructor.name }.render calls`);
|
||||||
|
if (this.onChangeText && this.onChangeText.stop) {
|
||||||
|
this.onChangeText.stop();
|
||||||
|
}
|
||||||
|
if (this.getUsers && this.getUsers.stop) {
|
||||||
|
this.getUsers.stop();
|
||||||
|
}
|
||||||
|
if (this.getRooms && this.getRooms.stop) {
|
||||||
|
this.getRooms.stop();
|
||||||
|
}
|
||||||
|
if (this.getEmojis && this.getEmojis.stop) {
|
||||||
|
this.getEmojis.stop();
|
||||||
|
}
|
||||||
|
if (this.getSlashCommands && this.getSlashCommands.stop) {
|
||||||
|
this.getSlashCommands.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onChangeText = (text) => {
|
||||||
const isTextEmpty = text.length === 0;
|
const isTextEmpty = text.length === 0;
|
||||||
this.setShowSend(!isTextEmpty);
|
this.setShowSend(!isTextEmpty);
|
||||||
|
this.debouncedOnChangeText(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line react/sort-comp
|
||||||
|
debouncedOnChangeText = debounce(async(text) => {
|
||||||
|
const db = database.active;
|
||||||
|
const isTextEmpty = text.length === 0;
|
||||||
|
// this.setShowSend(!isTextEmpty);
|
||||||
this.handleTyping(!isTextEmpty);
|
this.handleTyping(!isTextEmpty);
|
||||||
this.setInput(text);
|
this.setInput(text);
|
||||||
// matches if their is text that stats with '/' and group the command and params so we can use it "/command params"
|
// matches if their is text that stats with '/' and group the command and params so we can use it "/command params"
|
||||||
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) {
|
||||||
|
try {
|
||||||
const { start, end } = this.component._lastNativeSelection;
|
const { start, end } = this.component._lastNativeSelection;
|
||||||
const cursor = Math.max(start, end);
|
const cursor = Math.max(start, end);
|
||||||
const lastNativeText = this.component._lastNativeText;
|
const lastNativeText = this.component._lastNativeText || '';
|
||||||
// matches if text either starts with '/' or have (@,#,:) then it groups whatever comes next of mention type
|
// matches if text either starts with '/' or have (@,#,:) then it groups whatever comes next of mention type
|
||||||
const regexp = /(#|@|:|^\/)([a-z0-9._-]+)$/im;
|
const regexp = /(#|@|:|^\/)([a-z0-9._-]+)$/im;
|
||||||
const result = lastNativeText.substr(0, cursor).match(regexp);
|
const result = lastNativeText.substr(0, cursor).match(regexp);
|
||||||
this.showCommandPreview = false;
|
|
||||||
if (!result) {
|
if (!result) {
|
||||||
const slash = lastNativeText.match(/^\/$/); // matches only '/' in input
|
const slash = lastNativeText.match(/^\/$/); // matches only '/' in input
|
||||||
if (slash) {
|
if (slash) {
|
||||||
|
@ -249,9 +286,11 @@ class MessageBox extends Component {
|
||||||
}
|
}
|
||||||
const [, lastChar, name] = result;
|
const [, lastChar, name] = result;
|
||||||
this.identifyMentionKeyword(name, lastChar);
|
this.identifyMentionKeyword(name, lastChar);
|
||||||
|
} catch (e) {
|
||||||
|
log(e);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
this.stopTrackingMention();
|
this.stopTrackingMention();
|
||||||
this.showCommandPreview = false;
|
|
||||||
}
|
}
|
||||||
}, 100)
|
}, 100)
|
||||||
|
|
||||||
|
@ -274,7 +313,7 @@ class MessageBox extends Component {
|
||||||
: (item.username || item.name || item.command);
|
: (item.username || item.name || item.command);
|
||||||
const text = `${ result }${ mentionName } ${ msg.slice(cursor) }`;
|
const text = `${ result }${ mentionName } ${ msg.slice(cursor) }`;
|
||||||
if ((trackingType === MENTIONS_TRACKING_TYPE_COMMANDS) && item.providesPreview) {
|
if ((trackingType === MENTIONS_TRACKING_TYPE_COMMANDS) && item.providesPreview) {
|
||||||
this.showCommandPreview = true;
|
this.setState({ showCommandPreview: true });
|
||||||
}
|
}
|
||||||
this.setInput(text);
|
this.setInput(text);
|
||||||
this.focus();
|
this.focus();
|
||||||
|
@ -286,10 +325,10 @@ class MessageBox extends Component {
|
||||||
const { text } = this;
|
const { text } = this;
|
||||||
const command = text.substr(0, text.indexOf(' ')).slice(1);
|
const command = text.substr(0, text.indexOf(' ')).slice(1);
|
||||||
const params = text.substr(text.indexOf(' ') + 1) || 'params';
|
const params = text.substr(text.indexOf(' ') + 1) || 'params';
|
||||||
this.showCommandPreview = false;
|
this.setState({ commandPreview: [], showCommandPreview: false });
|
||||||
this.setState({ commandPreview: [] });
|
|
||||||
this.stopTrackingMention();
|
this.stopTrackingMention();
|
||||||
this.clearInput();
|
this.clearInput();
|
||||||
|
this.handleTyping(false);
|
||||||
try {
|
try {
|
||||||
RocketChat.executeCommandPreview(command, params, rid, item);
|
RocketChat.executeCommandPreview(command, params, rid, item);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -324,114 +363,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) {
|
||||||
|
@ -464,10 +438,9 @@ class MessageBox extends Component {
|
||||||
const { rid } = this.props;
|
const { rid } = this.props;
|
||||||
try {
|
try {
|
||||||
const { preview } = await RocketChat.getCommandPreview(command, rid, params);
|
const { preview } = await RocketChat.getCommandPreview(command, rid, params);
|
||||||
this.showCommandPreview = true;
|
this.setState({ commandPreview: preview.items, showCommandPreview: true });
|
||||||
this.setState({ commandPreview: preview.items });
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.showCommandPreview = false;
|
this.setState({ commandPreview: [], showCommandPreview: true });
|
||||||
log(e);
|
log(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -488,6 +461,16 @@ class MessageBox extends Component {
|
||||||
this.setShowSend(false);
|
this.setShowSend(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
canUploadFile = (file) => {
|
||||||
|
const { FileUpload_MediaTypeWhiteList, FileUpload_MaxFileSize } = this.props;
|
||||||
|
const result = canUploadFile(file, { FileUpload_MediaTypeWhiteList, FileUpload_MaxFileSize });
|
||||||
|
if (result.success) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
Alert.alert(I18n.t('Error_uploading'), I18n.t(result.error));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
sendMediaMessage = async(file) => {
|
sendMediaMessage = async(file) => {
|
||||||
const {
|
const {
|
||||||
rid, tmid, baseUrl: server, user
|
rid, tmid, baseUrl: server, user
|
||||||
|
@ -511,7 +494,9 @@ class MessageBox extends Component {
|
||||||
takePhoto = async() => {
|
takePhoto = async() => {
|
||||||
try {
|
try {
|
||||||
const image = await ImagePicker.openCamera(this.imagePickerConfig);
|
const image = await ImagePicker.openCamera(this.imagePickerConfig);
|
||||||
|
if (this.canUploadFile(image)) {
|
||||||
this.showUploadModal(image);
|
this.showUploadModal(image);
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log(e);
|
log(e);
|
||||||
}
|
}
|
||||||
|
@ -520,7 +505,9 @@ class MessageBox extends Component {
|
||||||
takeVideo = async() => {
|
takeVideo = async() => {
|
||||||
try {
|
try {
|
||||||
const video = await ImagePicker.openCamera(this.videoPickerConfig);
|
const video = await ImagePicker.openCamera(this.videoPickerConfig);
|
||||||
|
if (this.canUploadFile(video)) {
|
||||||
this.showUploadModal(video);
|
this.showUploadModal(video);
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log(e);
|
log(e);
|
||||||
}
|
}
|
||||||
|
@ -529,7 +516,9 @@ class MessageBox extends Component {
|
||||||
chooseFromLibrary = async() => {
|
chooseFromLibrary = async() => {
|
||||||
try {
|
try {
|
||||||
const image = await ImagePicker.openPicker(this.libraryPickerConfig);
|
const image = await ImagePicker.openPicker(this.libraryPickerConfig);
|
||||||
|
if (this.canUploadFile(image)) {
|
||||||
this.showUploadModal(image);
|
this.showUploadModal(image);
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log(e);
|
log(e);
|
||||||
}
|
}
|
||||||
|
@ -540,12 +529,15 @@ class MessageBox extends Component {
|
||||||
const res = await DocumentPicker.pick({
|
const res = await DocumentPicker.pick({
|
||||||
type: [DocumentPicker.types.allFiles]
|
type: [DocumentPicker.types.allFiles]
|
||||||
});
|
});
|
||||||
this.showUploadModal({
|
const file = {
|
||||||
filename: res.name,
|
filename: res.name,
|
||||||
size: res.size,
|
size: res.size,
|
||||||
mime: res.type,
|
mime: res.type,
|
||||||
path: res.uri
|
path: res.uri
|
||||||
});
|
};
|
||||||
|
if (this.canUploadFile(file)) {
|
||||||
|
this.showUploadModal(file);
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (!DocumentPicker.isCancel(e)) {
|
if (!DocumentPicker.isCancel(e)) {
|
||||||
log(e);
|
log(e);
|
||||||
|
@ -613,11 +605,10 @@ class MessageBox extends Component {
|
||||||
});
|
});
|
||||||
if (fileInfo) {
|
if (fileInfo) {
|
||||||
try {
|
try {
|
||||||
|
if (this.canUploadFile(fileInfo)) {
|
||||||
await RocketChat.sendFileMessage(rid, fileInfo, tmid, server, user);
|
await RocketChat.sendFileMessage(rid, fileInfo, tmid, server, user);
|
||||||
} catch (e) {
|
|
||||||
if (e && e.error === 'error-file-too-large') {
|
|
||||||
return Alert.alert(I18n.t(e.error));
|
|
||||||
}
|
}
|
||||||
|
} catch (e) {
|
||||||
log(e);
|
log(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -629,7 +620,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,13 +637,16 @@ 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.replace(/([^\s]+)/, '').trim();
|
||||||
RocketChat.runSlashCommand(command, roomId, messageWithoutCommand);
|
RocketChat.runSlashCommand(command, roomId, messageWithoutCommand);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log(e);
|
log(e);
|
||||||
|
@ -663,32 +657,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 {
|
||||||
|
@ -717,20 +714,16 @@ class MessageBox extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
stopTrackingMention = () => {
|
stopTrackingMention = () => {
|
||||||
const { trackingType } = this.state;
|
const { trackingType, showCommandPreview } = this.state;
|
||||||
if (!trackingType) {
|
if (!trackingType && !showCommandPreview) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.setState({
|
this.setState({
|
||||||
mentions: [],
|
mentions: [],
|
||||||
trackingType: '',
|
trackingType: '',
|
||||||
commandPreview: []
|
commandPreview: [],
|
||||||
|
showCommandPreview: false
|
||||||
});
|
});
|
||||||
this.users = [];
|
|
||||||
this.rooms = [];
|
|
||||||
this.customEmojis = [];
|
|
||||||
this.emojis = [];
|
|
||||||
this.commands = [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
renderFixedMentionItem = item => (
|
renderFixedMentionItem = item => (
|
||||||
|
@ -761,7 +754,7 @@ class MessageBox extends Component {
|
||||||
key='mention-item-avatar'
|
key='mention-item-avatar'
|
||||||
style={styles.mentionItemEmoji}
|
style={styles.mentionItemEmoji}
|
||||||
>
|
>
|
||||||
{emojify(`:${ item }:`, { output: 'unicode' })}
|
{shortnameToUnicode(`:${ item }:`)}
|
||||||
</Text>
|
</Text>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -797,33 +790,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>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
})()
|
})()
|
||||||
|
@ -846,8 +839,9 @@ class MessageBox extends Component {
|
||||||
<FlatList
|
<FlatList
|
||||||
style={styles.mentionList}
|
style={styles.mentionList}
|
||||||
data={mentions}
|
data={mentions}
|
||||||
|
extraData={mentions}
|
||||||
renderItem={this.renderMentionItem}
|
renderItem={this.renderMentionItem}
|
||||||
keyExtractor={item => item._id || item.username || item.command || item}
|
keyExtractor={item => item.id || item.username || item.command || item}
|
||||||
keyboardShouldPersistTaps='always'
|
keyboardShouldPersistTaps='always'
|
||||||
/>
|
/>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
@ -859,8 +853,8 @@ class MessageBox extends Component {
|
||||||
);
|
);
|
||||||
|
|
||||||
renderCommandPreview = () => {
|
renderCommandPreview = () => {
|
||||||
const { commandPreview } = this.state;
|
const { commandPreview, showCommandPreview } = this.state;
|
||||||
if (!this.showCommandPreview) {
|
if (!showCommandPreview) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
|
@ -880,12 +874,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 +890,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 +929,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,30 +955,25 @@ 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: {
|
||||||
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
|
||||||
}
|
},
|
||||||
|
FileUpload_MediaTypeWhiteList: state.settings.FileUpload_MediaTypeWhiteList,
|
||||||
|
FileUpload_MaxFileSize: state.settings.FileUpload_MaxFileSize
|
||||||
});
|
});
|
||||||
|
|
||||||
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);
|
||||||
|
|
|
@ -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);
|
|
||||||
|
|
|
@ -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';
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { Text } from 'react-native';
|
||||||
import styles from './styles';
|
import styles from './styles';
|
||||||
|
|
||||||
const AtMention = React.memo(({
|
const AtMention = React.memo(({
|
||||||
mention, mentions, username, navToRoomInfo
|
mention, mentions, username, navToRoomInfo, preview, style = []
|
||||||
}) => {
|
}) => {
|
||||||
let mentionStyle = styles.mention;
|
let mentionStyle = styles.mention;
|
||||||
if (mention === 'all' || mention === 'here') {
|
if (mention === 'all' || mention === 'here') {
|
||||||
|
@ -33,8 +33,8 @@ const AtMention = React.memo(({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Text
|
<Text
|
||||||
style={mentionStyle}
|
style={[preview ? styles.text : mentionStyle, ...style]}
|
||||||
onPress={handlePress}
|
onPress={preview ? undefined : handlePress}
|
||||||
>
|
>
|
||||||
{`@${ mention }`}
|
{`@${ mention }`}
|
||||||
</Text>
|
</Text>
|
||||||
|
@ -45,6 +45,8 @@ AtMention.propTypes = {
|
||||||
mention: PropTypes.string,
|
mention: PropTypes.string,
|
||||||
username: PropTypes.string,
|
username: PropTypes.string,
|
||||||
navToRoomInfo: PropTypes.func,
|
navToRoomInfo: PropTypes.func,
|
||||||
|
style: PropTypes.array,
|
||||||
|
preview: PropTypes.bool,
|
||||||
mentions: PropTypes.oneOfType([PropTypes.array, PropTypes.object])
|
mentions: PropTypes.oneOfType([PropTypes.array, PropTypes.object])
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,18 +1,18 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Text } from 'react-native';
|
import { Text } from 'react-native';
|
||||||
import { emojify } from 'react-emojione';
|
import { shortnameToUnicode } from 'emoji-toolkit';
|
||||||
|
|
||||||
import CustomEmoji from '../EmojiPicker/CustomEmoji';
|
import CustomEmoji from '../EmojiPicker/CustomEmoji';
|
||||||
|
|
||||||
import styles from './styles';
|
import styles from './styles';
|
||||||
|
|
||||||
const Emoji = React.memo(({
|
const Emoji = React.memo(({
|
||||||
emojiName, literal, isMessageContainsOnlyEmoji, getCustomEmoji, baseUrl
|
emojiName, literal, isMessageContainsOnlyEmoji, getCustomEmoji, baseUrl, customEmojis, style = []
|
||||||
}) => {
|
}) => {
|
||||||
const emojiUnicode = emojify(literal, { output: 'unicode' });
|
const emojiUnicode = shortnameToUnicode(literal);
|
||||||
const emoji = getCustomEmoji && getCustomEmoji(emojiName);
|
const emoji = getCustomEmoji && getCustomEmoji(emojiName);
|
||||||
if (emoji) {
|
if (emoji && customEmojis) {
|
||||||
return (
|
return (
|
||||||
<CustomEmoji
|
<CustomEmoji
|
||||||
baseUrl={baseUrl}
|
baseUrl={baseUrl}
|
||||||
|
@ -21,7 +21,16 @@ const Emoji = React.memo(({
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return <Text style={isMessageContainsOnlyEmoji ? styles.textBig : styles.text}>{emojiUnicode}</Text>;
|
return (
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
isMessageContainsOnlyEmoji ? styles.textBig : styles.text,
|
||||||
|
...style
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{emojiUnicode}
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
Emoji.propTypes = {
|
Emoji.propTypes = {
|
||||||
|
@ -29,7 +38,9 @@ Emoji.propTypes = {
|
||||||
literal: PropTypes.string,
|
literal: PropTypes.string,
|
||||||
isMessageContainsOnlyEmoji: PropTypes.bool,
|
isMessageContainsOnlyEmoji: PropTypes.bool,
|
||||||
getCustomEmoji: PropTypes.func,
|
getCustomEmoji: PropTypes.func,
|
||||||
baseUrl: PropTypes.string
|
baseUrl: PropTypes.string,
|
||||||
|
customEmojis: PropTypes.bool,
|
||||||
|
style: PropTypes.array
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Emoji;
|
export default Emoji;
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { Text } from 'react-native';
|
||||||
import styles from './styles';
|
import styles from './styles';
|
||||||
|
|
||||||
const Hashtag = React.memo(({
|
const Hashtag = React.memo(({
|
||||||
hashtag, channels, navToRoomInfo
|
hashtag, channels, navToRoomInfo, preview, style = []
|
||||||
}) => {
|
}) => {
|
||||||
const handlePress = () => {
|
const handlePress = () => {
|
||||||
const index = channels.findIndex(channel => channel.name === hashtag);
|
const index = channels.findIndex(channel => channel.name === hashtag);
|
||||||
|
@ -19,8 +19,8 @@ const Hashtag = React.memo(({
|
||||||
if (channels && channels.length && channels.findIndex(channel => channel.name === hashtag) !== -1) {
|
if (channels && channels.length && channels.findIndex(channel => channel.name === hashtag) !== -1) {
|
||||||
return (
|
return (
|
||||||
<Text
|
<Text
|
||||||
style={styles.mention}
|
style={[preview ? styles.text : styles.mention, ...style]}
|
||||||
onPress={handlePress}
|
onPress={preview ? undefined : handlePress}
|
||||||
>
|
>
|
||||||
{`#${ hashtag }`}
|
{`#${ hashtag }`}
|
||||||
</Text>
|
</Text>
|
||||||
|
@ -32,6 +32,8 @@ const Hashtag = React.memo(({
|
||||||
Hashtag.propTypes = {
|
Hashtag.propTypes = {
|
||||||
hashtag: PropTypes.string,
|
hashtag: PropTypes.string,
|
||||||
navToRoomInfo: PropTypes.func,
|
navToRoomInfo: PropTypes.func,
|
||||||
|
style: PropTypes.array,
|
||||||
|
preview: PropTypes.bool,
|
||||||
channels: PropTypes.oneOfType([PropTypes.array, PropTypes.object])
|
channels: PropTypes.oneOfType([PropTypes.array, PropTypes.object])
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ import styles from './styles';
|
||||||
import openLink from '../../utils/openLink';
|
import openLink from '../../utils/openLink';
|
||||||
|
|
||||||
const Link = React.memo(({
|
const Link = React.memo(({
|
||||||
children, link
|
children, link, preview
|
||||||
}) => {
|
}) => {
|
||||||
const handlePress = () => {
|
const handlePress = () => {
|
||||||
if (!link) {
|
if (!link) {
|
||||||
|
@ -20,7 +20,7 @@ const Link = React.memo(({
|
||||||
// if you have a [](https://rocket.chat) render https://rocket.chat
|
// if you have a [](https://rocket.chat) render https://rocket.chat
|
||||||
return (
|
return (
|
||||||
<Text
|
<Text
|
||||||
onPress={handlePress}
|
onPress={preview ? undefined : handlePress}
|
||||||
style={styles.link}
|
style={styles.link}
|
||||||
>
|
>
|
||||||
{ childLength !== 0 ? children : link }
|
{ childLength !== 0 ? children : link }
|
||||||
|
@ -30,7 +30,8 @@ const Link = React.memo(({
|
||||||
|
|
||||||
Link.propTypes = {
|
Link.propTypes = {
|
||||||
children: PropTypes.node,
|
children: PropTypes.node,
|
||||||
link: PropTypes.string
|
link: PropTypes.string,
|
||||||
|
preview: PropTypes.bool
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Link;
|
export default Link;
|
||||||
|
|
|
@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
const List = React.memo(({
|
const List = React.memo(({
|
||||||
children, ordered, start, tight
|
children, ordered, start, tight, numberOfLines = 0
|
||||||
}) => {
|
}) => {
|
||||||
let bulletWidth = 15;
|
let bulletWidth = 15;
|
||||||
|
|
||||||
|
@ -11,7 +11,13 @@ const List = React.memo(({
|
||||||
bulletWidth = (9 * lastNumber.toString().length) + 7;
|
bulletWidth = (9 * lastNumber.toString().length) + 7;
|
||||||
}
|
}
|
||||||
|
|
||||||
const _children = React.Children.map(children, (child, index) => React.cloneElement(child, {
|
let items = React.Children.toArray(children);
|
||||||
|
|
||||||
|
if (numberOfLines) {
|
||||||
|
items = items.slice(0, numberOfLines);
|
||||||
|
}
|
||||||
|
|
||||||
|
const _children = items.map((child, index) => React.cloneElement(child, {
|
||||||
bulletWidth,
|
bulletWidth,
|
||||||
ordered,
|
ordered,
|
||||||
tight,
|
tight,
|
||||||
|
@ -29,7 +35,8 @@ List.propTypes = {
|
||||||
children: PropTypes.node,
|
children: PropTypes.node,
|
||||||
ordered: PropTypes.bool,
|
ordered: PropTypes.bool,
|
||||||
start: PropTypes.number,
|
start: PropTypes.number,
|
||||||
tight: PropTypes.bool
|
tight: PropTypes.bool,
|
||||||
|
numberOfLines: PropTypes.number
|
||||||
};
|
};
|
||||||
|
|
||||||
List.defaultProps = {
|
List.defaultProps = {
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
import { View, Text, Image } from 'react-native';
|
import { Text, Image } from 'react-native';
|
||||||
import { Parser, Node } from 'commonmark';
|
import { Parser, Node } from 'commonmark';
|
||||||
import Renderer from 'commonmark-react-renderer';
|
import Renderer from 'commonmark-react-renderer';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
import { toShort, shortnameToUnicode } from 'emoji-toolkit';
|
||||||
|
|
||||||
import I18n from '../../i18n';
|
import I18n from '../../i18n';
|
||||||
|
|
||||||
|
@ -62,21 +63,24 @@ export default class Markdown extends PureComponent {
|
||||||
isEdited: PropTypes.bool,
|
isEdited: PropTypes.bool,
|
||||||
numberOfLines: PropTypes.number,
|
numberOfLines: PropTypes.number,
|
||||||
useMarkdown: PropTypes.bool,
|
useMarkdown: PropTypes.bool,
|
||||||
|
customEmojis: PropTypes.bool,
|
||||||
channels: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
|
channels: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
|
||||||
mentions: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
|
mentions: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
|
||||||
navToRoomInfo: PropTypes.func
|
navToRoomInfo: PropTypes.func,
|
||||||
|
preview: PropTypes.bool,
|
||||||
|
style: PropTypes.array
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.parser = this.createParser();
|
this.parser = this.createParser();
|
||||||
this.renderer = this.createRenderer();
|
this.renderer = this.createRenderer(props.preview);
|
||||||
}
|
}
|
||||||
|
|
||||||
createParser = () => new Parser();
|
createParser = () => new Parser();
|
||||||
|
|
||||||
createRenderer = () => new Renderer({
|
createRenderer = (preview = false) => new Renderer({
|
||||||
renderers: {
|
renderers: {
|
||||||
text: this.renderText,
|
text: this.renderText,
|
||||||
|
|
||||||
|
@ -109,7 +113,7 @@ export default class Markdown extends PureComponent {
|
||||||
table_row: this.renderTableRow,
|
table_row: this.renderTableRow,
|
||||||
table_cell: this.renderTableCell,
|
table_cell: this.renderTableCell,
|
||||||
|
|
||||||
editedIndicator: this.renderEditedIndicator
|
editedIndicator: preview ? () => null : this.renderEditedIndicator
|
||||||
},
|
},
|
||||||
renderParagraphsInLists: true
|
renderParagraphsInLists: true
|
||||||
});
|
});
|
||||||
|
@ -130,12 +134,17 @@ export default class Markdown extends PureComponent {
|
||||||
};
|
};
|
||||||
|
|
||||||
renderText = ({ context, literal }) => {
|
renderText = ({ context, literal }) => {
|
||||||
const { numberOfLines } = this.props;
|
const { numberOfLines, preview, style = [] } = this.props;
|
||||||
|
const defaultStyle = [
|
||||||
|
this.isMessageContainsOnlyEmoji && !preview ? styles.textBig : {},
|
||||||
|
...context.map(type => styles[type])
|
||||||
|
];
|
||||||
return (
|
return (
|
||||||
<Text
|
<Text
|
||||||
style={[
|
style={[
|
||||||
this.isMessageContainsOnlyEmoji ? styles.textBig : styles.text,
|
styles.text,
|
||||||
...context.map(type => styles[type])
|
!preview ? defaultStyle : {},
|
||||||
|
...style
|
||||||
]}
|
]}
|
||||||
numberOfLines={numberOfLines}
|
numberOfLines={numberOfLines}
|
||||||
>
|
>
|
||||||
|
@ -144,9 +153,15 @@ export default class Markdown extends PureComponent {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderCodeInline = ({ literal }) => <Text style={styles.codeInline}>{literal}</Text>;
|
renderCodeInline = ({ literal }) => {
|
||||||
|
const { preview } = this.props;
|
||||||
|
return <Text style={!preview ? styles.codeInline : {}}>{literal}</Text>;
|
||||||
|
};
|
||||||
|
|
||||||
renderCodeBlock = ({ literal }) => <Text style={styles.codeBlock}>{literal}</Text>;
|
renderCodeBlock = ({ literal }) => {
|
||||||
|
const { preview } = this.props;
|
||||||
|
return <Text style={!preview ? styles.codeBlock : {}}>{literal}</Text>;
|
||||||
|
};
|
||||||
|
|
||||||
renderBreak = () => {
|
renderBreak = () => {
|
||||||
const { tmid } = this.props;
|
const { tmid } = this.props;
|
||||||
|
@ -154,58 +169,70 @@ export default class Markdown extends PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
renderParagraph = ({ children }) => {
|
renderParagraph = ({ children }) => {
|
||||||
const { numberOfLines } = this.props;
|
const { numberOfLines, style } = this.props;
|
||||||
|
|
||||||
if (!children || children.length === 0) {
|
if (!children || children.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<View style={styles.block}>
|
<Text style={style} numberOfLines={numberOfLines}>
|
||||||
<Text numberOfLines={numberOfLines}>
|
|
||||||
{children}
|
{children}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
renderLink = ({ children, href }) => (
|
renderLink = ({ children, href }) => {
|
||||||
<MarkdownLink link={href}>
|
const { preview } = this.props;
|
||||||
|
return (
|
||||||
|
<MarkdownLink link={href} preview={preview}>
|
||||||
{children}
|
{children}
|
||||||
</MarkdownLink>
|
</MarkdownLink>
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
renderHashtag = ({ hashtag }) => {
|
renderHashtag = ({ hashtag }) => {
|
||||||
const { channels, navToRoomInfo } = this.props;
|
const {
|
||||||
|
channels, navToRoomInfo, style, preview
|
||||||
|
} = this.props;
|
||||||
return (
|
return (
|
||||||
<MarkdownHashtag
|
<MarkdownHashtag
|
||||||
hashtag={hashtag}
|
hashtag={hashtag}
|
||||||
channels={channels}
|
channels={channels}
|
||||||
navToRoomInfo={navToRoomInfo}
|
navToRoomInfo={navToRoomInfo}
|
||||||
|
preview={preview}
|
||||||
|
style={style}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderAtMention = ({ mentionName }) => {
|
renderAtMention = ({ mentionName }) => {
|
||||||
const { username, mentions, navToRoomInfo } = this.props;
|
const {
|
||||||
|
username, mentions, navToRoomInfo, preview, style
|
||||||
|
} = this.props;
|
||||||
return (
|
return (
|
||||||
<MarkdownAtMention
|
<MarkdownAtMention
|
||||||
mentions={mentions}
|
mentions={mentions}
|
||||||
mention={mentionName}
|
mention={mentionName}
|
||||||
username={username}
|
username={username}
|
||||||
navToRoomInfo={navToRoomInfo}
|
navToRoomInfo={navToRoomInfo}
|
||||||
|
preview={preview}
|
||||||
|
style={style}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderEmoji = ({ emojiName, literal }) => {
|
renderEmoji = ({ emojiName, literal }) => {
|
||||||
const { getCustomEmoji, baseUrl } = this.props;
|
const {
|
||||||
|
getCustomEmoji, baseUrl, customEmojis = true, preview, style
|
||||||
|
} = this.props;
|
||||||
return (
|
return (
|
||||||
<MarkdownEmoji
|
<MarkdownEmoji
|
||||||
emojiName={emojiName}
|
emojiName={emojiName}
|
||||||
literal={literal}
|
literal={literal}
|
||||||
isMessageContainsOnlyEmoji={this.isMessageContainsOnlyEmoji}
|
isMessageContainsOnlyEmoji={this.isMessageContainsOnlyEmoji && !preview}
|
||||||
getCustomEmoji={getCustomEmoji}
|
getCustomEmoji={getCustomEmoji}
|
||||||
baseUrl={baseUrl}
|
baseUrl={baseUrl}
|
||||||
|
customEmojis={customEmojis}
|
||||||
|
style={style}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -215,9 +242,10 @@ export default class Markdown extends PureComponent {
|
||||||
renderEditedIndicator = () => <Text style={styles.edited}> ({I18n.t('edited')})</Text>;
|
renderEditedIndicator = () => <Text style={styles.edited}> ({I18n.t('edited')})</Text>;
|
||||||
|
|
||||||
renderHeading = ({ children, level }) => {
|
renderHeading = ({ children, level }) => {
|
||||||
|
const { numberOfLines } = this.props;
|
||||||
const textStyle = styles[`heading${ level }Text`];
|
const textStyle = styles[`heading${ level }Text`];
|
||||||
return (
|
return (
|
||||||
<Text style={textStyle}>
|
<Text numberOfLines={numberOfLines} style={textStyle}>
|
||||||
{children}
|
{children}
|
||||||
</Text>
|
</Text>
|
||||||
);
|
);
|
||||||
|
@ -225,15 +253,19 @@ export default class Markdown extends PureComponent {
|
||||||
|
|
||||||
renderList = ({
|
renderList = ({
|
||||||
children, start, tight, type
|
children, start, tight, type
|
||||||
}) => (
|
}) => {
|
||||||
|
const { numberOfLines } = this.props;
|
||||||
|
return (
|
||||||
<MarkdownList
|
<MarkdownList
|
||||||
ordered={type !== 'bullet'}
|
ordered={type !== 'bullet'}
|
||||||
start={start}
|
start={start}
|
||||||
tight={tight}
|
tight={tight}
|
||||||
|
numberOfLines={numberOfLines}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</MarkdownList>
|
</MarkdownList>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
renderListItem = ({
|
renderListItem = ({
|
||||||
children, context, ...otherProps
|
children, context, ...otherProps
|
||||||
|
@ -250,11 +282,17 @@ export default class Markdown extends PureComponent {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
renderBlockQuote = ({ children }) => (
|
renderBlockQuote = ({ children }) => {
|
||||||
|
const { preview } = this.props;
|
||||||
|
if (preview) {
|
||||||
|
return children;
|
||||||
|
}
|
||||||
|
return (
|
||||||
<MarkdownBlockQuote>
|
<MarkdownBlockQuote>
|
||||||
{children}
|
{children}
|
||||||
</MarkdownBlockQuote>
|
</MarkdownBlockQuote>
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
renderTable = ({ children, numColumns }) => (
|
renderTable = ({ children, numColumns }) => (
|
||||||
<MarkdownTable numColumns={numColumns}>
|
<MarkdownTable numColumns={numColumns}>
|
||||||
|
@ -268,7 +306,7 @@ export default class Markdown extends PureComponent {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
msg, useMarkdown = true, numberOfLines
|
msg, useMarkdown = true, numberOfLines, preview = false
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
if (!msg) {
|
if (!msg) {
|
||||||
|
@ -280,13 +318,19 @@ export default class Markdown extends PureComponent {
|
||||||
// Ex: '[ ](https://open.rocket.chat/group/test?msg=abcdef) Test'
|
// Ex: '[ ](https://open.rocket.chat/group/test?msg=abcdef) Test'
|
||||||
// Return: 'Test'
|
// Return: 'Test'
|
||||||
m = m.replace(/^\[([\s]]*)\]\(([^)]*)\)\s/, '').trim();
|
m = m.replace(/^\[([\s]]*)\]\(([^)]*)\)\s/, '').trim();
|
||||||
|
m = shortnameToUnicode(m);
|
||||||
|
|
||||||
if (!useMarkdown) {
|
if (preview) {
|
||||||
|
m = m.split('\n').reduce((lines, line) => `${ lines } ${ line }`, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!useMarkdown && !preview) {
|
||||||
return <Text style={styles.text} numberOfLines={numberOfLines}>{m}</Text>;
|
return <Text style={styles.text} numberOfLines={numberOfLines}>{m}</Text>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ast = this.parser.parse(m);
|
const ast = this.parser.parse(m);
|
||||||
this.isMessageContainsOnlyEmoji = isOnlyEmoji(m) && emojiCount(m) <= 3;
|
const encodedEmojis = toShort(m);
|
||||||
|
this.isMessageContainsOnlyEmoji = isOnlyEmoji(encodedEmojis) && emojiCount(encodedEmojis) <= 3;
|
||||||
|
|
||||||
this.editedMessage(ast);
|
this.editedMessage(ast);
|
||||||
|
|
||||||
|
|
|
@ -21,7 +21,8 @@ export default StyleSheet.create({
|
||||||
block: {
|
block: {
|
||||||
alignItems: 'flex-start',
|
alignItems: 'flex-start',
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
flexWrap: 'wrap'
|
flexWrap: 'wrap',
|
||||||
|
flex: 1
|
||||||
},
|
},
|
||||||
emph: {
|
emph: {
|
||||||
fontStyle: 'italic'
|
fontStyle: 'italic'
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import {
|
import {
|
||||||
View, StyleSheet, Text, Easing
|
View, StyleSheet, Text, Easing, Dimensions
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import Video from 'react-native-video';
|
import Video from 'react-native-video';
|
||||||
import Slider from 'react-native-slider';
|
import Slider from '@react-native-community/slider';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import equal from 'deep-equal';
|
import equal from 'deep-equal';
|
||||||
import Touchable from 'react-native-platform-touchable';
|
import Touchable from 'react-native-platform-touchable';
|
||||||
|
@ -13,6 +13,7 @@ import Markdown from '../markdown';
|
||||||
import { CustomIcon } from '../../lib/Icons';
|
import { CustomIcon } from '../../lib/Icons';
|
||||||
import sharedStyles from '../../views/Styles';
|
import sharedStyles from '../../views/Styles';
|
||||||
import { COLOR_BACKGROUND_CONTAINER, COLOR_BORDER, COLOR_PRIMARY } from '../../constants/colors';
|
import { COLOR_BACKGROUND_CONTAINER, COLOR_BORDER, COLOR_PRIMARY } from '../../constants/colors';
|
||||||
|
import { isAndroid, isIOS } from '../../utils/deviceInfo';
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
audioContainer: {
|
audioContainer: {
|
||||||
|
@ -42,13 +43,6 @@ const styles = StyleSheet.create({
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
...sharedStyles.textColorNormal,
|
...sharedStyles.textColorNormal,
|
||||||
...sharedStyles.textRegular
|
...sharedStyles.textRegular
|
||||||
},
|
|
||||||
thumbStyle: {
|
|
||||||
width: 12,
|
|
||||||
height: 12
|
|
||||||
},
|
|
||||||
trackStyle: {
|
|
||||||
height: 2
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -168,7 +162,7 @@ export default class Audio extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<>
|
||||||
<View style={styles.audioContainer}>
|
<View style={styles.audioContainer}>
|
||||||
<Video
|
<Video
|
||||||
ref={this.setRef}
|
ref={this.setRef}
|
||||||
|
@ -187,16 +181,15 @@ export default class Audio extends React.Component {
|
||||||
minimumValue={0}
|
minimumValue={0}
|
||||||
animateTransitions
|
animateTransitions
|
||||||
animationConfig={sliderAnimationConfig}
|
animationConfig={sliderAnimationConfig}
|
||||||
thumbTintColor={COLOR_PRIMARY}
|
thumbTintColor={isAndroid && COLOR_PRIMARY}
|
||||||
minimumTrackTintColor={COLOR_PRIMARY}
|
minimumTrackTintColor={COLOR_PRIMARY}
|
||||||
onValueChange={this.onValueChange}
|
onValueChange={this.onValueChange}
|
||||||
thumbStyle={styles.thumbStyle}
|
thumbImage={isIOS && { uri: 'audio_thumb', scale: Dimensions.get('window').scale }}
|
||||||
trackStyle={styles.trackStyle}
|
|
||||||
/>
|
/>
|
||||||
<Text style={styles.duration}>{this.duration}</Text>
|
<Text style={styles.duration}>{this.duration}</Text>
|
||||||
</View>
|
</View>
|
||||||
<Markdown msg={description} baseUrl={baseUrl} username={user.username} getCustomEmoji={getCustomEmoji} useMarkdown={useMarkdown} />
|
<Markdown msg={description} baseUrl={baseUrl} username={user.username} getCustomEmoji={getCustomEmoji} useMarkdown={useMarkdown} />
|
||||||
</React.Fragment>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,10 +21,10 @@ const Broadcast = React.memo(({
|
||||||
style={styles.button}
|
style={styles.button}
|
||||||
hitSlop={BUTTON_HIT_SLOP}
|
hitSlop={BUTTON_HIT_SLOP}
|
||||||
>
|
>
|
||||||
<React.Fragment>
|
<>
|
||||||
<CustomIcon name='back' size={20} style={styles.buttonIcon} />
|
<CustomIcon name='back' size={20} style={styles.buttonIcon} />
|
||||||
<Text style={styles.buttonText}>{I18n.t('Reply')}</Text>
|
<Text style={styles.buttonText}>{I18n.t('Reply')}</Text>
|
||||||
</React.Fragment>
|
</>
|
||||||
</Touchable>
|
</Touchable>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { View, Text } from 'react-native';
|
||||||
|
import Touchable from 'react-native-platform-touchable';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import { formatLastMessage, BUTTON_HIT_SLOP } from './utils';
|
||||||
|
import styles from './styles';
|
||||||
|
import I18n from '../../i18n';
|
||||||
|
import { CustomIcon } from '../../lib/Icons';
|
||||||
|
|
||||||
|
const CallButton = React.memo(({
|
||||||
|
dlm, callJitsi
|
||||||
|
}) => {
|
||||||
|
const time = formatLastMessage(dlm);
|
||||||
|
return (
|
||||||
|
<View style={styles.buttonContainer}>
|
||||||
|
<Touchable
|
||||||
|
onPress={callJitsi}
|
||||||
|
background={Touchable.Ripple('#fff')}
|
||||||
|
style={[styles.button, styles.smallButton]}
|
||||||
|
hitSlop={BUTTON_HIT_SLOP}
|
||||||
|
>
|
||||||
|
<>
|
||||||
|
<CustomIcon name='video' size={20} style={styles.buttonIcon} />
|
||||||
|
<Text style={styles.buttonText}>{I18n.t('Click_to_join')}</Text>
|
||||||
|
</>
|
||||||
|
</Touchable>
|
||||||
|
<Text style={styles.time}>{time}</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
CallButton.propTypes = {
|
||||||
|
dlm: PropTypes.string,
|
||||||
|
callJitsi: PropTypes.func
|
||||||
|
};
|
||||||
|
CallButton.displayName = 'CallButton';
|
||||||
|
|
||||||
|
export default CallButton;
|
|
@ -24,10 +24,11 @@ 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}
|
||||||
|
preview={props.tmid && !props.isThreadRoom}
|
||||||
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 +46,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,
|
||||||
|
|
|
@ -15,7 +15,7 @@ const Discussion = React.memo(({
|
||||||
const time = formatLastMessage(dlm);
|
const time = formatLastMessage(dlm);
|
||||||
const buttonText = formatMessageCount(dcount, DISCUSSION);
|
const buttonText = formatMessageCount(dcount, DISCUSSION);
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<>
|
||||||
<Text style={styles.startedDiscussion}>{I18n.t('Started_discussion')}</Text>
|
<Text style={styles.startedDiscussion}>{I18n.t('Started_discussion')}</Text>
|
||||||
<Text style={styles.text}>{msg}</Text>
|
<Text style={styles.text}>{msg}</Text>
|
||||||
<View style={styles.buttonContainer}>
|
<View style={styles.buttonContainer}>
|
||||||
|
@ -25,14 +25,14 @@ const Discussion = React.memo(({
|
||||||
style={[styles.button, styles.smallButton]}
|
style={[styles.button, styles.smallButton]}
|
||||||
hitSlop={BUTTON_HIT_SLOP}
|
hitSlop={BUTTON_HIT_SLOP}
|
||||||
>
|
>
|
||||||
<React.Fragment>
|
<>
|
||||||
<CustomIcon name='chat' size={20} style={styles.buttonIcon} />
|
<CustomIcon name='chat' size={20} style={styles.buttonIcon} />
|
||||||
<Text style={styles.buttonText}>{buttonText}</Text>
|
<Text style={styles.buttonText}>{buttonText}</Text>
|
||||||
</React.Fragment>
|
</>
|
||||||
</Touchable>
|
</Touchable>
|
||||||
<Text style={styles.time}>{time}</Text>
|
<Text style={styles.time}>{time}</Text>
|
||||||
</View>
|
</View>
|
||||||
</React.Fragment>
|
</>
|
||||||
);
|
);
|
||||||
}, (prevProps, nextProps) => {
|
}, (prevProps, nextProps) => {
|
||||||
if (prevProps.msg !== nextProps.msg) {
|
if (prevProps.msg !== nextProps.msg) {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Text } from 'react-native';
|
import { Text } from 'react-native';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { emojify } from 'react-emojione';
|
import { shortnameToUnicode } from 'emoji-toolkit';
|
||||||
|
|
||||||
import CustomEmoji from '../EmojiPicker/CustomEmoji';
|
import CustomEmoji from '../EmojiPicker/CustomEmoji';
|
||||||
|
|
||||||
|
@ -13,7 +13,7 @@ const Emoji = React.memo(({
|
||||||
if (emoji) {
|
if (emoji) {
|
||||||
return <CustomEmoji key={content} baseUrl={baseUrl} style={customEmojiStyle} emoji={emoji} />;
|
return <CustomEmoji key={content} baseUrl={baseUrl} style={customEmojiStyle} emoji={emoji} />;
|
||||||
}
|
}
|
||||||
return <Text style={standardEmojiStyle}>{ emojify(content, { output: 'unicode' }) }</Text>;
|
return <Text style={standardEmojiStyle}>{ shortnameToUnicode(content) }</Text>;
|
||||||
}, () => true);
|
}, () => true);
|
||||||
|
|
||||||
Emoji.propTypes = {
|
Emoji.propTypes = {
|
||||||
|
|
|
@ -16,18 +16,28 @@ import Broadcast from './Broadcast';
|
||||||
import Discussion from './Discussion';
|
import Discussion from './Discussion';
|
||||||
import Content from './Content';
|
import Content from './Content';
|
||||||
import ReadReceipt from './ReadReceipt';
|
import ReadReceipt from './ReadReceipt';
|
||||||
|
import CallButton from './CallButton';
|
||||||
|
|
||||||
const MessageInner = React.memo((props) => {
|
const MessageInner = React.memo((props) => {
|
||||||
if (props.type === 'discussion-created') {
|
if (props.type === 'discussion-created') {
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<>
|
||||||
<User {...props} />
|
<User {...props} />
|
||||||
<Discussion {...props} />
|
<Discussion {...props} />
|
||||||
</React.Fragment>
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (props.type === 'jitsi_call_started') {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<User {...props} />
|
||||||
|
<Content {...props} isInfo />
|
||||||
|
<CallButton {...props} />
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<>
|
||||||
<User {...props} />
|
<User {...props} />
|
||||||
<Content {...props} />
|
<Content {...props} />
|
||||||
<Attachments {...props} />
|
<Attachments {...props} />
|
||||||
|
@ -35,7 +45,7 @@ const MessageInner = React.memo((props) => {
|
||||||
<Thread {...props} />
|
<Thread {...props} />
|
||||||
<Reactions {...props} />
|
<Reactions {...props} />
|
||||||
<Broadcast {...props} />
|
<Broadcast {...props} />
|
||||||
</React.Fragment>
|
</>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
MessageInner.displayName = 'MessageInner';
|
MessageInner.displayName = 'MessageInner';
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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';
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { View, Text } from 'react-native';
|
import { View, Text } from 'react-native';
|
||||||
import removeMarkdown from 'remove-markdown';
|
import removeMarkdown from 'remove-markdown';
|
||||||
import { emojify } from 'react-emojione';
|
import { shortnameToUnicode } from 'emoji-toolkit';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import { CustomIcon } from '../../lib/Icons';
|
import { CustomIcon } from '../../lib/Icons';
|
||||||
|
@ -9,18 +9,18 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
let msg = emojify(tmsg, { output: 'unicode' });
|
let msg = shortnameToUnicode(tmsg);
|
||||||
msg = removeMarkdown(msg);
|
msg = removeMarkdown(msg);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -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
|
||||||
|
|
|
@ -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 || tcount === 0) {
|
||||||
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';
|
||||||
|
|
||||||
|
|
|
@ -89,10 +89,10 @@ const Url = React.memo(({
|
||||||
style={[styles.button, index > 0 && styles.marginTop, styles.container]}
|
style={[styles.button, index > 0 && styles.marginTop, styles.container]}
|
||||||
background={Touchable.Ripple('#fff')}
|
background={Touchable.Ripple('#fff')}
|
||||||
>
|
>
|
||||||
<React.Fragment>
|
<>
|
||||||
<UrlImage image={url.image} user={user} baseUrl={baseUrl} />
|
<UrlImage image={url.image} user={user} baseUrl={baseUrl} />
|
||||||
<UrlContent title={url.title} description={url.description} />
|
<UrlContent title={url.title} description={url.description} />
|
||||||
</React.Fragment>
|
</>
|
||||||
</Touchable>
|
</Touchable>
|
||||||
);
|
);
|
||||||
}, (oldProps, newProps) => isEqual(oldProps.url, newProps.url));
|
}, (oldProps, newProps) => isEqual(oldProps.url, newProps.url));
|
||||||
|
|
|
@ -48,7 +48,7 @@ const Video = React.memo(({
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<>
|
||||||
<Touchable
|
<Touchable
|
||||||
onPress={onPress}
|
onPress={onPress}
|
||||||
style={styles.button}
|
style={styles.button}
|
||||||
|
@ -61,7 +61,7 @@ const Video = React.memo(({
|
||||||
/>
|
/>
|
||||||
</Touchable>
|
</Touchable>
|
||||||
<Markdown msg={file.description} baseUrl={baseUrl} username={user.username} getCustomEmoji={getCustomEmoji} useMarkdown={useMarkdown} />
|
<Markdown msg={file.description} baseUrl={baseUrl} username={user.username} getCustomEmoji={getCustomEmoji} useMarkdown={useMarkdown} />
|
||||||
</React.Fragment>
|
</>
|
||||||
);
|
);
|
||||||
}, (prevProps, nextProps) => isEqual(prevProps.file, nextProps.file));
|
}, (prevProps, nextProps) => isEqual(prevProps.file, nextProps.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,86 +21,86 @@ 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,
|
||||||
navToRoomInfo: PropTypes.func
|
navToRoomInfo: PropTypes.func,
|
||||||
|
callJitsi: PropTypes.func
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onReactionPress = (emoji) => {
|
onReactionPress = (emoji) => {
|
||||||
const { onReactionPress, item } = this.props;
|
const { onReactionPress, item } = this.props;
|
||||||
if (onReactionPress) {
|
if (onReactionPress) {
|
||||||
onReactionPress(emoji, item._id);
|
onReactionPress(emoji, item.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -132,6 +132,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 +143,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 +163,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 +189,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, callJitsi
|
||||||
} = 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 +220,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 +256,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,11 +266,12 @@ 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}
|
||||||
navToRoomInfo={navToRoomInfo}
|
navToRoomInfo={navToRoomInfo}
|
||||||
|
callJitsi={callJitsi}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) => {
|
||||||
|
@ -68,6 +67,8 @@ export const getInfoMessage = ({
|
||||||
return I18n.t('Room_name_changed', { name: msg, userBy: username });
|
return I18n.t('Room_name_changed', { name: msg, userBy: username });
|
||||||
} else if (type === 'message_pinned') {
|
} else if (type === 'message_pinned') {
|
||||||
return I18n.t('Message_pinned');
|
return I18n.t('Message_pinned');
|
||||||
|
} else if (type === 'jitsi_call_started') {
|
||||||
|
return I18n.t('Started_call', { userBy: username });
|
||||||
} else if (type === 'ul') {
|
} else if (type === 'ul') {
|
||||||
return I18n.t('Has_left_the_channel');
|
return I18n.t('Has_left_the_channel');
|
||||||
} else if (type === 'ru') {
|
} else if (type === 'ru') {
|
||||||
|
@ -96,25 +97,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;
|
||||||
|
|
|
@ -120,6 +120,8 @@ export default {
|
||||||
Channel_Name: 'Channel Name',
|
Channel_Name: 'Channel Name',
|
||||||
Channels: 'Channels',
|
Channels: 'Channels',
|
||||||
Chats: 'Chats',
|
Chats: 'Chats',
|
||||||
|
Call_already_ended: 'Call already ended!',
|
||||||
|
Click_to_join: 'Click to Join!',
|
||||||
Close: 'Close',
|
Close: 'Close',
|
||||||
Close_emoji_selector: 'Close emoji selector',
|
Close_emoji_selector: 'Close emoji selector',
|
||||||
Choose: 'Choose',
|
Choose: 'Choose',
|
||||||
|
@ -364,6 +366,7 @@ export default {
|
||||||
Starred: 'Starred',
|
Starred: 'Starred',
|
||||||
Start_of_conversation: 'Start of conversation',
|
Start_of_conversation: 'Start of conversation',
|
||||||
Started_discussion: 'Started a discussion:',
|
Started_discussion: 'Started a discussion:',
|
||||||
|
Started_call: 'Call started by {{userBy}}',
|
||||||
Submit: 'Submit',
|
Submit: 'Submit',
|
||||||
Table: 'Table',
|
Table: 'Table',
|
||||||
Take_a_photo: 'Take a photo',
|
Take_a_photo: 'Take a photo',
|
||||||
|
|
|
@ -122,6 +122,8 @@ export default {
|
||||||
Channel_Name: 'Nome do Canal',
|
Channel_Name: 'Nome do Canal',
|
||||||
Channels: 'Canais',
|
Channels: 'Canais',
|
||||||
Chats: 'Conversas',
|
Chats: 'Conversas',
|
||||||
|
Call_already_ended: 'A chamada já terminou!',
|
||||||
|
Click_to_join: 'Clique para participar!',
|
||||||
Close: 'Fechar',
|
Close: 'Fechar',
|
||||||
Close_emoji_selector: 'Fechar seletor de emojis',
|
Close_emoji_selector: 'Fechar seletor de emojis',
|
||||||
Choose: 'Escolher',
|
Choose: 'Escolher',
|
||||||
|
@ -325,6 +327,7 @@ export default {
|
||||||
starred: 'favoritou',
|
starred: 'favoritou',
|
||||||
Starred: 'Mensagens Favoritas',
|
Starred: 'Mensagens Favoritas',
|
||||||
Start_of_conversation: 'Início da conversa',
|
Start_of_conversation: 'Início da conversa',
|
||||||
|
Started_call: 'Chamada iniciada por {{userBy}}',
|
||||||
Started_discussion: 'Iniciou uma discussão:',
|
Started_discussion: 'Iniciou uma discussão:',
|
||||||
Submit: 'Enviar',
|
Submit: 'Enviar',
|
||||||
Table: 'Tabela',
|
Table: 'Tabela',
|
||||||
|
|
21
app/index.js
21
app/index.js
|
@ -1,7 +1,7 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {
|
import { createAppContainer, createSwitchNavigator } from 'react-navigation';
|
||||||
createStackNavigator, createAppContainer, createSwitchNavigator, createDrawerNavigator
|
import { createStackNavigator } from 'react-navigation-stack';
|
||||||
} from 'react-navigation';
|
import { createDrawerNavigator } from 'react-navigation-drawer';
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
import { useScreens } from 'react-native-screens'; // eslint-disable-line import/no-unresolved
|
import { useScreens } from 'react-native-screens'; // eslint-disable-line import/no-unresolved
|
||||||
import { Linking } from 'react-native';
|
import { Linking } from 'react-native';
|
||||||
|
@ -19,6 +19,7 @@ import { defaultHeader, onNavigationStateChange } from './utils/navigation';
|
||||||
import { loggerConfig, analytics } from './utils/log';
|
import { loggerConfig, analytics } from './utils/log';
|
||||||
import Toast from './containers/Toast';
|
import Toast from './containers/Toast';
|
||||||
import RocketChat from './lib/rocketchat';
|
import RocketChat from './lib/rocketchat';
|
||||||
|
import LayoutAnimation from './utils/layoutAnimation';
|
||||||
|
|
||||||
useScreens();
|
useScreens();
|
||||||
|
|
||||||
|
@ -195,7 +196,8 @@ const ChatsDrawer = createDrawerNavigator({
|
||||||
SettingsStack,
|
SettingsStack,
|
||||||
AdminPanelStack
|
AdminPanelStack
|
||||||
}, {
|
}, {
|
||||||
contentComponent: Sidebar
|
contentComponent: Sidebar,
|
||||||
|
overlayColor: '#00000090'
|
||||||
});
|
});
|
||||||
|
|
||||||
const NewMessageStack = createStackNavigator({
|
const NewMessageStack = createStackNavigator({
|
||||||
|
@ -214,7 +216,10 @@ const NewMessageStack = createStackNavigator({
|
||||||
|
|
||||||
const InsideStackModal = createStackNavigator({
|
const InsideStackModal = createStackNavigator({
|
||||||
Main: ChatsDrawer,
|
Main: ChatsDrawer,
|
||||||
NewMessageStack
|
NewMessageStack,
|
||||||
|
JitsiMeetView: {
|
||||||
|
getScreen: () => require('./views/JitsiMeetView').default
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
mode: 'modal',
|
mode: 'modal',
|
||||||
|
@ -237,11 +242,11 @@ class CustomInsideStack extends React.Component {
|
||||||
render() {
|
render() {
|
||||||
const { navigation } = this.props;
|
const { navigation } = this.props;
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<>
|
||||||
<InsideStackModal navigation={navigation} />
|
<InsideStackModal navigation={navigation} />
|
||||||
<NotificationBadge navigation={navigation} />
|
<NotificationBadge navigation={navigation} />
|
||||||
<Toast />
|
<Toast />
|
||||||
</React.Fragment>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -308,12 +313,14 @@ export default class Root extends React.Component {
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
|
<LayoutAnimation>
|
||||||
<App
|
<App
|
||||||
ref={(navigatorRef) => {
|
ref={(navigatorRef) => {
|
||||||
Navigation.setTopLevelNavigator(navigatorRef);
|
Navigation.setTopLevelNavigator(navigatorRef);
|
||||||
}}
|
}}
|
||||||
onNavigationStateChange={onNavigationStateChange}
|
onNavigationStateChange={onNavigationStateChange}
|
||||||
/>
|
/>
|
||||||
|
</LayoutAnimation>
|
||||||
</Provider>
|
</Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,90 @@
|
||||||
|
import { Database } from '@nozbe/watermelondb';
|
||||||
|
import SQLiteAdapter from '@nozbe/watermelondb/adapters/sqlite';
|
||||||
|
import logger from '@nozbe/watermelondb/utils/common/logger';
|
||||||
|
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 migrations from './model/migrations';
|
||||||
|
|
||||||
|
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,
|
||||||
|
migrations
|
||||||
|
});
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
if (!__DEV__) {
|
||||||
|
logger.silence();
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -0,0 +1,90 @@
|
||||||
|
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;
|
||||||
|
|
||||||
|
@date('jitsi_timeout') jitsiTimeout;
|
||||||
|
|
||||||
|
@field('auto_translate') autoTranslate;
|
||||||
|
|
||||||
|
@field('auto_translate_language') autoTranslateLanguage;
|
||||||
|
|
||||||
|
@json('last_message', sanitizer) lastMessage;
|
||||||
|
|
||||||
|
@children('messages') messages;
|
||||||
|
|
||||||
|
@children('threads') threads;
|
||||||
|
|
||||||
|
@children('thread_messages') threadMessages;
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { schemaMigrations, addColumns } from '@nozbe/watermelondb/Schema/migrations';
|
||||||
|
|
||||||
|
export default schemaMigrations({
|
||||||
|
migrations: [
|
||||||
|
{
|
||||||
|
toVersion: 2,
|
||||||
|
steps: [
|
||||||
|
addColumns({
|
||||||
|
table: 'subscriptions',
|
||||||
|
columns: [
|
||||||
|
{ name: 'jitsi_timeout', type: 'number', isOptional: true }
|
||||||
|
]
|
||||||
|
})
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
|
@ -0,0 +1,223 @@
|
||||||
|
import { appSchema, tableSchema } from '@nozbe/watermelondb';
|
||||||
|
|
||||||
|
export default appSchema({
|
||||||
|
version: 2,
|
||||||
|
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: 'jitsi_timeout', 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 }
|
||||||
|
]
|
||||||
|
})
|
||||||
|
]
|
||||||
|
});
|
|
@ -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 }
|
||||||
|
]
|
||||||
|
})
|
||||||
|
]
|
||||||
|
});
|
|
@ -0,0 +1 @@
|
||||||
|
export const sanitizer = r => r;
|
|
@ -0,0 +1,26 @@
|
||||||
|
import reduxStore from '../createStore';
|
||||||
|
import Navigation from '../Navigation';
|
||||||
|
|
||||||
|
const jitsiBaseUrl = ({
|
||||||
|
Jitsi_Enabled, Jitsi_SSL, Jitsi_Domain, Jitsi_URL_Room_Prefix, uniqueID
|
||||||
|
}) => {
|
||||||
|
if (!Jitsi_Enabled) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
const uniqueIdentifier = uniqueID || 'undefined';
|
||||||
|
const domain = Jitsi_Domain;
|
||||||
|
const prefix = Jitsi_URL_Room_Prefix;
|
||||||
|
|
||||||
|
const urlProtocol = Jitsi_SSL ? 'https://' : 'http://';
|
||||||
|
const urlDomain = `${ domain }/`;
|
||||||
|
|
||||||
|
return `${ urlProtocol }${ urlDomain }${ prefix }${ uniqueIdentifier }`;
|
||||||
|
};
|
||||||
|
|
||||||
|
function callJitsi(rid, onlyAudio = false) {
|
||||||
|
const { settings } = reduxStore.getState();
|
||||||
|
|
||||||
|
Navigation.navigate('JitsiMeetView', { url: `${ jitsiBaseUrl(settings) }${ rid }`, onlyAudio, rid });
|
||||||
|
}
|
||||||
|
|
||||||
|
export default callJitsi;
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -11,18 +11,18 @@ 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.jitsiTimeout = room.jitsiTimeout;
|
||||||
|
}
|
||||||
|
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 = [];
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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,33 +9,30 @@ 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.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 { id: Site_Url } = serverInfo;
|
||||||
const { id, token } = user;
|
const { id, token } = user;
|
||||||
|
|
||||||
// -1 maxFileSize means there is no limit
|
|
||||||
if (FileUpload_MaxFileSize > -1 && fileInfo.size > FileUpload_MaxFileSize) {
|
|
||||||
return reject({ error: 'error-file-too-large' }); // eslint-disable-line
|
|
||||||
}
|
|
||||||
|
|
||||||
const uploadUrl = `${ Site_Url }/api/v1/rooms.upload/${ rid }`;
|
const uploadUrl = `${ Site_Url }/api/v1/rooms.upload/${ rid }`;
|
||||||
|
|
||||||
const xhr = new XMLHttpRequest();
|
const xhr = new XMLHttpRequest();
|
||||||
|
@ -41,13 +40,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 +79,59 @@ 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);
|
||||||
|
}
|
||||||
|
try {
|
||||||
const response = JSON.parse(xhr.response);
|
const response = JSON.parse(xhr.response);
|
||||||
reject(response);
|
reject(response);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
reject(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);
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -1,69 +1,27 @@
|
||||||
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;
|
||||||
let notifyRoomListener;
|
let notifyRoomListener;
|
||||||
let messageReceivedListener;
|
let messageReceivedListener;
|
||||||
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 +32,148 @@ 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]));
|
||||||
|
const lastOpen = new Date();
|
||||||
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, lastOpen);
|
||||||
} 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);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -152,16 +199,7 @@ export default function subscribeRoom({ rid }) {
|
||||||
messageReceivedListener.then(removeListener);
|
messageReceivedListener.then(removeListener);
|
||||||
messageReceivedListener = false;
|
messageReceivedListener = false;
|
||||||
}
|
}
|
||||||
Object.keys(typingTimeouts).forEach((key) => {
|
reduxStore.dispatch(clearUserTyping());
|
||||||
if (typingTimeouts[key]) {
|
|
||||||
clearTimeout(typingTimeouts[key]);
|
|
||||||
typingTimeouts[key] = null;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
database.memoryDatabase.write(() => {
|
|
||||||
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);
|
||||||
|
|
|
@ -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,113 @@ 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,
|
||||||
|
jitsiTimeout: s.jitsiTimeout,
|
||||||
|
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 +135,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 +179,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;
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue