Merge branch 'develop' into single-server

# Conflicts:
#	android/app/build.gradle
#	android/gradle.properties
#	app/sagas/init.js
#	app/sagas/login.js
#	ios/RocketChatRN.xcodeproj/project.pbxproj
This commit is contained in:
Diego Mello 2020-05-25 17:33:45 -03:00
commit 3eb4545c6b
3746 changed files with 365596 additions and 68979 deletions

View File

@ -1,12 +1,132 @@
defaults: &defaults
working_directory: ~/repo
version: 2
macos: &macos
macos:
xcode: "11.2.1"
bash-env: &bash-env
BASH_ENV: "~/.nvm/nvm.sh"
install-npm-modules: &install-npm-modules
name: Install NPM modules
command: yarn
restore-npm-cache-linux: &restore-npm-cache-linux
name: Restore NPM cache
key: node-modules-{{ checksum "yarn.lock" }}
save-npm-cache-linux: &save-npm-cache-linux
key: node-modules-{{ checksum "yarn.lock" }}
name: Save NPM cache
paths:
- ./node_modules
restore-npm-cache-mac: &restore-npm-cache-mac
name: Restore NPM cache
key: node-v1-mac-{{ checksum "yarn.lock" }}
save-npm-cache-mac: &save-npm-cache-mac
key: node-v1-mac-{{ checksum "yarn.lock" }}
name: Save NPM cache
paths:
- ./node_modules
install-node: &install-node
name: Install Node 10
command: |
curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.6/install.sh | bash
source ~/.nvm/nvm.sh
# https://github.com/creationix/nvm/issues/1394
set +e
nvm install 10
echo 'export PATH="/home/circleci/.nvm/versions/node/v10.20.1/bin:$PATH"' >> ~/.bash_profile
source ~/.bash_profile
restore-gems-cache: &restore-gems-cache
name: Restore gems cache
key: bundle-v1-{{ checksum "ios/Gemfile.lock" }}
save-gems-cache: &save-gems-cache
name: Save gems cache
key: bundle-v1-{{ checksum "ios/Gemfile.lock" }}
paths:
- vendor/bundle
update-fastlane: &update-fastlane
name: Update Fastlane
command: |
echo "ruby-2.6.4" > ~/.ruby-version
bundle install
working_directory: ios
restore-brew-cache: &restore-brew-cache
name: Restore Brew cache
key: brew-{{ checksum "yarn.lock" }}-{{ checksum ".circleci/config.yml" }}
save-brew-cache: &save-brew-cache
name: Save brew cache
key: brew-{{ checksum "yarn.lock" }}-{{ checksum ".circleci/config.yml" }}
paths:
- /usr/local/Homebrew
install-apple-sim-utils: &install-apple-sim-utils
name: Install appleSimUtils
command: |
brew update
brew tap wix/brew
brew install wix/brew/applesimutils
rebuild-detox: &rebuild-detox
name: Rebuild Detox framework cache
command: |
npx detox clean-framework-cache
npx detox build-framework-cache
version: 2.1
# EXECUTORS
executors:
mac-env:
<<: *macos
environment:
<<: *bash-env
# COMMANDS
commands:
detox-test:
parameters:
folder:
type: string
steps:
- checkout
- attach_workspace:
at: .
- restore_cache: *restore-npm-cache-mac
- restore_cache: *restore-brew-cache
- run: *install-node
- run: *install-apple-sim-utils
- run: *install-npm-modules
- run: *rebuild-detox
- run:
name: Test
command: |
npx detox test << parameters.folder >> --configuration ios.sim.release --cleanup
# JOBS
jobs:
lint-testunit:
<<: *defaults
docker:
- image: circleci/node:8
- image: circleci/node:10
environment:
CODECOV_TOKEN: caa771ab-3d45-4756-8e2a-e1f25996fef6
@ -14,14 +134,9 @@ jobs:
steps:
- checkout
- restore_cache:
name: Restore NPM cache
key: node-modules-{{ checksum "yarn.lock" }}
- restore_cache: *restore-npm-cache-linux
- run:
name: Install NPM modules
command: |
yarn
- run: *install-npm-modules
- run:
name: Lint
@ -38,162 +153,79 @@ jobs:
command: |
yarn codecov
- save_cache:
key: node-modules-{{ checksum "yarn.lock" }}
name: Save NPM cache
paths:
- ./node_modules
- save_cache: *save-npm-cache-linux
# E2E
e2e-build:
macos:
xcode: "11.2.1"
environment:
BASH_ENV: "~/.nvm/nvm.sh"
executor: mac-env
steps:
- checkout
- restore_cache:
name: Restore NPM cache
key: node-v1-mac-{{ checksum "yarn.lock" }}
- restore_cache: *restore-npm-cache-mac
- run:
name: Install Node 8
command: |
curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.6/install.sh | bash
source ~/.nvm/nvm.sh
# https://github.com/creationix/nvm/issues/1394
set +e
nvm install 8
- restore_cache: *restore-brew-cache
- run:
name: Install appleSimUtils
command: |
brew update
brew tap wix/brew
brew install wix/brew/applesimutils
- run: *install-node
- run:
name: Install NPM modules
command: |
yarn global add detox-cli
yarn
- run: *install-apple-sim-utils
- run:
name: Rebuild Detox framework cache
command: |
detox clean-framework-cache
detox build-framework-cache
- run: *install-npm-modules
- run: *rebuild-detox
- run:
name: Build
command: |
detox build --configuration ios.sim.release
npx detox build --configuration ios.sim.release
- persist_to_workspace:
root: .
paths:
- ios/build/Build/Products/Release-iphonesimulator/RocketChatRN.app
- save_cache:
name: Save NPM cache
key: node-v1-mac-{{ checksum "yarn.lock" }}
paths:
- node_modules
- save_cache: *save-npm-cache-mac
e2e-test:
macos:
xcode: "11.2.1"
environment:
BASH_ENV: "~/.nvm/nvm.sh"
- save_cache: *save-brew-cache
e2e-test-onboarding:
executor: mac-env
steps:
- checkout
- detox-test:
folder: "./e2e/tests/onboarding"
- attach_workspace:
at: .
e2e-test-room:
executor: mac-env
steps:
- detox-test:
folder: "./e2e/tests/room"
- restore_cache:
name: Restore NPM cache
key: node-v1-mac-{{ checksum "yarn.lock" }}
- run:
name: Install Node 8
command: |
curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.6/install.sh | bash
source ~/.nvm/nvm.sh
# https://github.com/creationix/nvm/issues/1394
set +e
nvm install 8
- run:
name: Install appleSimUtils
command: |
brew update
brew tap wix/brew
brew install wix/brew/applesimutils
- run:
name: Install NPM modules
command: |
yarn global add detox-cli
yarn
- run:
name: Rebuild Detox framework cache
command: |
detox clean-framework-cache
detox build-framework-cache
- run:
name: Test
command: |
detox test --configuration ios.sim.release --cleanup
- save_cache:
name: Save NPM cache
key: node-v1-mac-{{ checksum "yarn.lock" }}
paths:
- node_modules
e2e-test-assorted:
executor: mac-env
steps:
- detox-test:
folder: "./e2e/tests/assorted"
# Android builds
android-build:
<<: *defaults
docker:
- image: circleci/android:api-28-node
environment:
# GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx4096m -XX:+HeapDumpOnOutOfMemoryError"
# GRADLE_OPTS: -Xmx2048m -Dorg.gradle.daemon=false
# JVM_OPTS: -Xmx4096m
JAVA_OPTS: '-Xms512m -Xmx2g'
GRADLE_OPTS: '-Xmx3g -Dorg.gradle.daemon=false -Dorg.gradle.jvmargs="-Xmx2g -XX:+HeapDumpOnOutOfMemoryError"'
TERM: dumb
BASH_ENV: "~/.nvm/nvm.sh"
<<: *bash-env
steps:
- checkout
- run:
name: Install Node 8
command: |
curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.6/install.sh | bash
source ~/.nvm/nvm.sh
# https://github.com/creationix/nvm/issues/1394
set +e
nvm install 8
echo 'export PATH="/home/circleci/.nvm/versions/node/v8.16.0/bin:$PATH"' >> ~/.bash_profile
source ~/.bash_profile
- run: *install-node
- restore_cache:
name: Restore NPM cache
key: node-modules-{{ checksum "yarn.lock" }}
- restore_cache: *restore-npm-cache-linux
- run:
name: Install NPM modules
command: |
yarn
- run: *install-npm-modules
- restore_cache:
name: Restore gradle cache
@ -206,6 +238,7 @@ jobs:
# echo -e "android.enableAapt2=false" >> ./gradle.properties
echo -e "android.useAndroidX=true" >> ./gradle.properties
echo -e "android.enableJetifier=true" >> ./gradle.properties
echo -e "FLIPPER_VERSION=0.33.1" >> ./gradle.properties
if [[ $KEYSTORE ]]; then
echo $KEYSTORE_BASE64 | base64 --decode > ./app/$KEYSTORE
@ -234,8 +267,7 @@ jobs:
name: Build Android App
command: |
if [[ $KEYSTORE ]]; then
# TODO: enable app bundle again
./gradlew assembleRelease
./gradlew bundleRelease
else
./gradlew assembleDebug
fi
@ -261,11 +293,7 @@ jobs:
- store_artifacts:
path: /tmp/build/outputs
- save_cache:
name: Save NPM cache
key: node-modules-{{ checksum "yarn.lock" }}
paths:
- ./node_modules
- save_cache: *save-npm-cache-linux
- save_cache:
name: Save gradle cache
@ -273,44 +301,22 @@ jobs:
paths:
- ~/.gradle
# iOS builds
ios-build:
macos:
xcode: "11.2.1"
environment:
BASH_ENV: "~/.nvm/nvm.sh"
executor: mac-env
steps:
- checkout
- restore_cache:
name: Restore gems cache
key: bundle-v1-{{ checksum "ios/Gemfile.lock" }}
- restore_cache: *restore-gems-cache
- restore_cache:
name: Restore NPM cache
key: node-v1-mac-{{ checksum "yarn.lock" }}
- restore_cache: *restore-npm-cache-mac
- run:
name: Install Node 8
command: |
curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.6/install.sh | bash
source ~/.nvm/nvm.sh
# https://github.com/creationix/nvm/issues/1394
set +e
nvm install 8
- run: *install-node
- run:
name: Install NPM modules
command: |
yarn
- run: *install-npm-modules
- run:
name: Update Fastlane
command: |
echo "ruby-2.6.4" > ~/.ruby-version
bundle install
working_directory: ios
- run: *update-fastlane
- run:
name: Set Google Services
@ -348,17 +354,9 @@ jobs:
fi
working_directory: ios
- save_cache:
name: Save NPM cache
key: node-v1-mac-{{ checksum "yarn.lock" }}
paths:
- node_modules
- save_cache: *save-npm-cache-mac
- save_cache:
name: Save gems cache
key: bundle-v1-{{ checksum "ios/Gemfile.lock" }}
paths:
- vendor/bundle
- save_cache: *save-gems-cache
- store_artifacts:
path: ios/RocketChatRN.ipa
@ -370,8 +368,7 @@ jobs:
- ios/fastlane/report.xml
ios-testflight:
macos:
xcode: "11.2.1"
executor: mac-env
steps:
- checkout
@ -379,16 +376,9 @@ jobs:
- attach_workspace:
at: ios
- restore_cache:
name: Restore gems cache
key: bundle-v1-{{ checksum "ios/Gemfile.lock" }}
- restore_cache: *restore-gems-cache
- run:
name: Update Fastlane
command: |
echo "ruby-2.4" > ~/.ruby-version
bundle install
working_directory: ios
- run: *update-fastlane
- run:
name: Fastlane Tesflight Upload
@ -396,14 +386,9 @@ jobs:
bundle exec fastlane ios beta
working_directory: ios
- save_cache:
name: Save gems cache
key: bundle-v1-{{ checksum "ios/Gemfile.lock" }}
paths:
- vendor/bundle
- save_cache: *save-gems-cache
workflows:
version: 2
build-and-test:
jobs:
- lint-testunit
@ -415,7 +400,13 @@ workflows:
- e2e-build:
requires:
- e2e-hold
- e2e-test:
- e2e-test-onboarding:
requires:
- e2e-build
- e2e-test-room:
requires:
- e2e-build
- e2e-test-assorted:
requires:
- e2e-build

View File

@ -87,6 +87,7 @@ module.exports = {
"no-regex-spaces": 2,
"no-undef": 2,
"no-unreachable": 2,
"no-unused-expressions": 0,
"no-unused-vars": [2, {
"vars": "all",
"args": "after-used"
@ -131,7 +132,23 @@ module.exports = {
"react-native/no-unused-styles": 2,
"react/jsx-one-expression-per-line": 0,
"require-await": 2,
"func-names": 0
"func-names": 0,
"react/sort-comp": ["error", {
"order": [
"static-variables",
"static-methods",
"lifecycle",
"everything-else",
"render"
]
}],
"react/static-property-placement": [0],
"arrow-parens": ["error", "as-needed", { requireForBlockBody: true }],
"react/jsx-props-no-spreading": [1],
"react/jsx-curly-newline": [0],
"react/state-in-constructor": [0],
"no-async-promise-executor": [0],
"max-classes-per-file": [0]
},
"globals": {
"__DEV__": true

1
.gitignore vendored
View File

@ -42,6 +42,7 @@ coverage/
buck-out/
\.buckd/
*.keystore
*.jks
# fastlane
#

View File

@ -208,13 +208,15 @@ Readme will guide you on how to config.
- Build your app
```bash
$ detox build --configuration ios.sim.release
$ npx detox build --configuration ios.sim.release
```
- Run tests
```bash
$ detox test --configuration ios.sim.release
$ npx detox test ./e2e/tests/onboarding --configuration ios.sim.release
$ npx detox test ./e2e/tests/room --configuration ios.sim.release
$ npx detox test ./e2e/tests/assorted --configuration ios.sim.release
```
## Storybook

14
__mocks__/expo-av.js Normal file
View File

@ -0,0 +1,14 @@
export class Sound {
loadAsync = () => {};
playAsync = () => {};
pauseAsync = () => {};
stopAsync = () => {};
setOnPlaybackStatusUpdate = () => {};
setPositionAsync = () => {};
}
export const Audio = { Sound };

View File

@ -0,0 +1,4 @@
export default {
activateKeepAwake: () => '',
deactivateKeepAwake: () => ''
};

View File

@ -2,5 +2,6 @@ export default {
getModel: () => '',
getReadableVersion: () => '',
getBundleId: () => '',
isTablet: () => false
isTablet: () => false,
hasNotch: () => false
};

File diff suppressed because it is too large Load Diff

View File

@ -19,7 +19,9 @@ import com.android.build.OutputFile
* // the name of the generated asset file containing your JS bundle
* bundleAssetName: "index.android.bundle",
*
* // the entry file for bundle generation
* // the entry file for bundle generation. If none specified and
* // "index.android.js" exists, it will be used. Otherwise "index.js" is
* // default. Can be overridden with ENTRY_FILE environment variable.
* entryFile: "index.android.js",
*
* // https://facebook.github.io/react-native/docs/performance#enable-the-ram-format
@ -80,7 +82,6 @@ import com.android.build.OutputFile
*/
project.ext.react = [
entryFile: "index.js",
bundleAssetName: "app.bundle",
iconFontNames: [ 'custom.ttf' ],
enableHermes: true, // clean and rebuild if changing
@ -141,6 +142,7 @@ android {
versionName VERSIONNAME as String
vectorDrawables.useSupportLibrary = true
manifestPlaceholders = [BugsnagAPIKey: BugsnagAPIKey as String]
missingDimensionStrategy "RNNotifications.reactNativeVersion", "reactNative60" // See note below!
}
signingConfigs {
@ -168,6 +170,14 @@ android {
signingConfig signingConfigs.release
}
}
packagingOptions {
pickFirst '**/armeabi-v7a/libc++_shared.so'
pickFirst '**/x86/libc++_shared.so'
pickFirst '**/arm64-v8a/libc++_shared.so'
pickFirst '**/x86_64/libc++_shared.so'
}
// applicationVariants are e.g. debug, release
applicationVariants.all { variant ->
variant.outputs.each { output ->
@ -202,6 +212,7 @@ dependencies {
implementation project(":reactnativekeyboardinput")
implementation project(':@react-native-community_viewpager')
implementation fileTree(dir: "libs", include: ["*.jar"])
//noinspection GradleDynamicVersion
implementation "com.facebook.react:react-native:+" // From node_modules
implementation "com.google.firebase:firebase-messaging:18.0.0"
implementation "com.google.firebase:firebase-core:16.0.9"
@ -209,6 +220,16 @@ dependencies {
implementation('com.crashlytics.sdk.android:crashlytics:2.9.9@aar') {
transitive = true
}
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.0.0"
debugImplementation("com.facebook.flipper:flipper:${FLIPPER_VERSION}") {
exclude group:'com.facebook.fbjni'
}
debugImplementation("com.facebook.flipper:flipper-network-plugin:${FLIPPER_VERSION}") {
exclude group:'com.facebook.flipper'
}
debugImplementation("com.facebook.flipper:flipper-fresco-plugin:${FLIPPER_VERSION}") {
exclude group:'com.facebook.flipper'
}
if (enableHermes) {
def hermesPath = "../../node_modules/hermes-engine/android/";

View File

@ -0,0 +1,67 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* <p>This source code is licensed under the MIT license found in the LICENSE file in the root
* directory of this source tree.
*/
package com.rndiffapp;
import android.content.Context;
import com.facebook.flipper.android.AndroidFlipperClient;
import com.facebook.flipper.android.utils.FlipperUtils;
import com.facebook.flipper.core.FlipperClient;
import com.facebook.flipper.plugins.crashreporter.CrashReporterPlugin;
import com.facebook.flipper.plugins.databases.DatabasesFlipperPlugin;
import com.facebook.flipper.plugins.fresco.FrescoFlipperPlugin;
import com.facebook.flipper.plugins.inspector.DescriptorMapping;
import com.facebook.flipper.plugins.inspector.InspectorFlipperPlugin;
import com.facebook.flipper.plugins.network.FlipperOkhttpInterceptor;
import com.facebook.flipper.plugins.network.NetworkFlipperPlugin;
import com.facebook.flipper.plugins.react.ReactFlipperPlugin;
import com.facebook.flipper.plugins.sharedpreferences.SharedPreferencesFlipperPlugin;
import com.facebook.react.ReactInstanceManager;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.modules.network.NetworkingModule;
import okhttp3.OkHttpClient;
public class ReactNativeFlipper {
public static void initializeFlipper(Context context, ReactInstanceManager reactInstanceManager) {
if (FlipperUtils.shouldEnableFlipper(context)) {
final FlipperClient client = AndroidFlipperClient.getInstance(context);
client.addPlugin(new InspectorFlipperPlugin(context, DescriptorMapping.withDefaults()));
client.addPlugin(new ReactFlipperPlugin());
client.addPlugin(new DatabasesFlipperPlugin(context));
client.addPlugin(new SharedPreferencesFlipperPlugin(context));
client.addPlugin(CrashReporterPlugin.getInstance());
NetworkFlipperPlugin networkFlipperPlugin = new NetworkFlipperPlugin();
NetworkingModule.setCustomClientBuilder(
new NetworkingModule.CustomClientBuilder() {
@Override
public void apply(OkHttpClient.Builder builder) {
builder.addNetworkInterceptor(new FlipperOkhttpInterceptor(networkFlipperPlugin));
}
});
client.addPlugin(networkFlipperPlugin);
client.start();
// Fresco Plugin needs to ensure that ImagePipelineFactory is initialized
// Hence we run if after all native modules have been initialized
ReactContext reactContext = reactInstanceManager.getCurrentReactContext();
if (reactContext == null) {
reactInstanceManager.addReactInstanceEventListener(
new ReactInstanceManager.ReactInstanceEventListener() {
@Override
public void onReactContextInitialized(ReactContext reactContext) {
reactInstanceManager.removeReactInstanceEventListener(this);
reactContext.runOnNativeModulesQueueThread(
new Runnable() {
@Override
public void run() {
client.addPlugin(new FrescoFlipperPlugin());
}
});
}
});
} else {
client.addPlugin(new FrescoFlipperPlugin());
}
}
}
}

View File

@ -3,13 +3,13 @@
package="chat.rocket.reactnative">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CAMERA" />
<!-- <uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.DOWNLOAD_WITHOUT_NOTIFICATION" />
<uses-permission-sdk-23 android:name="android.permission.VIBRATE"/>
<uses-permission android:name="android.permission.DOWNLOAD_WITHOUT_NOTIFICATION" /> -->
<!-- <uses-permission-sdk-23 android:name="android.permission.VIBRATE"/> -->
<application
android:name=".MainApplication"
@ -30,7 +30,7 @@
<activity
android:name=".MainActivity"
android:label="@string/app_name"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|uiMode"
android:launchMode="singleTask"
android:windowSoftInputMode="adjustResize"
android:exported="true">

View File

@ -9,9 +9,11 @@ import com.facebook.react.PackageList;
import com.facebook.hermes.reactexecutor.HermesExecutorFactory;
import com.facebook.react.bridge.JavaScriptExecutorFactory;
import com.facebook.react.ReactApplication;
import com.facebook.react.ReactInstanceManager;
import com.facebook.react.ReactNativeHost;
import com.facebook.react.ReactPackage;
import com.facebook.soloader.SoLoader;
import java.lang.reflect.InvocationTargetException;
import chat.rocket.reactnative.generated.BasePackageList;
@ -39,7 +41,7 @@ import java.util.List;
public class MainApplication extends Application implements ReactApplication, INotificationsApplication {
private final ReactModuleRegistryProvider mModuleRegistryProvider = new ReactModuleRegistryProvider(new BasePackageList().getPackageList(), Arrays.<SingletonModule>asList());
private final ReactModuleRegistryProvider mModuleRegistryProvider = new ReactModuleRegistryProvider(new BasePackageList().getPackageList(), null);
private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) {
@Override
@ -58,7 +60,11 @@ public class MainApplication extends Application implements ReactApplication, IN
packages.add(new RNNotificationsPackage(MainApplication.this));
packages.add(new WatermelonDBPackage());
packages.add(new RNCViewPagerPackage());
packages.add(new ModuleRegistryAdapter(mModuleRegistryProvider));
// packages.add(new ModuleRegistryAdapter(mModuleRegistryProvider));
List<ReactPackage> unimodules = Arrays.<ReactPackage>asList(
new ModuleRegistryAdapter(mModuleRegistryProvider)
);
packages.addAll(unimodules);
return packages;
}
@ -82,6 +88,38 @@ public class MainApplication extends Application implements ReactApplication, IN
public void onCreate() {
super.onCreate();
SoLoader.init(this, /* native exopackage */ false);
initializeFlipper(this, getReactNativeHost().getReactInstanceManager());
}
/**
* Loads Flipper in React Native templates. Call this in the onCreate method with something like
* initializeFlipper(this, getReactNativeHost().getReactInstanceManager());
*
* @param context
* @param reactInstanceManager
*/
private static void initializeFlipper(
Context context, ReactInstanceManager reactInstanceManager) {
if (BuildConfig.DEBUG) {
try {
/*
We use reflection here to pick up the class that initializes Flipper,
since Flipper library is not available in release mode
*/
Class<?> aClass = Class.forName("chat.rocket.reactnative");
aClass
.getMethod("initializeFlipper", Context.class, ReactInstanceManager.class)
.invoke(null, context, reactInstanceManager);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
}
@Override

View File

@ -11,6 +11,9 @@ public class BasePackageList {
new expo.modules.constants.ConstantsPackage(),
new expo.modules.filesystem.FileSystemPackage(),
new expo.modules.haptics.HapticsPackage(),
new expo.modules.imageloader.ImageLoaderPackage(),
new expo.modules.keepawake.KeepAwakePackage(),
new expo.modules.localauthentication.LocalAuthenticationPackage(),
new expo.modules.permissions.PermissionsPackage(),
new expo.modules.webbrowser.WebBrowserPackage()
);

View File

@ -18,7 +18,7 @@ buildscript {
}
}
dependencies {
classpath 'com.android.tools.build:gradle:3.4.2'
classpath 'com.android.tools.build:gradle:3.5.2'
classpath 'com.google.gms:google-services:4.2.0'
classpath 'io.fabric.tools:gradle:1.28.1'
classpath 'com.google.firebase:perf-plugin:1.2.1'
@ -42,16 +42,14 @@ allprojects {
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"
url "https://github.com/jitsi/jitsi-maven-repository/raw/master/releases"
}
google()
jcenter()
maven { url 'https://maven.google.com' }
maven { url "https://jitpack.io" }
maven { url 'https://www.jitpack.io' }
}
}
@ -64,6 +62,12 @@ subprojects { subproject ->
defaultConfig {
targetSdkVersion 28
}
variantFilter { variant ->
def names = variant.flavors*.name
if (names.contains("reactNative59")) {
setIgnore(true)
}
}
}
}
}

View File

@ -19,7 +19,11 @@
# android.enableAapt2=false # commenting this makes notifications to stop working
# android.useDeprecatedNdk=true
org.gradle.jvmargs=-Xmx2048M -XX\:MaxHeapSize\=32g
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# Automatically convert third-party libraries to use AndroidX
android.enableJetifier=true
APPLICATIONID=chat.rocket.reactnative
VERSIONNAME=4.5.1
@ -29,3 +33,6 @@ KEYSTORE=my-upload-key.keystore
KEY_ALIAS=my-key-alias
KEYSTORE_PASSWORD=
KEY_PASSWORD=
# Version of flipper SDK to use with React Native
FLIPPER_VERSION=0.33.1

View File

@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.5-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-6.0.1-all.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

8
android/gradlew vendored
View File

@ -7,7 +7,7 @@
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
@ -44,7 +44,7 @@ APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=''
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
@ -125,8 +125,8 @@ if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin, switch paths to Windows format before running java
if $cygwin ; then
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`

View File

@ -31,7 +31,7 @@ export const ROOMS = createRequestTypes('ROOMS', [
'OPEN_SEARCH_HEADER',
'CLOSE_SEARCH_HEADER'
]);
export const ROOM = createRequestTypes('ROOM', ['LEAVE', 'DELETE', 'REMOVED', 'USER_TYPING']);
export const ROOM = createRequestTypes('ROOM', ['SUBSCRIBE', 'UNSUBSCRIBE', 'LEAVE', 'DELETE', 'REMOVED', 'CLOSE', 'FORWARD', 'USER_TYPING']);
export const APP = createRequestTypes('APP', ['START', 'READY', 'INIT', 'INIT_LOCAL_SETTINGS']);
export const MESSAGES = createRequestTypes('MESSAGES', ['REPLY_BROADCAST']);
export const CREATE_CHANNEL = createRequestTypes('CREATE_CHANNEL', [...defaultTypes]);
@ -64,3 +64,4 @@ export const INVITE_LINKS = createRequestTypes('INVITE_LINKS', [
...defaultTypes
]);
export const SETTINGS = createRequestTypes('SETTINGS', ['CLEAR', 'ADD']);
export const APP_STATE = createRequestTypes('APP_STATE', ['FOREGROUND', 'BACKGROUND']);

View File

@ -1,5 +1,19 @@
import * as types from './actionsTypes';
export function subscribeRoom(rid) {
return {
type: types.ROOM.SUBSCRIBE,
rid
};
}
export function unsubscribeRoom(rid) {
return {
type: types.ROOM.UNSUBSCRIBE,
rid
};
}
export function leaveRoom(rid, t) {
return {
type: types.ROOM.LEAVE,
@ -16,6 +30,21 @@ export function deleteRoom(rid, t) {
};
}
export function closeRoom(rid) {
return {
type: types.ROOM.CLOSE,
rid
};
}
export function forwardRoom(rid, transferData) {
return {
type: types.ROOM.FORWARD,
transferData,
rid
};
}
export function removedRoom() {
return {
type: types.ROOM.REMOVED

View File

@ -46,7 +46,14 @@ export const themes = {
messageboxBackground: '#ffffff',
searchboxBackground: '#E6E6E7',
buttonBackground: '#414852',
buttonText: '#ffffff'
buttonText: '#ffffff',
passcodeBackground: '#EEEFF1',
passcodeButtonActive: '#E4E7EA',
passcodeLockIcon: '#6C727A',
passcodePrimary: '#2F343D',
passcodeSecondary: '#6C727A',
passcodeDotEmpty: '#CBCED1',
passcodeDotFull: '#6C727A'
},
dark: {
backgroundColor: '#030b1b',
@ -81,7 +88,14 @@ export const themes = {
messageboxBackground: '#0b182c',
searchboxBackground: '#192d4d',
buttonBackground: '#414852',
buttonText: '#ffffff'
buttonText: '#ffffff',
passcodeBackground: '#030C1B',
passcodeButtonActive: '#0B182C',
passcodeLockIcon: '#6C727A',
passcodePrimary: '#FFFFFF',
passcodeSecondary: '#CBCED1',
passcodeDotEmpty: '#CBCED1',
passcodeDotFull: '#6C727A'
},
black: {
backgroundColor: '#000000',
@ -100,7 +114,7 @@ export const themes = {
infoText: '#6d6d72',
tintColor: '#1e9bfe',
auxiliaryTintColor: '#cdcdcd',
actionTintColor: '#1ea1fe',
actionTintColor: '#1e9bfe',
separatorColor: '#272728',
navbarBackground: '#0d0d0d',
headerBorder: '#323232',
@ -116,6 +130,13 @@ export const themes = {
messageboxBackground: '#0d0d0d',
searchboxBackground: '#1f1f1f',
buttonBackground: '#414852',
buttonText: '#ffffff'
buttonText: '#ffffff',
passcodeBackground: '#000000',
passcodeButtonActive: '#0E0D0D',
passcodeLockIcon: '#6C727A',
passcodePrimary: '#FFFFFF',
passcodeSecondary: '#CBCED1',
passcodeDotEmpty: '#CBCED1',
passcodeDotFull: '#6C727A'
}
};

View File

@ -0,0 +1,12 @@
export const PASSCODE_KEY = 'kPasscode';
export const LOCKED_OUT_TIMER_KEY = 'kLockedOutTimer';
export const ATTEMPTS_KEY = 'kAttempts';
export const LOCAL_AUTHENTICATE_EMITTER = 'LOCAL_AUTHENTICATE';
export const CHANGE_PASSCODE_EMITTER = 'CHANGE_PASSCODE';
export const PASSCODE_LENGTH = 6;
export const MAX_ATTEMPTS = 6;
export const TIME_TO_LOCK = 30000;
export const DEFAULT_AUTO_LOCK = 1800;

View File

@ -68,6 +68,9 @@ export default {
LDAP_Enable: {
type: 'valueAsBoolean'
},
Livechat_request_comment_when_closing_conversation: {
type: 'valueAsBoolean'
},
Jitsi_Enabled: {
type: 'valueAsBoolean'
},
@ -125,6 +128,9 @@ export default {
uniqueID: {
type: 'valueAsString'
},
UI_Allow_room_names_with_special_chars: {
type: 'valueAsBoolean'
},
UI_Use_Real_Name: {
type: 'valueAsBoolean'
},
@ -157,5 +163,11 @@ export default {
},
CAS_login_url: {
type: 'valueAsString'
},
Force_Screen_Lock: {
type: 'valueAsBoolean'
},
Force_Screen_Lock_After: {
type: 'valueAsNumber'
}
};

View File

@ -2,12 +2,14 @@ import React from 'react';
import PropTypes from 'prop-types';
import { View } from 'react-native';
import FastImage from 'react-native-fast-image';
import Touchable from 'react-native-platform-touchable';
import { settings as RocketChatSettings } from '@rocket.chat/sdk';
import Touch from '../utils/touch';
import { avatarURL } from '../utils/avatar';
import Emoji from './markdown/Emoji';
const Avatar = React.memo(({
text, size, baseUrl, borderRadius, style, avatar, type, children, userId, token, onPress, theme
text, size, baseUrl, borderRadius, style, avatar, type, children, userId, token, onPress, theme, emoji, getCustomEmoji
}) => {
const avatarStyle = {
width: size,
@ -23,7 +25,15 @@ const Avatar = React.memo(({
type, text, size, userId, token, avatar, baseUrl
});
let image = (
let image = emoji ? (
<Emoji
theme={theme}
baseUrl={baseUrl}
getCustomEmoji={getCustomEmoji}
isMessageContainsOnlyEmoji
literal={emoji}
/>
) : (
<FastImage
style={avatarStyle}
source={{
@ -36,9 +46,9 @@ const Avatar = React.memo(({
if (onPress) {
image = (
<Touch onPress={onPress} theme={theme}>
<Touchable onPress={onPress}>
{image}
</Touch>
</Touchable>
);
}
@ -55,6 +65,7 @@ Avatar.propTypes = {
style: PropTypes.any,
text: PropTypes.string,
avatar: PropTypes.string,
emoji: PropTypes.string,
size: PropTypes.number,
borderRadius: PropTypes.number,
type: PropTypes.string,
@ -62,7 +73,8 @@ Avatar.propTypes = {
userId: PropTypes.string,
token: PropTypes.string,
theme: PropTypes.string,
onPress: PropTypes.func
onPress: PropTypes.func,
getCustomEmoji: PropTypes.func
};
Avatar.defaultProps = {

View File

@ -23,7 +23,7 @@ export const FormContainerInner = ({ children }) => (
</View>
);
const FormContainer = ({ children, theme }) => (
const FormContainer = ({ children, theme, testID }) => (
<KeyboardView
style={{ backgroundColor: themes[theme].backgroundColor }}
contentContainerStyle={sharedStyles.container}
@ -31,7 +31,7 @@ const FormContainer = ({ children, theme }) => (
>
<StatusBar theme={theme} />
<ScrollView {...scrollPersistTaps} style={sharedStyles.container} contentContainerStyle={[sharedStyles.containerScrollView, styles.scrollView]}>
<SafeAreaView style={sharedStyles.container} forceInset={{ top: 'never' }}>
<SafeAreaView style={sharedStyles.container} forceInset={{ top: 'never' }} testID={testID}>
{children}
<AppVersion theme={theme} />
</SafeAreaView>
@ -41,6 +41,7 @@ const FormContainer = ({ children, theme }) => (
FormContainer.propTypes = {
theme: PropTypes.string,
testID: PropTypes.string,
children: PropTypes.element
};

View File

@ -0,0 +1,29 @@
import React from 'react';
import { View, Text, StyleSheet } from 'react-native';
import PropTypes from 'prop-types';
import sharedStyles from '../views/Styles';
import { themes } from '../constants/colors';
const styles = StyleSheet.create({
infoContainer: {
padding: 15
},
infoText: {
fontSize: 14,
...sharedStyles.textRegular
}
});
const ItemInfo = React.memo(({ info, theme }) => (
<View style={[styles.infoContainer, { backgroundColor: themes[theme].auxiliaryBackground }]}>
<Text style={[styles.infoText, { color: themes[theme].infoText }]}>{info}</Text>
</View>
));
ItemInfo.propTypes = {
info: PropTypes.string,
theme: PropTypes.string
};
export default ItemInfo;

View File

@ -12,7 +12,7 @@ import sharedStyles from '../views/Styles';
import { themes } from '../constants/colors';
import { loginRequest as loginRequestAction } from '../actions/login';
import Button from './Button';
import OnboardingSeparator from './OnboardingSeparator';
import OrSeparator from './OrSeparator';
import Touch from '../utils/touch';
import I18n from '../i18n';
import random from '../utils/random';
@ -252,12 +252,12 @@ class LoginServices extends React.PureComponent {
style={styles.options}
color={themes[theme].actionTintColor}
/>
<OnboardingSeparator theme={theme} />
<OrSeparator theme={theme} />
</>
);
}
if (length > 0 && separator) {
return <OnboardingSeparator theme={theme} />;
return <OrSeparator theme={theme} />;
}
return null;
}

View File

@ -64,7 +64,7 @@ const MentionItem = ({
content = (
<>
<Text style={[styles.slash, { backgroundColor: themes[theme].borderColor, color: themes[theme].tintColor }]}>/</Text>
<Text style={[styles.mentionText, { color: themes[theme].titleText }]}>{ item.command}</Text>
<Text style={[styles.mentionText, { color: themes[theme].titleText }]}>{item.id}</Text>
</>
);
}

View File

@ -5,6 +5,7 @@ import {
} from 'react-native';
import { AudioRecorder, AudioUtils } from 'react-native-audio';
import { BorderlessButton } from 'react-native-gesture-handler';
import { activateKeepAwake, deactivateKeepAwake } from 'expo-keep-awake';
import RNFetchBlob from 'rn-fetch-blob';
import styles from './styles';
@ -59,7 +60,8 @@ export default class extends React.PureComponent {
SampleRate: 22050,
Channels: 1,
AudioQuality: 'Low',
AudioEncoding: 'aac'
AudioEncoding: 'aac',
OutputFormat: 'aac_adts'
});
AudioRecorder.onProgress = (data) => {
@ -74,12 +76,16 @@ export default class extends React.PureComponent {
}
};
AudioRecorder.startRecording();
activateKeepAwake();
}
componentWillUnmount() {
if (this.recording) {
this.cancelAudioMessage();
}
deactivateKeepAwake();
}
finishRecording = (didSucceed, filePath, size) => {

View File

@ -8,7 +8,7 @@ const SendButton = React.memo(({ theme, onPress }) => (
onPress={onPress}
testID='messagebox-send-message'
accessibilityLabel='Send_message'
icon='send1'
icon='Send-active'
theme={theme}
/>
));

View File

@ -190,7 +190,7 @@ class MessageBox extends Component {
});
}
componentWillReceiveProps(nextProps) {
UNSAFE_componentWillReceiveProps(nextProps) {
const { isFocused, editing, replying } = this.props;
if (!isFocused()) {
return;
@ -306,9 +306,9 @@ class MessageBox extends Component {
if (!isTextEmpty) {
try {
const { start, end } = this.component._lastNativeSelection;
const { start, end } = this.component?.lastNativeSelection;
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
const regexp = /(#|@|:|^\/)([a-z0-9._-]+)$/im;
const result = lastNativeText.substr(0, cursor).match(regexp);
@ -339,7 +339,7 @@ class MessageBox extends Component {
}
const { trackingType } = this.state;
const msg = this.text;
const { start, end } = this.component._lastNativeSelection;
const { start, end } = this.component?.lastNativeSelection;
const cursor = Math.max(start, end);
const regexp = /([a-z0-9._-]+)$/im;
const result = msg.substr(0, cursor).replace(regexp, '');
@ -383,8 +383,8 @@ class MessageBox extends Component {
let newText = '';
// if messagebox has an active cursor
if (this.component && this.component._lastNativeSelection) {
const { start, end } = this.component._lastNativeSelection;
if (this.component?.lastNativeSelection) {
const { start, end } = this.component.lastNativeSelection;
const cursor = Math.max(start, end);
newText = `${ text.substr(0, cursor) }${ emoji }${ text.substr(cursor) }`;
} else {

View File

@ -24,7 +24,7 @@ const styles = StyleSheet.create({
}
});
const DateSeparator = React.memo(({ theme }) => {
const OrSeparator = React.memo(({ theme }) => {
const line = { backgroundColor: themes[theme].borderColor };
const text = { color: themes[theme].auxiliaryText };
return (
@ -36,8 +36,8 @@ const DateSeparator = React.memo(({ theme }) => {
);
});
DateSeparator.propTypes = {
OrSeparator.propTypes = {
theme: PropTypes.string
};
export default DateSeparator;
export default OrSeparator;

View File

@ -0,0 +1,47 @@
import React from 'react';
import { Text } from 'react-native';
import PropTypes from 'prop-types';
import styles from './styles';
import { themes } from '../../../constants/colors';
import Touch from '../../../utils/touch';
import { CustomIcon } from '../../../lib/Icons';
const Button = React.memo(({
text, disabled, theme, onPress, icon
}) => {
const press = () => onPress && onPress(text);
return (
<Touch
style={[styles.buttonView, { backgroundColor: 'transparent' }]}
underlayColor={themes[theme].passcodeButtonActive}
rippleColor={themes[theme].passcodeButtonActive}
enabled={!disabled}
theme={theme}
onPress={press}
>
{
icon
? (
<CustomIcon name={icon} size={36} color={themes[theme].passcodePrimary} />
)
: (
<Text style={[styles.buttonText, { color: themes[theme].passcodePrimary }]}>
{text}
</Text>
)
}
</Touch>
);
});
Button.propTypes = {
text: PropTypes.string,
icon: PropTypes.string,
theme: PropTypes.string,
disabled: PropTypes.bool,
onPress: PropTypes.func
};
export default Button;

View File

@ -0,0 +1,51 @@
import React from 'react';
import { View } from 'react-native';
import _ from 'lodash';
import PropTypes from 'prop-types';
import styles from './styles';
import { themes } from '../../../constants/colors';
const SIZE_EMPTY = 12;
const SIZE_FULL = 16;
const Dots = React.memo(({ passcode, theme, length }) => (
<View style={styles.dotsContainer}>
{_.range(length).map((val) => {
const lengthSup = (passcode.length >= val + 1);
const height = lengthSup ? SIZE_FULL : SIZE_EMPTY;
const width = lengthSup ? SIZE_FULL : SIZE_EMPTY;
let backgroundColor = '';
if (lengthSup && passcode.length > 0) {
backgroundColor = themes[theme].passcodeDotFull;
} else {
backgroundColor = themes[theme].passcodeDotEmpty;
}
const borderRadius = lengthSup ? SIZE_FULL / 2 : SIZE_EMPTY / 2;
const marginRight = lengthSup ? 10 - (SIZE_FULL - SIZE_EMPTY) / 2 : 10;
const marginLeft = lengthSup ? 10 - (SIZE_FULL - SIZE_EMPTY) / 2 : 10;
return (
<View style={styles.dotsView}>
<View
style={{
height,
width,
borderRadius,
backgroundColor,
marginRight,
marginLeft
}}
/>
</View>
);
})}
</View>
));
Dots.propTypes = {
passcode: PropTypes.string,
theme: PropTypes.string,
length: PropTypes.string
};
export default Dots;

View File

@ -0,0 +1,22 @@
import React from 'react';
import { View } from 'react-native';
import { Row } from 'react-native-easy-grid';
import PropTypes from 'prop-types';
import styles from './styles';
import { themes } from '../../../constants/colors';
import { CustomIcon } from '../../../lib/Icons';
const LockIcon = React.memo(({ theme }) => (
<Row style={styles.row}>
<View style={styles.iconView}>
<CustomIcon name='lock' size={40} color={themes[theme].passcodeLockIcon} />
</View>
</Row>
));
LockIcon.propTypes = {
theme: PropTypes.string
};
export default LockIcon;

View File

@ -0,0 +1,74 @@
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { Grid } from 'react-native-easy-grid';
import { themes } from '../../../constants/colors';
import { resetAttempts } from '../../../utils/localAuthentication';
import { TYPE } from '../constants';
import { getLockedUntil, getDiff } from '../utils';
import I18n from '../../../i18n';
import styles from './styles';
import Title from './Title';
import Subtitle from './Subtitle';
import LockIcon from './LockIcon';
const Timer = React.memo(({ time, theme, setStatus }) => {
const calcTimeLeft = () => {
const diff = getDiff(time);
if (diff > 0) {
return Math.floor((diff / 1000) % 60);
}
};
const [timeLeft, setTimeLeft] = useState(calcTimeLeft());
useEffect(() => {
setTimeout(() => {
setTimeLeft(calcTimeLeft());
if (timeLeft <= 1) {
resetAttempts();
setStatus(TYPE.ENTER);
}
}, 1000);
});
if (!timeLeft) {
return null;
}
return <Subtitle text={I18n.t('Passcode_app_locked_subtitle', { timeLeft })} theme={theme} />;
});
const Locked = React.memo(({ theme, setStatus }) => {
const [lockedUntil, setLockedUntil] = useState(null);
const readItemFromStorage = async() => {
const l = await getLockedUntil();
setLockedUntil(l);
};
useEffect(() => {
readItemFromStorage();
}, []);
return (
<Grid style={[styles.grid, { backgroundColor: themes[theme].passcodeBackground }]} r>
<LockIcon theme={theme} />
<Title text={I18n.t('Passcode_app_locked_title')} theme={theme} />
<Timer theme={theme} time={lockedUntil} setStatus={setStatus} />
</Grid>
);
});
Locked.propTypes = {
theme: PropTypes.string,
setStatus: PropTypes.func
};
Timer.propTypes = {
time: PropTypes.string,
theme: PropTypes.string,
setStatus: PropTypes.func
};
export default Locked;

View File

@ -0,0 +1,22 @@
import React from 'react';
import { View, Text } from 'react-native';
import { Row } from 'react-native-easy-grid';
import PropTypes from 'prop-types';
import styles from './styles';
import { themes } from '../../../constants/colors';
const Subtitle = React.memo(({ text, theme }) => (
<Row style={styles.row}>
<View style={styles.subtitleView}>
<Text style={[styles.textSubtitle, { color: themes[theme].passcodeSecondary }]}>{text}</Text>
</View>
</Row>
));
Subtitle.propTypes = {
text: PropTypes.string,
theme: PropTypes.string
};
export default Subtitle;

View File

@ -0,0 +1,22 @@
import React from 'react';
import { View, Text } from 'react-native';
import { Row } from 'react-native-easy-grid';
import PropTypes from 'prop-types';
import styles from './styles';
import { themes } from '../../../constants/colors';
const Title = React.memo(({ text, theme }) => (
<Row style={styles.row}>
<View style={styles.titleView}>
<Text style={[styles.textTitle, { color: themes[theme].passcodePrimary }]}>{text}</Text>
</View>
</Row>
));
Title.propTypes = {
text: PropTypes.string,
theme: PropTypes.string
};
export default Title;

View File

@ -0,0 +1,139 @@
import React, {
useState, forwardRef, useImperativeHandle, useRef
} from 'react';
import { Col, Row, Grid } from 'react-native-easy-grid';
import _ from 'lodash';
import PropTypes from 'prop-types';
import * as Animatable from 'react-native-animatable';
import * as Haptics from 'expo-haptics';
import styles from './styles';
import Button from './Button';
import Dots from './Dots';
import { TYPE } from '../constants';
import { themes } from '../../../constants/colors';
import { PASSCODE_LENGTH } from '../../../constants/localAuthentication';
import LockIcon from './LockIcon';
import Title from './Title';
import Subtitle from './Subtitle';
const Base = forwardRef(({
theme, type, onEndProcess, previousPasscode, title, subtitle, onError, showBiometry, onBiometryPress
}, ref) => {
const rootRef = useRef();
const dotsRef = useRef();
const [passcode, setPasscode] = useState('');
const clearPasscode = () => setPasscode('');
const wrongPasscode = () => {
clearPasscode();
dotsRef?.current?.shake(500);
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
};
const animate = (animation, duration = 500) => {
rootRef?.current?.[animation](duration);
};
const onPressNumber = text => setPasscode((p) => {
const currentPasscode = p + text;
if (currentPasscode?.length === PASSCODE_LENGTH) {
switch (type) {
case TYPE.CHOOSE:
onEndProcess(currentPasscode);
break;
case TYPE.CONFIRM:
if (currentPasscode !== previousPasscode) {
onError();
} else {
onEndProcess(currentPasscode);
}
break;
case TYPE.ENTER:
onEndProcess(currentPasscode);
break;
default:
break;
}
}
return currentPasscode;
});
const onPressDelete = () => setPasscode((p) => {
if (p?.length > 0) {
const newPasscode = p.slice(0, -1);
return newPasscode;
}
return '';
});
useImperativeHandle(ref, () => ({
wrongPasscode, animate, clearPasscode
}));
return (
<Animatable.View ref={rootRef} style={styles.container}>
<Grid style={[styles.grid, { backgroundColor: themes[theme].passcodeBackground }]}>
<LockIcon theme={theme} />
<Title text={title} theme={theme} />
<Subtitle text={subtitle} theme={theme} />
<Row style={styles.row}>
<Animatable.View ref={dotsRef}>
<Dots passcode={passcode} theme={theme} length={PASSCODE_LENGTH} />
</Animatable.View>
</Row>
<Row style={[styles.row, styles.buttonRow]}>
{_.range(1, 4).map(i => (
<Col key={i} style={styles.colButton}>
<Button text={i} theme={theme} onPress={onPressNumber} />
</Col>
))}
</Row>
<Row style={[styles.row, styles.buttonRow]}>
{_.range(4, 7).map(i => (
<Col key={i} style={styles.colButton}>
<Button text={i} theme={theme} onPress={onPressNumber} />
</Col>
))}
</Row>
<Row style={[styles.row, styles.buttonRow]}>
{_.range(7, 10).map(i => (
<Col key={i} style={styles.colButton}>
<Button text={i} theme={theme} onPress={onPressNumber} />
</Col>
))}
</Row>
<Row style={[styles.row, styles.buttonRow]}>
{showBiometry
? (
<Col style={styles.colButton}>
<Button icon='fingerprint' theme={theme} onPress={onBiometryPress} />
</Col>
)
: <Col style={styles.colButton} />}
<Col style={styles.colButton}>
<Button text='0' theme={theme} onPress={onPressNumber} />
</Col>
<Col style={styles.colButton}>
<Button icon='backspace' theme={theme} onPress={onPressDelete} />
</Col>
</Row>
</Grid>
</Animatable.View>
);
});
Base.propTypes = {
theme: PropTypes.string,
type: PropTypes.string,
previousPasscode: PropTypes.string,
title: PropTypes.string,
subtitle: PropTypes.string,
showBiometry: PropTypes.string,
onEndProcess: PropTypes.func,
onError: PropTypes.func,
onBiometryPress: PropTypes.func
};
export default Base;

View File

@ -0,0 +1,70 @@
import { StyleSheet } from 'react-native';
import sharedStyles from '../../../views/Styles';
export default StyleSheet.create({
container: {
flex: 1
},
titleView: {
justifyContent: 'center'
},
subtitleView: {
justifyContent: 'center',
height: 32
},
row: {
flex: 0,
alignItems: 'center',
justifyContent: 'center'
},
buttonRow: {
height: 102
},
colButton: {
flex: 0,
marginLeft: 12,
marginRight: 12,
alignItems: 'center',
width: 78,
height: 78
},
buttonText: {
fontSize: 28,
...sharedStyles.textRegular
},
buttonView: {
alignItems: 'center',
justifyContent: 'center',
width: 78,
height: 78,
borderRadius: 4
},
textTitle: {
fontSize: 22,
...sharedStyles.textRegular
},
textSubtitle: {
fontSize: 16,
...sharedStyles.textMedium
},
dotsContainer: {
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
marginTop: 24,
marginBottom: 40
},
dotsView: {
justifyContent: 'center',
alignItems: 'center',
height: 16
},
grid: {
justifyContent: 'center',
flexDirection: 'column'
},
iconView: {
marginVertical: 16
}
});

View File

@ -0,0 +1,69 @@
import React, { useState, useRef } from 'react';
import PropTypes from 'prop-types';
import * as Haptics from 'expo-haptics';
import { gestureHandlerRootHOC } from 'react-native-gesture-handler';
import Base from './Base';
import { TYPE } from './constants';
import I18n from '../../i18n';
const PasscodeChoose = ({ theme, finishProcess, force = false }) => {
const chooseRef = useRef(null);
const confirmRef = useRef(null);
const [subtitle, setSubtitle] = useState(null);
const [status, setStatus] = useState(TYPE.CHOOSE);
const [previousPasscode, setPreviouPasscode] = useState(null);
const firstStep = (p) => {
setTimeout(() => {
setStatus(TYPE.CONFIRM);
setPreviouPasscode(p);
confirmRef?.current?.clearPasscode();
}, 200);
};
const changePasscode = p => finishProcess && finishProcess(p);
const onError = () => {
setTimeout(() => {
setStatus(TYPE.CHOOSE);
setSubtitle(I18n.t('Passcode_choose_error'));
chooseRef?.current?.animate('shake');
chooseRef?.current?.clearPasscode();
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
}, 200);
};
if (status === TYPE.CONFIRM) {
return (
<Base
ref={confirmRef}
theme={theme}
type={TYPE.CONFIRM}
onEndProcess={changePasscode}
previousPasscode={previousPasscode}
title={I18n.t('Passcode_choose_confirm_title')}
onError={onError}
/>
);
}
return (
<Base
ref={chooseRef}
theme={theme}
type={TYPE.CHOOSE}
onEndProcess={firstStep}
title={I18n.t('Passcode_choose_title')}
subtitle={subtitle || (force ? I18n.t('Passcode_choose_force_set') : null)}
/>
);
};
PasscodeChoose.propTypes = {
theme: PropTypes.string,
force: PropTypes.bool,
finishProcess: PropTypes.func
};
export default gestureHandlerRootHOC(PasscodeChoose);

View File

@ -0,0 +1,106 @@
import React, { useEffect, useRef, useState } from 'react';
import { useAsyncStorage } from '@react-native-community/async-storage';
import RNUserDefaults from 'rn-user-defaults';
import PropTypes from 'prop-types';
import { gestureHandlerRootHOC } from 'react-native-gesture-handler';
import * as Haptics from 'expo-haptics';
import { sha256 } from 'js-sha256';
import Base from './Base';
import Locked from './Base/Locked';
import { TYPE } from './constants';
import {
ATTEMPTS_KEY, LOCKED_OUT_TIMER_KEY, PASSCODE_KEY, MAX_ATTEMPTS
} from '../../constants/localAuthentication';
import { resetAttempts, biometryAuth } from '../../utils/localAuthentication';
import { getLockedUntil, getDiff } from './utils';
import I18n from '../../i18n';
const PasscodeEnter = ({ theme, hasBiometry, finishProcess }) => {
const ref = useRef(null);
let attempts = 0;
let lockedUntil = false;
const [passcode, setPasscode] = useState(null);
const [status, setStatus] = useState(null);
const { getItem: getAttempts, setItem: setAttempts } = useAsyncStorage(ATTEMPTS_KEY);
const { setItem: setLockedUntil } = useAsyncStorage(LOCKED_OUT_TIMER_KEY);
const fetchPasscode = async() => {
const p = await RNUserDefaults.get(PASSCODE_KEY);
setPasscode(p);
};
const biometry = async() => {
if (hasBiometry && status === TYPE.ENTER) {
const result = await biometryAuth();
if (result?.success) {
finishProcess();
}
}
};
const readStorage = async() => {
lockedUntil = await getLockedUntil();
if (lockedUntil) {
const diff = getDiff(lockedUntil);
if (diff <= 1) {
await resetAttempts();
setStatus(TYPE.ENTER);
} else {
attempts = await getAttempts();
setStatus(TYPE.LOCKED);
}
} else {
setStatus(TYPE.ENTER);
}
await fetchPasscode();
biometry();
};
useEffect(() => {
readStorage();
}, [status]);
const onEndProcess = (p) => {
setTimeout(() => {
if (sha256(p) === passcode) {
finishProcess();
} else {
attempts += 1;
if (attempts >= MAX_ATTEMPTS) {
setStatus(TYPE.LOCKED);
setLockedUntil(new Date().toISOString());
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
} else {
ref.current.wrongPasscode();
setAttempts(attempts?.toString());
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Warning);
}
}
}, 200);
};
if (status === TYPE.LOCKED) {
return <Locked theme={theme} setStatus={setStatus} />;
}
return (
<Base
ref={ref}
theme={theme}
type={TYPE.ENTER}
title={I18n.t('Passcode_enter_title')}
showBiometry={hasBiometry}
onEndProcess={onEndProcess}
onBiometryPress={biometry}
/>
);
};
PasscodeEnter.propTypes = {
theme: PropTypes.string,
hasBiometry: PropTypes.string,
finishProcess: PropTypes.func
};
export default gestureHandlerRootHOC(PasscodeEnter);

View File

@ -0,0 +1,6 @@
export const TYPE = {
CHOOSE: 'choose',
CONFIRM: 'confirm',
ENTER: 'enter',
LOCKED: 'locked'
};

View File

@ -0,0 +1,4 @@
import PasscodeEnter from './PasscodeEnter';
import PasscodeChoose from './PasscodeChoose';
export { PasscodeEnter, PasscodeChoose };

View File

@ -0,0 +1,14 @@
import AsyncStorage from '@react-native-community/async-storage';
import moment from 'moment';
import { LOCKED_OUT_TIMER_KEY, TIME_TO_LOCK } from '../../constants/localAuthentication';
export const getLockedUntil = async() => {
const t = await AsyncStorage.getItem(LOCKED_OUT_TIMER_KEY);
if (t) {
return moment(t).add(TIME_TO_LOCK);
}
return null;
};
export const getDiff = t => new Date(t) - new Date();

View File

@ -2,7 +2,7 @@ import React from 'react';
import { Image, StyleSheet } from 'react-native';
import PropTypes from 'prop-types';
import { CustomIcon } from '../lib/Icons';
import { themes } from '../constants/colors';
import { STATUS_COLORS, themes } from '../constants/colors';
const styles = StyleSheet.create({
style: {
@ -15,7 +15,7 @@ const styles = StyleSheet.create({
});
const RoomTypeIcon = React.memo(({
type, size, isGroupChat, style, theme
type, size, isGroupChat, status, style, theme
}) => {
if (!type) {
return null;
@ -36,7 +36,7 @@ const RoomTypeIcon = React.memo(({
}
return <CustomIcon name='at' size={13} style={[styles.style, styles.discussion, { color }]} />;
} if (type === 'l') {
return <CustomIcon name='livechat' size={13} style={[styles.style, styles.discussion, { color }]} />;
return <CustomIcon name='omnichannel' size={13} style={[styles.style, styles.discussion, { color: STATUS_COLORS[status] }]} />;
}
return <Image source={{ uri: 'lock' }} style={[styles.style, style, { width: size, height: size, tintColor: color }]} />;
});
@ -45,6 +45,7 @@ RoomTypeIcon.propTypes = {
theme: PropTypes.string,
type: PropTypes.string,
isGroupChat: PropTypes.bool,
status: PropTypes.string,
size: PropTypes.number,
style: PropTypes.object
};

View File

@ -64,8 +64,10 @@ export default class RCTextInput extends React.PureComponent {
inputRef: PropTypes.func,
testID: PropTypes.string,
iconLeft: PropTypes.string,
iconRight: PropTypes.string,
placeholder: PropTypes.string,
left: PropTypes.element,
onIconRightPress: PropTypes.func,
theme: PropTypes.string
}
@ -90,6 +92,19 @@ export default class RCTextInput extends React.PureComponent {
);
}
get iconRight() {
const { iconRight, onIconRightPress, theme } = this.props;
return (
<BorderlessButton onPress={onIconRightPress} style={[styles.iconContainer, styles.iconRight]}>
<CustomIcon
name={iconRight}
style={{ color: themes[theme].bodyText }}
size={20}
/>
</BorderlessButton>
);
}
get iconPassword() {
const { showPassword } = this.state;
const { testID, theme } = this.props;
@ -117,7 +132,7 @@ export default class RCTextInput extends React.PureComponent {
render() {
const { showPassword } = this.state;
const {
label, left, error, loading, secureTextEntry, containerStyle, inputRef, iconLeft, inputStyle, testID, placeholder, theme, ...inputProps
label, left, error, loading, secureTextEntry, containerStyle, inputRef, iconLeft, iconRight, inputStyle, testID, placeholder, theme, ...inputProps
} = this.props;
const { dangerColor } = themes[theme];
return (
@ -140,7 +155,7 @@ export default class RCTextInput extends React.PureComponent {
style={[
styles.input,
iconLeft && styles.inputIconLeft,
secureTextEntry && styles.inputIconRight,
(secureTextEntry || iconRight) && styles.inputIconRight,
{
backgroundColor: themes[theme].backgroundColor,
borderColor: themes[theme].separatorColor,
@ -165,6 +180,7 @@ export default class RCTextInput extends React.PureComponent {
{...inputProps}
/>
{iconLeft ? this.iconLeft : null}
{iconRight ? this.iconRight : null}
{secureTextEntry ? this.iconPassword : null}
{loading ? this.loading : null}
{left}

View File

@ -92,7 +92,7 @@ const TwoFactor = React.memo(({ theme, split }) => {
isVisible={visible}
hideModalContentWhileAnimating
>
<View style={styles.container}>
<View style={styles.container} testID='two-factor'>
<View style={[styles.content, split && [sharedStyles.modal, sharedStyles.modalFormSheet], { backgroundColor: themes[theme].backgroundColor }]}>
<Text style={[styles.title, { color }]}>{I18n.t(method?.title || 'Two_Factor_Authentication')}</Text>
{method?.text ? <Text style={[styles.subtitle, { color }]}>{I18n.t(method.text)}</Text> : null}
@ -106,6 +106,7 @@ const TwoFactor = React.memo(({ theme, split }) => {
keyboardType={method?.keyboardType}
secureTextEntry={method?.secureTextEntry}
error={data.invalid && { error: 'totp-invalid', reason: I18n.t('Code_or_password_invalid') }}
testID='two-factor-input'
/>
{isEmail && <Text style={[styles.sendEmail, { color }]} onPress={sendEmail}>{I18n.t('Send_me_the_code_again')}</Text>}
<View style={styles.buttonContainer}>
@ -123,6 +124,7 @@ const TwoFactor = React.memo(({ theme, split }) => {
style={styles.button}
onPress={onSubmit}
theme={theme}
testID='two-factor-send'
/>
</View>
</View>

View File

@ -12,11 +12,13 @@ import styles from './styles';
const keyExtractor = item => item.value.toString();
const Chip = ({ item, onSelect, theme }) => (
const Chip = ({
item, onSelect, style, theme
}) => (
<Touchable
key={item.value}
onPress={() => onSelect(item)}
style={[styles.chip, { backgroundColor: themes[theme].auxiliaryBackground }]}
style={[styles.chip, { backgroundColor: themes[theme].auxiliaryBackground }, style]}
background={Touchable.Ripple(themes[theme].bannerBackground)}
>
<>
@ -29,17 +31,21 @@ const Chip = ({ item, onSelect, theme }) => (
Chip.propTypes = {
item: PropTypes.object,
onSelect: PropTypes.func,
style: PropTypes.object,
theme: PropTypes.string
};
const Chips = ({ items, onSelect, theme }) => (
const Chips = ({
items, onSelect, style, theme
}) => (
<View style={styles.chips}>
{items.map(item => <Chip key={keyExtractor(item)} item={item} onSelect={onSelect} theme={theme} />)}
{items.map(item => <Chip key={keyExtractor(item)} item={item} onSelect={onSelect} style={style} theme={theme} />)}
</View>
);
Chips.propTypes = {
items: PropTypes.array,
onSelect: PropTypes.func,
style: PropTypes.object,
theme: PropTypes.string
};

View File

@ -1,5 +1,5 @@
import React from 'react';
import { View } from 'react-native';
import { View, Text } from 'react-native';
import PropTypes from 'prop-types';
import Touchable from 'react-native-platform-touchable';
@ -9,16 +9,16 @@ import ActivityIndicator from '../../ActivityIndicator';
import styles from './styles';
const Input = ({
children, open, theme, loading, inputStyle, disabled
children, onPress, theme, loading, inputStyle, placeholder, disabled
}) => (
<Touchable
onPress={() => open(true)}
onPress={onPress}
style={[{ backgroundColor: themes[theme].backgroundColor }, inputStyle]}
background={Touchable.Ripple(themes[theme].bannerBackground)}
disabled={disabled}
>
<View style={[styles.input, { borderColor: themes[theme].separatorColor }]}>
{children}
{placeholder ? <Text style={[styles.pickerText, { color: themes[theme].auxiliaryText }]}>{placeholder}</Text> : children}
{
loading
? <ActivityIndicator style={[styles.loading, styles.icon]} />
@ -29,10 +29,11 @@ const Input = ({
);
Input.propTypes = {
children: PropTypes.node,
open: PropTypes.func,
onPress: PropTypes.func,
theme: PropTypes.string,
inputStyle: PropTypes.object,
disabled: PropTypes.bool,
placeholder: PropTypes.string,
loading: PropTypes.bool
};

View File

@ -43,14 +43,14 @@ export const MultiSelect = React.memo(({
inputStyle,
theme
}) => {
const [selected, select] = useState(values || []);
const [selected, select] = useState(Array.isArray(values) ? values : []);
const [open, setOpen] = useState(false);
const [search, onSearchChange] = useState('');
const [currentValue, setCurrentValue] = useState('');
const [showContent, setShowContent] = useState(false);
useEffect(() => {
if (values) {
if (Array.isArray(values)) {
select(values);
}
}, [values]);
@ -136,7 +136,7 @@ export const MultiSelect = React.memo(({
/>
) : (
<Input
open={onShow}
onPress={onShow}
theme={theme}
loading={loading}
disabled={disabled}
@ -150,7 +150,7 @@ export const MultiSelect = React.memo(({
const items = options.filter(option => selected.includes(option.value));
button = (
<Input
open={onShow}
onPress={onShow}
theme={theme}
loading={loading}
disabled={disabled}

View File

@ -4,7 +4,6 @@ import PropTypes from 'prop-types';
import { BLOCK_CONTEXT } from '@rocket.chat/ui-kit';
import { themes } from '../../constants/colors';
import sharedStyles from '../../views/Styles';
const styles = StyleSheet.create({
content: {
@ -18,11 +17,7 @@ const styles = StyleSheet.create({
},
text: {
flex: 1,
padding: 4,
fontSize: 16,
lineHeight: 22,
textAlignVertical: 'center',
...sharedStyles.textRegular
padding: 4
},
field: {
marginVertical: 6
@ -54,7 +49,7 @@ export const Section = ({
accessory && accessoriesRight.includes(accessory.type) ? styles.row : styles.column
]}
>
{text ? <Text style={[styles.text, { color: themes[theme].bodyText }]}>{parser.text(text)}</Text> : null}
{text ? <View style={styles.text}>{parser.text(text)}</View> : null}
{fields ? <Fields fields={fields} theme={theme} parser={parser} /> : null}
{accessory ? <Accessory element={{ blockId, appId, ...accessory }} parser={parser} /> : null}
</View>

View File

@ -1,6 +1,6 @@
/* eslint-disable class-methods-use-this */
import React, { useContext } from 'react';
import { StyleSheet } from 'react-native';
import { StyleSheet, Text } from 'react-native';
import {
uiKitMessage,
UiKitParserMessage,
@ -13,8 +13,9 @@ import Markdown from '../markdown';
import Button from '../Button';
import TextInput from '../TextInput';
import { useBlockContext } from './utils';
import { useBlockContext, textParser } from './utils';
import { themes } from '../../constants/colors';
import sharedStyles from '../../views/Styles';
import { Divider } from './Divider';
import { Section } from './Section';
@ -37,6 +38,12 @@ const styles = StyleSheet.create({
},
button: {
marginBottom: 16
},
text: {
fontSize: 16,
lineHeight: 22,
textAlignVertical: 'center',
...sharedStyles.textRegular
}
});
@ -46,7 +53,7 @@ class MessageParser extends UiKitParserMessage {
text({ text, type } = { text: '' }, context) {
const { theme } = useContext(ThemeContext);
if (type !== 'mrkdwn') {
return text;
return <Text style={[styles.text, { color: themes[theme].bodyText }]}>{text}</Text>;
}
const isContext = context === BLOCK_CONTEXT.CONTEXT;
@ -70,7 +77,7 @@ class MessageParser extends UiKitParserMessage {
<Button
key={actionId}
type={style}
title={this.text(text)}
title={textParser([text])}
loading={loading}
onPress={() => action({ value })}
style={styles.button}

View File

@ -11,11 +11,10 @@ const AtMention = React.memo(({
}) => {
let mentionStyle = { ...styles.mention, color: themes[theme].buttonText };
if (mention === 'all' || mention === 'here') {
mentionStyle = {
...mentionStyle,
...styles.mentionAll
};
} else if (mention === username) {
return <Text style={[mentionStyle, styles.mentionAll, ...style]}>{mention}</Text>;
}
if (mention === username) {
mentionStyle = {
...mentionStyle,
backgroundColor: themes[theme].actionTintColor

View File

@ -9,10 +9,10 @@ import { themes } from '../../constants/colors';
import styles from './styles';
const Emoji = React.memo(({
emojiName, literal, isMessageContainsOnlyEmoji, getCustomEmoji, baseUrl, customEmojis, style = [], theme
literal, isMessageContainsOnlyEmoji, getCustomEmoji, baseUrl, customEmojis = true, style = [], theme
}) => {
const emojiUnicode = shortnameToUnicode(literal);
const emoji = getCustomEmoji && getCustomEmoji(emojiName);
const emoji = getCustomEmoji && getCustomEmoji(literal.replace(/:/g, ''));
if (emoji && customEmojis) {
return (
<CustomEmoji
@ -36,7 +36,6 @@ const Emoji = React.memo(({
});
Emoji.propTypes = {
emojiName: PropTypes.string,
literal: PropTypes.string,
isMessageContainsOnlyEmoji: PropTypes.bool,
getCustomEmoji: PropTypes.func,

View File

@ -261,13 +261,12 @@ class Markdown extends PureComponent {
);
}
renderEmoji = ({ emojiName, literal }) => {
renderEmoji = ({ literal }) => {
const {
getCustomEmoji, baseUrl, customEmojis = true, style, theme
getCustomEmoji, baseUrl, customEmojis, style, theme
} = this.props;
return (
<MarkdownEmoji
emojiName={emojiName}
literal={literal}
isMessageContainsOnlyEmoji={this.isMessageContainsOnlyEmoji}
getCustomEmoji={getCustomEmoji}

View File

@ -8,7 +8,7 @@ import Video from './Video';
import Reply from './Reply';
const Attachments = React.memo(({
attachments, timeFormat, user, baseUrl, showAttachment, getCustomEmoji, theme
attachments, timeFormat, showAttachment, getCustomEmoji, theme
}) => {
if (!attachments || attachments.length === 0) {
return null;
@ -16,25 +16,23 @@ const Attachments = React.memo(({
return attachments.map((file, index) => {
if (file.image_url) {
return <Image key={file.image_url} file={file} user={user} baseUrl={baseUrl} showAttachment={showAttachment} getCustomEmoji={getCustomEmoji} theme={theme} />;
return <Image key={file.image_url} file={file} showAttachment={showAttachment} getCustomEmoji={getCustomEmoji} theme={theme} />;
}
if (file.audio_url) {
return <Audio key={file.audio_url} file={file} user={user} baseUrl={baseUrl} getCustomEmoji={getCustomEmoji} theme={theme} />;
return <Audio key={file.audio_url} file={file} getCustomEmoji={getCustomEmoji} theme={theme} />;
}
if (file.video_url) {
return <Video key={file.video_url} file={file} user={user} baseUrl={baseUrl} showAttachment={showAttachment} getCustomEmoji={getCustomEmoji} theme={theme} />;
return <Video key={file.video_url} file={file} showAttachment={showAttachment} getCustomEmoji={getCustomEmoji} theme={theme} />;
}
// eslint-disable-next-line react/no-array-index-key
return <Reply key={index} index={index} attachment={file} timeFormat={timeFormat} user={user} baseUrl={baseUrl} getCustomEmoji={getCustomEmoji} theme={theme} />;
return <Reply key={index} index={index} attachment={file} timeFormat={timeFormat} getCustomEmoji={getCustomEmoji} theme={theme} />;
});
}, (prevProps, nextProps) => isEqual(prevProps.attachments, nextProps.attachments) && prevProps.theme === nextProps.theme);
Attachments.propTypes = {
attachments: PropTypes.array,
timeFormat: PropTypes.string,
user: PropTypes.object,
baseUrl: PropTypes.string,
showAttachment: PropTypes.func,
getCustomEmoji: PropTypes.func,
theme: PropTypes.string

View File

@ -3,18 +3,31 @@ import PropTypes from 'prop-types';
import {
View, StyleSheet, Text, Easing, Dimensions
} from 'react-native';
import Video from 'react-native-video';
import { Audio } from 'expo-av';
import Slider from '@react-native-community/slider';
import moment from 'moment';
import equal from 'deep-equal';
import Touchable from 'react-native-platform-touchable';
import { activateKeepAwake, deactivateKeepAwake } from 'expo-keep-awake';
import Touchable from './Touchable';
import Markdown from '../markdown';
import { CustomIcon } from '../../lib/Icons';
import sharedStyles from '../../views/Styles';
import { themes } from '../../constants/colors';
import { isAndroid, isIOS } from '../../utils/deviceInfo';
import { withSplit } from '../../split';
import MessageContext from './Context';
import ActivityIndicator from '../ActivityIndicator';
const mode = {
allowsRecordingIOS: false,
playsInSilentModeIOS: true,
staysActiveInBackground: false,
shouldDuckAndroid: true,
playThroughEarpieceAndroid: false,
interruptionModeIOS: Audio.INTERRUPTION_MODE_IOS_DO_NOT_MIX,
interruptionModeAndroid: Audio.INTERRUPTION_MODE_ANDROID_DO_NOT_MIX
};
const styles = StyleSheet.create({
audioContainer: {
@ -31,6 +44,9 @@ const styles = StyleSheet.create({
alignItems: 'center',
backgroundColor: 'transparent'
},
audioLoading: {
marginHorizontal: 8
},
slider: {
flex: 1
},
@ -51,29 +67,36 @@ const sliderAnimationConfig = {
delay: 0
};
const Button = React.memo(({ paused, onPress, theme }) => (
const Button = React.memo(({
loading, paused, onPress, theme
}) => (
<Touchable
style={styles.playPauseButton}
onPress={onPress}
hitSlop={BUTTON_HIT_SLOP}
background={Touchable.SelectableBackgroundBorderless()}
>
<CustomIcon name={paused ? 'play' : 'pause'} size={36} color={themes[theme].tintColor} />
{
loading
? <ActivityIndicator style={[styles.playPauseButton, styles.audioLoading]} theme={theme} />
: <CustomIcon name={paused ? 'play' : 'pause'} size={36} color={themes[theme].tintColor} />
}
</Touchable>
));
Button.propTypes = {
loading: PropTypes.bool,
paused: PropTypes.bool,
theme: PropTypes.string,
onPress: PropTypes.func
};
Button.displayName = 'MessageAudioButton';
class Audio extends React.Component {
class MessageAudio extends React.Component {
static contextType = MessageContext;
static propTypes = {
file: PropTypes.object.isRequired,
baseUrl: PropTypes.string.isRequired,
user: PropTypes.object.isRequired,
theme: PropTypes.string,
split: PropTypes.bool,
getCustomEmoji: PropTypes.func
@ -81,18 +104,39 @@ class Audio extends React.Component {
constructor(props) {
super(props);
const { baseUrl, file, user } = props;
this.state = {
loading: false,
currentTime: 0,
duration: 0,
paused: true,
uri: `${ baseUrl }${ file.audio_url }?rc_uid=${ user.id }&rc_token=${ user.token }`
paused: true
};
this.sound = new Audio.Sound();
this.sound.setOnPlaybackStatusUpdate(this.onPlaybackStatusUpdate);
}
async componentDidMount() {
const { file } = this.props;
const { baseUrl, user } = this.context;
let url = file.audio_url;
if (!url.startsWith('http')) {
url = `${ baseUrl }${ file.audio_url }`;
}
this.setState({ loading: true });
try {
await Audio.setAudioModeAsync(mode);
await this.sound.loadAsync({ uri: `${ url }?rc_uid=${ user.id }&rc_token=${ user.token }` });
} catch {
// Do nothing
}
this.setState({ loading: false });
}
shouldComponentUpdate(nextProps, nextState) {
const {
currentTime, duration, paused, uri
currentTime, duration, paused, loading
} = this.state;
const { file, split, theme } = this.props;
if (nextProps.theme !== theme) {
@ -107,58 +151,108 @@ class Audio extends React.Component {
if (nextState.paused !== paused) {
return true;
}
if (nextState.uri !== uri) {
return true;
}
if (!equal(nextProps.file, file)) {
return true;
}
if (nextProps.split !== split) {
return true;
}
if (nextState.loading !== loading) {
return true;
}
return false;
}
componentDidUpdate() {
const { paused } = this.state;
if (paused) {
deactivateKeepAwake();
} else {
activateKeepAwake();
}
}
async componentWillUnmount() {
try {
await this.sound.stopAsync();
} catch {
// Do nothing
}
}
onPlaybackStatusUpdate = (status) => {
if (status) {
this.onLoad(status);
this.onProgress(status);
this.onEnd(status);
}
}
onLoad = (data) => {
this.setState({ duration: data.duration > 0 ? data.duration : 0 });
const duration = data.durationMillis / 1000;
this.setState({ duration: duration > 0 ? duration : 0 });
}
onProgress = (data) => {
const { duration } = this.state;
if (data.currentTime <= duration) {
this.setState({ currentTime: data.currentTime });
const currentTime = data.positionMillis / 1000;
if (currentTime <= duration) {
this.setState({ currentTime });
}
}
onEnd = () => {
onEnd = async(data) => {
if (data.didJustFinish) {
try {
await this.sound.stopAsync();
this.setState({ paused: true, currentTime: 0 });
requestAnimationFrame(() => {
this.player.seek(0);
});
} catch {
// do nothing
}
}
}
get duration() {
const { duration } = this.state;
return formatTime(duration);
const { currentTime, duration } = this.state;
return formatTime(currentTime || duration);
}
setRef = ref => this.player = ref;
togglePlayPause = () => {
const { paused } = this.state;
this.setState({ paused: !paused });
this.setState({ paused: !paused }, this.playPause);
}
onValueChange = value => this.setState({ currentTime: value });
playPause = async() => {
const { paused } = this.state;
try {
if (paused) {
await this.sound.pauseAsync();
} else {
await this.sound.playAsync();
}
} catch {
// Do nothing
}
}
onValueChange = async(value) => {
try {
this.setState({ currentTime: value });
await this.sound.setPositionAsync(value * 1000);
} catch {
// Do nothing
}
}
render() {
const {
uri, paused, currentTime, duration
loading, paused, currentTime, duration
} = this.state;
const {
user, baseUrl, file, getCustomEmoji, split, theme
file, getCustomEmoji, split, theme
} = this.props;
const { description } = file;
const { baseUrl, user } = this.context;
if (!baseUrl) {
return null;
@ -173,17 +267,7 @@ class Audio extends React.Component {
split && sharedStyles.tabletContent
]}
>
<Video
ref={this.setRef}
source={{ uri }}
onLoad={this.onLoad}
onProgress={this.onProgress}
onEnd={this.onEnd}
paused={paused}
repeat={false}
ignoreSilentSwitch='ignore'
/>
<Button paused={paused} onPress={this.togglePlayPause} theme={theme} />
<Button loading={loading} paused={paused} onPress={this.togglePlayPause} theme={theme} />
<Slider
style={styles.slider}
value={currentTime}
@ -205,4 +289,4 @@ class Audio extends React.Component {
}
}
export default withSplit(Audio);
export default withSplit(MessageAudio);

View File

@ -1,17 +1,19 @@
import React from 'react';
import React, { useContext } from 'react';
import { View, Text } from 'react-native';
import Touchable from 'react-native-platform-touchable';
import PropTypes from 'prop-types';
import Touchable from './Touchable';
import { CustomIcon } from '../../lib/Icons';
import styles from './styles';
import { BUTTON_HIT_SLOP } from './utils';
import I18n from '../../i18n';
import { themes } from '../../constants/colors';
import MessageContext from './Context';
const Broadcast = React.memo(({
author, user, broadcast, replyBroadcast, theme
author, broadcast, theme
}) => {
const { user, replyBroadcast } = useContext(MessageContext);
const isOwn = author._id === user.id;
if (broadcast && !isOwn) {
return (
@ -36,10 +38,8 @@ const Broadcast = React.memo(({
Broadcast.propTypes = {
author: PropTypes.object,
user: PropTypes.object,
broadcast: PropTypes.bool,
theme: PropTypes.string,
replyBroadcast: PropTypes.func
theme: PropTypes.string
};
Broadcast.displayName = 'MessageBroadcast';

View File

@ -1,8 +1,8 @@
import React from 'react';
import { View, Text } from 'react-native';
import Touchable from 'react-native-platform-touchable';
import PropTypes from 'prop-types';
import Touchable from './Touchable';
import { formatLastMessage, BUTTON_HIT_SLOP } from './utils';
import styles from './styles';
import I18n from '../../i18n';

View File

@ -1,4 +1,4 @@
import React from 'react';
import React, { useContext } from 'react';
import { Text, View } from 'react-native';
import PropTypes from 'prop-types';
import equal from 'deep-equal';
@ -8,6 +8,7 @@ import styles from './styles';
import Markdown from '../markdown';
import { getInfoMessage } from './utils';
import { themes } from '../../constants/colors';
import MessageContext from './Context';
const Content = React.memo((props) => {
if (props.isInfo) {
@ -26,12 +27,13 @@ const Content = React.memo((props) => {
if (props.tmid && !props.msg) {
content = <Text style={[styles.text, { color: themes[props.theme].bodyText }]}>{I18n.t('Sent_an_attachment')}</Text>;
} else {
const { baseUrl, user } = useContext(MessageContext);
content = (
<Markdown
msg={props.msg}
baseUrl={props.baseUrl}
baseUrl={baseUrl}
getCustomEmoji={props.getCustomEmoji}
username={props.user.username}
username={user.username}
isEdited={props.isEdited}
numberOfLines={(props.tmid && !props.isThreadRoom) ? 1 : 0}
preview={props.tmid && !props.isThreadRoom}
@ -77,8 +79,6 @@ Content.propTypes = {
msg: PropTypes.string,
theme: PropTypes.string,
isEdited: PropTypes.bool,
baseUrl: PropTypes.string,
user: PropTypes.object,
getCustomEmoji: PropTypes.func,
channels: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
mentions: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),

View File

@ -0,0 +1,4 @@
import React from 'react';
const MessageContext = React.createContext();
export default MessageContext;

View File

@ -1,20 +1,22 @@
import React from 'react';
import React, { useContext } from 'react';
import { View, Text } from 'react-native';
import Touchable from 'react-native-platform-touchable';
import PropTypes from 'prop-types';
import Touchable from './Touchable';
import { formatLastMessage, formatMessageCount, BUTTON_HIT_SLOP } from './utils';
import styles from './styles';
import I18n from '../../i18n';
import { CustomIcon } from '../../lib/Icons';
import { DISCUSSION } from './constants';
import { themes } from '../../constants/colors';
import MessageContext from './Context';
const Discussion = React.memo(({
msg, dcount, dlm, onDiscussionPress, theme
msg, dcount, dlm, theme
}) => {
const time = formatLastMessage(dlm);
const buttonText = formatMessageCount(dcount, DISCUSSION);
const { onDiscussionPress } = useContext(MessageContext);
return (
<>
<Text style={[styles.startedDiscussion, { color: themes[theme].auxiliaryText }]}>{I18n.t('Started_discussion')}</Text>
@ -55,8 +57,7 @@ Discussion.propTypes = {
msg: PropTypes.string,
dcount: PropTypes.number,
dlm: PropTypes.string,
theme: PropTypes.string,
onDiscussionPress: PropTypes.func
theme: PropTypes.string
};
Discussion.displayName = 'MessageDiscussion';

View File

@ -6,7 +6,7 @@ import shortnameToUnicode from '../../utils/shortnameToUnicode';
import CustomEmoji from '../EmojiPicker/CustomEmoji';
const Emoji = React.memo(({
content, standardEmojiStyle, customEmojiStyle, baseUrl, getCustomEmoji
content, baseUrl, standardEmojiStyle, customEmojiStyle, getCustomEmoji
}) => {
const parsedContent = content.replace(/^:|:$/g, '');
const emoji = getCustomEmoji(parsedContent);
@ -18,9 +18,9 @@ const Emoji = React.memo(({
Emoji.propTypes = {
content: PropTypes.string,
baseUrl: PropTypes.string,
standardEmojiStyle: PropTypes.object,
customEmojiStyle: PropTypes.object,
baseUrl: PropTypes.string,
getCustomEmoji: PropTypes.func
};
Emoji.displayName = 'MessageEmoji';

View File

@ -1,18 +1,19 @@
import React from 'react';
import React, { useContext } from 'react';
import { View } from 'react-native';
import PropTypes from 'prop-types';
import FastImage from 'react-native-fast-image';
import equal from 'deep-equal';
import Touchable from 'react-native-platform-touchable';
import { createImageProgress } from 'react-native-image-progress';
import * as Progress from 'react-native-progress';
import Touchable from './Touchable';
import Markdown from '../markdown';
import styles from './styles';
import { formatAttachmentUrl } from '../../lib/utils';
import { withSplit } from '../../split';
import { themes } from '../../constants/colors';
import sharedStyles from '../../views/Styles';
import MessageContext from './Context';
const ImageProgress = createImageProgress(FastImage);
@ -41,8 +42,9 @@ export const MessageImage = React.memo(({ img, theme }) => (
));
const ImageContainer = React.memo(({
file, imageUrl, baseUrl, user, showAttachment, getCustomEmoji, split, theme
file, imageUrl, showAttachment, getCustomEmoji, split, theme
}) => {
const { baseUrl, user } = useContext(MessageContext);
const img = imageUrl || formatAttachmentUrl(file.image_url, user.id, user.token, baseUrl);
if (!img) {
return null;
@ -71,8 +73,6 @@ const ImageContainer = React.memo(({
ImageContainer.propTypes = {
file: PropTypes.object,
imageUrl: PropTypes.string,
baseUrl: PropTypes.string,
user: PropTypes.object,
showAttachment: PropTypes.func,
theme: PropTypes.string,
getCustomEmoji: PropTypes.func,

View File

@ -1,8 +1,10 @@
import React from 'react';
import React, { useContext } from 'react';
import PropTypes from 'prop-types';
import { View } from 'react-native';
import Touchable from 'react-native-platform-touchable';
import MessageContext from './Context';
import User from './User';
import styles from './styles';
import RepliedThread from './RepliedThread';
@ -111,10 +113,11 @@ const MessageTouchable = React.memo((props) => {
</View>
);
}
const { onPress, onLongPress } = useContext(MessageContext);
return (
<Touchable
onLongPress={props.onLongPress}
onPress={props.onPress}
onLongPress={onLongPress}
onPress={onPress}
disabled={props.isInfo || props.archived || props.isTemp}
>
<View>
@ -129,9 +132,7 @@ MessageTouchable.propTypes = {
hasError: PropTypes.bool,
isInfo: PropTypes.bool,
isTemp: PropTypes.bool,
archived: PropTypes.bool,
onLongPress: PropTypes.func,
onPress: PropTypes.func
archived: PropTypes.bool
};
Message.propTypes = {
@ -143,7 +144,6 @@ Message.propTypes = {
hasError: PropTypes.bool,
style: PropTypes.any,
onLongPress: PropTypes.func,
onPress: PropTypes.func,
isReadReceiptEnabled: PropTypes.bool,
unread: PropTypes.bool,
theme: PropTypes.string

View File

@ -1,34 +1,34 @@
import React from 'react';
import React, { useContext } from 'react';
import PropTypes from 'prop-types';
import { TouchableOpacity } from 'react-native';
import Avatar from '../Avatar';
import styles from './styles';
import MessageContext from './Context';
const MessageAvatar = React.memo(({
isHeader, avatar, author, baseUrl, user, small, navToRoomInfo
isHeader, avatar, author, small, navToRoomInfo, emoji, getCustomEmoji, theme
}) => {
const { baseUrl, user } = useContext(MessageContext);
if (isHeader && author) {
const navParam = {
t: 'd',
rid: author._id
};
return (
<TouchableOpacity
onPress={() => navToRoomInfo(navParam)}
disabled={author._id === user.id}
>
<Avatar
style={small ? styles.avatarSmall : styles.avatar}
text={avatar ? '' : author.username}
size={small ? 20 : 36}
borderRadius={small ? 2 : 4}
onPress={author._id === user.id ? undefined : () => navToRoomInfo(navParam)}
getCustomEmoji={getCustomEmoji}
avatar={avatar}
emoji={emoji}
baseUrl={baseUrl}
userId={user.id}
token={user.token}
theme={theme}
/>
</TouchableOpacity>
);
}
return null;
@ -37,11 +37,12 @@ const MessageAvatar = React.memo(({
MessageAvatar.propTypes = {
isHeader: PropTypes.bool,
avatar: PropTypes.string,
emoji: PropTypes.string,
author: PropTypes.obj,
baseUrl: PropTypes.string,
user: PropTypes.obj,
small: PropTypes.bool,
navToRoomInfo: PropTypes.func
navToRoomInfo: PropTypes.func,
getCustomEmoji: PropTypes.func,
theme: PropTypes.string
};
MessageAvatar.displayName = 'MessageAvatar';

View File

@ -1,16 +1,18 @@
import React from 'react';
import Touchable from 'react-native-platform-touchable';
import React, { useContext } from 'react';
import PropTypes from 'prop-types';
import Touchable from './Touchable';
import { CustomIcon } from '../../lib/Icons';
import styles from './styles';
import { BUTTON_HIT_SLOP } from './utils';
import { themes } from '../../constants/colors';
import MessageContext from './Context';
const MessageError = React.memo(({ hasError, onErrorPress, theme }) => {
const MessageError = React.memo(({ hasError, theme }) => {
if (!hasError) {
return null;
}
const { onErrorPress } = useContext(MessageContext);
return (
<Touchable onPress={onErrorPress} style={styles.errorButton} hitSlop={BUTTON_HIT_SLOP}>
<CustomIcon name='warning' color={themes[theme].dangerColor} size={18} />
@ -20,7 +22,6 @@ const MessageError = React.memo(({ hasError, onErrorPress, theme }) => {
MessageError.propTypes = {
hasError: PropTypes.bool,
onErrorPress: PropTypes.func,
theme: PropTypes.string
};
MessageError.displayName = 'MessageError';

View File

@ -1,16 +1,19 @@
import React from 'react';
import React, { useContext } from 'react';
import { View, Text } from 'react-native';
import Touchable from 'react-native-platform-touchable';
import PropTypes from 'prop-types';
import Touchable from './Touchable';
import { CustomIcon } from '../../lib/Icons';
import styles from './styles';
import Emoji from './Emoji';
import { BUTTON_HIT_SLOP } from './utils';
import { themes } from '../../constants/colors';
import { withTheme } from '../../theme';
import MessageContext from './Context';
const AddReaction = React.memo(({ reactionInit, theme }) => (
const AddReaction = React.memo(({ theme }) => {
const { reactionInit } = useContext(MessageContext);
return (
<Touchable
onPress={reactionInit}
key='message-add-reaction'
@ -23,11 +26,15 @@ const AddReaction = React.memo(({ reactionInit, theme }) => (
<CustomIcon name='add-reaction' size={21} color={themes[theme].tintColor} />
</View>
</Touchable>
));
);
});
const Reaction = React.memo(({
reaction, user, onReactionLongPress, onReactionPress, baseUrl, getCustomEmoji, theme
reaction, getCustomEmoji, theme
}) => {
const {
onReactionPress, onReactionLongPress, baseUrl, user
} = useContext(MessageContext);
const reacted = reaction.usernames.findIndex(item => item === user.username) !== -1;
return (
<Touchable
@ -54,7 +61,7 @@ const Reaction = React.memo(({
});
const Reactions = React.memo(({
reactions, user, baseUrl, onReactionPress, reactionInit, onReactionLongPress, getCustomEmoji, theme
reactions, getCustomEmoji, theme
}) => {
if (!Array.isArray(reactions) || reactions.length === 0) {
return null;
@ -65,25 +72,17 @@ const Reactions = React.memo(({
<Reaction
key={reaction.emoji}
reaction={reaction}
user={user}
baseUrl={baseUrl}
onReactionLongPress={onReactionLongPress}
onReactionPress={onReactionPress}
getCustomEmoji={getCustomEmoji}
theme={theme}
/>
))}
<AddReaction reactionInit={reactionInit} theme={theme} />
<AddReaction theme={theme} />
</View>
);
});
Reaction.propTypes = {
reaction: PropTypes.object,
user: PropTypes.object,
baseUrl: PropTypes.string,
onReactionPress: PropTypes.func,
onReactionLongPress: PropTypes.func,
getCustomEmoji: PropTypes.func,
theme: PropTypes.string
};
@ -91,18 +90,12 @@ Reaction.displayName = 'MessageReaction';
Reactions.propTypes = {
reactions: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
user: PropTypes.object,
baseUrl: PropTypes.string,
onReactionPress: PropTypes.func,
reactionInit: PropTypes.func,
onReactionLongPress: PropTypes.func,
getCustomEmoji: PropTypes.func,
theme: PropTypes.string
};
Reactions.displayName = 'MessageReactions';
AddReaction.propTypes = {
reactionInit: PropTypes.func,
theme: PropTypes.string
};
AddReaction.displayName = 'MessageAddReaction';

View File

@ -1,15 +1,16 @@
import React from 'react';
import React, { useContext } from 'react';
import { View, Text, StyleSheet } from 'react-native';
import PropTypes from 'prop-types';
import moment from 'moment';
import Touchable from 'react-native-platform-touchable';
import isEqual from 'deep-equal';
import Touchable from './Touchable';
import Markdown from '../markdown';
import openLink from '../../utils/openLink';
import sharedStyles from '../../views/Styles';
import { themes } from '../../constants/colors';
import { withSplit } from '../../split';
import MessageContext from './Context';
const styles = StyleSheet.create({
button: {
@ -79,12 +80,13 @@ const Title = React.memo(({ attachment, timeFormat, theme }) => {
});
const Description = React.memo(({
attachment, baseUrl, user, getCustomEmoji, theme
attachment, getCustomEmoji, theme
}) => {
const text = attachment.text || attachment.title;
if (!text) {
return null;
}
const { baseUrl, user } = useContext(MessageContext);
return (
<Markdown
msg={text}
@ -124,11 +126,12 @@ const Fields = React.memo(({ attachment, theme }) => {
}, (prevProps, nextProps) => isEqual(prevProps.attachment.fields, nextProps.attachment.fields) && prevProps.theme === nextProps.theme);
const Reply = React.memo(({
attachment, timeFormat, baseUrl, user, index, getCustomEmoji, split, theme
attachment, timeFormat, index, getCustomEmoji, split, theme
}) => {
if (!attachment) {
return null;
}
const { baseUrl, user } = useContext(MessageContext);
const onPress = () => {
let url = attachment.title_link || attachment.author_link;
@ -136,7 +139,10 @@ const Reply = React.memo(({
return;
}
if (attachment.type === 'file') {
url = `${ baseUrl }${ url }?rc_uid=${ user.id }&rc_token=${ user.token }`;
if (!url.startsWith('http')) {
url = `${ baseUrl }${ url }`;
}
url = `${ url }?rc_uid=${ user.id }&rc_token=${ user.token }`;
}
openLink(url, theme);
};
@ -160,8 +166,6 @@ const Reply = React.memo(({
<Description
attachment={attachment}
timeFormat={timeFormat}
baseUrl={baseUrl}
user={user}
getCustomEmoji={getCustomEmoji}
theme={theme}
/>
@ -174,8 +178,6 @@ const Reply = React.memo(({
Reply.propTypes = {
attachment: PropTypes.object,
timeFormat: PropTypes.string,
baseUrl: PropTypes.string,
user: PropTypes.object,
index: PropTypes.number,
theme: PropTypes.string,
getCustomEmoji: PropTypes.func,
@ -192,8 +194,6 @@ Title.displayName = 'MessageReplyTitle';
Description.propTypes = {
attachment: PropTypes.object,
baseUrl: PropTypes.string,
user: PropTypes.object,
getCustomEmoji: PropTypes.func,
theme: PropTypes.string
};

View File

@ -0,0 +1,25 @@
import React, { useContext } from 'react';
import Touchable from 'react-native-platform-touchable';
import PropTypes from 'prop-types';
import MessageContext from './Context';
const RCTouchable = React.memo(({ children, ...props }) => {
const { onLongPress } = useContext(MessageContext);
return (
<Touchable
onLongPress={onLongPress}
{...props}
>
{children}
</Touchable>
);
});
RCTouchable.propTypes = {
children: PropTypes.node
};
RCTouchable.Ripple = (...args) => Touchable.Ripple(...args);
RCTouchable.SelectableBackgroundBorderless = () => Touchable.SelectableBackgroundBorderless();
export default RCTouchable;

View File

@ -1,12 +1,12 @@
import React from 'react';
import React, { useContext } from 'react';
import {
View, Text, StyleSheet, Clipboard
} from 'react-native';
import PropTypes from 'prop-types';
import FastImage from 'react-native-fast-image';
import Touchable from 'react-native-platform-touchable';
import isEqual from 'lodash/isEqual';
import Touchable from './Touchable';
import openLink from '../../utils/openLink';
import sharedStyles from '../../views/Styles';
import { themes } from '../../constants/colors';
@ -15,6 +15,7 @@ import { withSplit } from '../../split';
import { LISTENER } from '../Toast';
import EventEmitter from '../../utils/events';
import I18n from '../../i18n';
import MessageContext from './Context';
const styles = StyleSheet.create({
button: {
@ -52,10 +53,11 @@ const styles = StyleSheet.create({
}
});
const UrlImage = React.memo(({ image, user, baseUrl }) => {
const UrlImage = React.memo(({ image }) => {
if (!image) {
return null;
}
const { baseUrl, user } = useContext(MessageContext);
image = image.includes('http') ? image : `${ baseUrl }/${ image }?rc_uid=${ user.id }&rc_token=${ user.token }`;
return <FastImage source={{ uri: image }} style={styles.image} resizeMode={FastImage.resizeMode.cover} />;
}, (prevProps, nextProps) => prevProps.image === nextProps.image);
@ -79,7 +81,7 @@ const UrlContent = React.memo(({ title, description, theme }) => (
});
const Url = React.memo(({
url, index, user, baseUrl, split, theme
url, index, split, theme
}) => {
if (!url) {
return null;
@ -109,7 +111,7 @@ const Url = React.memo(({
background={Touchable.Ripple(themes[theme].bannerBackground)}
>
<>
<UrlImage image={url.image} user={user} baseUrl={baseUrl} />
<UrlImage image={url.image} />
<UrlContent title={url.title} description={url.description} theme={theme} />
</>
</Touchable>
@ -117,21 +119,19 @@ const Url = React.memo(({
}, (oldProps, newProps) => isEqual(oldProps.url, newProps.url) && oldProps.split === newProps.split && oldProps.theme === newProps.theme);
const Urls = React.memo(({
urls, user, baseUrl, split, theme
urls, split, theme
}) => {
if (!urls || urls.length === 0) {
return null;
}
return urls.map((url, index) => (
<Url url={url} key={url.url} index={index} user={user} baseUrl={baseUrl} split={split} theme={theme} />
<Url url={url} key={url.url} index={index} split={split} theme={theme} />
));
}, (oldProps, newProps) => isEqual(oldProps.urls, newProps.urls) && oldProps.split === newProps.split && oldProps.theme === newProps.theme);
UrlImage.propTypes = {
image: PropTypes.string,
user: PropTypes.object,
baseUrl: PropTypes.string
image: PropTypes.string
};
UrlImage.displayName = 'MessageUrlImage';
@ -145,8 +145,6 @@ UrlContent.displayName = 'MessageUrlContent';
Url.propTypes = {
url: PropTypes.object.isRequired,
index: PropTypes.number,
user: PropTypes.object,
baseUrl: PropTypes.string,
theme: PropTypes.string,
split: PropTypes.bool
};
@ -154,8 +152,6 @@ Url.displayName = 'MessageUrl';
Urls.propTypes = {
urls: PropTypes.array,
user: PropTypes.object,
baseUrl: PropTypes.string,
theme: PropTypes.string,
split: PropTypes.bool
};

View File

@ -1,4 +1,4 @@
import React from 'react';
import React, { useContext } from 'react';
import PropTypes from 'prop-types';
import {
View, Text, StyleSheet, TouchableOpacity
@ -11,6 +11,7 @@ import { withTheme } from '../../theme';
import MessageError from './MessageError';
import sharedStyles from '../../views/Styles';
import messageStyles from './styles';
import MessageContext from './Context';
const styles = StyleSheet.create({
container: {
@ -35,13 +36,14 @@ const styles = StyleSheet.create({
});
const User = React.memo(({
isHeader, useRealName, author, alias, ts, timeFormat, hasError, theme, navToRoomInfo, user, ...props
isHeader, useRealName, author, alias, ts, timeFormat, hasError, theme, navToRoomInfo, ...props
}) => {
if (isHeader || hasError) {
const navParam = {
t: 'd',
rid: author._id
};
const { user } = useContext(MessageContext);
const username = (useRealName && author.name) || author.username;
const aliasUsername = alias ? (<Text style={[styles.alias, { color: themes[theme].auxiliaryText }]}> @{username}</Text>) : null;
const time = moment(ts).format(timeFormat);
@ -49,15 +51,14 @@ const User = React.memo(({
return (
<View style={styles.container}>
<TouchableOpacity
style={styles.titleContainer}
onPress={() => navToRoomInfo(navParam)}
disabled={author._id === user.id}
>
<View style={styles.titleContainer}>
<Text style={[styles.username, { color: themes[theme].titleText }]} numberOfLines={1}>
{alias || username}
{aliasUsername}
</Text>
</View>
</TouchableOpacity>
<Text style={[messageStyles.time, { color: themes[theme].auxiliaryText }]}>{time}</Text>
{ hasError && <MessageError hasError={hasError} theme={theme} {...props} /> }
@ -76,7 +77,6 @@ User.propTypes = {
ts: PropTypes.instanceOf(Date),
timeFormat: PropTypes.string,
theme: PropTypes.string,
user: PropTypes.obj,
navToRoomInfo: PropTypes.func
};
User.displayName = 'MessageUser';

View File

@ -1,9 +1,9 @@
import React from 'react';
import React, { useContext } from 'react';
import PropTypes from 'prop-types';
import { StyleSheet } from 'react-native';
import Touchable from 'react-native-platform-touchable';
import isEqual from 'deep-equal';
import Touchable from './Touchable';
import Markdown from '../markdown';
import openLink from '../../utils/openLink';
import { isIOS, isTablet } from '../../utils/deviceInfo';
@ -11,6 +11,7 @@ import { CustomIcon } from '../../lib/Icons';
import { formatAttachmentUrl } from '../../lib/utils';
import { themes } from '../../constants/colors';
import sharedStyles from '../../views/Styles';
import MessageContext from './Context';
const SUPPORTED_TYPES = ['video/quicktime', 'video/mp4', ...(isIOS ? [] : ['video/3gp', 'video/mkv'])];
const isTypeSupported = type => SUPPORTED_TYPES.indexOf(type) !== -1;
@ -27,12 +28,12 @@ const styles = StyleSheet.create({
});
const Video = React.memo(({
file, baseUrl, user, showAttachment, getCustomEmoji, theme
file, showAttachment, getCustomEmoji, theme
}) => {
const { baseUrl, user } = useContext(MessageContext);
if (!baseUrl) {
return null;
}
const onPress = () => {
if (isTypeSupported(file.video_type)) {
return showAttachment(file);
@ -61,8 +62,6 @@ const Video = React.memo(({
Video.propTypes = {
file: PropTypes.object,
baseUrl: PropTypes.string,
user: PropTypes.object,
showAttachment: PropTypes.func,
getCustomEmoji: PropTypes.func,
theme: PropTypes.string

View File

@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import { KeyboardUtils } from 'react-native-keyboard-input';
import Message from './Message';
import MessageContext from './Context';
import debounce from '../../utils/debounce';
import { SYSTEM_MESSAGES, getMessageTranslation } from './utils';
import messagesStatus from '../../constants/messagesStatus';
@ -229,7 +230,7 @@ class MessageContainer extends React.Component {
item, user, style, archived, baseUrl, useRealName, broadcast, fetchThreadName, customThreadTimeFormat, showAttachment, timeFormat, isReadReceiptEnabled, autoTranslateRoom, autoTranslateLanguage, navToRoomInfo, getCustomEmoji, isThreadRoom, callJitsi, blockAction, rid, theme
} = this.props;
const {
id, msg, ts, attachments, urls, reactions, t, avatar, u, alias, editedBy, role, drid, dcount, dlm, tmid, tcount, tlm, tmsg, mentions, channels, unread, blocks, autoTranslate: autoTranslateMessage
id, msg, ts, attachments, urls, reactions, t, avatar, emoji, u, alias, editedBy, role, drid, dcount, dlm, tmid, tcount, tlm, tmsg, mentions, channels, unread, blocks, autoTranslate: autoTranslateMessage
} = item;
let message = msg;
@ -240,6 +241,20 @@ class MessageContainer extends React.Component {
}
return (
<MessageContext.Provider
value={{
user,
baseUrl,
onPress: this.onPress,
onLongPress: this.onLongPress,
reactionInit: this.reactionInit,
onErrorPress: this.onErrorPress,
replyBroadcast: this.replyBroadcast,
onReactionPress: this.onReactionPress,
onDiscussionPress: this.onDiscussionPress,
onReactionLongPress: this.onReactionLongPress
}}
>
<Message
id={id}
msg={message}
@ -253,13 +268,12 @@ class MessageContainer extends React.Component {
reactions={reactions}
alias={alias}
avatar={avatar}
user={user}
emoji={emoji}
timeFormat={timeFormat}
customThreadTimeFormat={customThreadTimeFormat}
style={style}
archived={archived}
broadcast={broadcast}
baseUrl={baseUrl}
useRealName={useRealName}
isReadReceiptEnabled={isReadReceiptEnabled}
unread={unread}
@ -282,14 +296,6 @@ class MessageContainer extends React.Component {
isInfo={this.isInfo}
isTemp={this.isTemp}
hasError={this.hasError}
onErrorPress={this.onErrorPress}
onPress={this.onPress}
onLongPress={this.onLongPress}
onReactionLongPress={this.onReactionLongPress}
onReactionPress={this.onReactionPress}
replyBroadcast={this.replyBroadcast}
reactionInit={this.reactionInit}
onDiscussionPress={this.onDiscussionPress}
showAttachment={showAttachment}
getCustomEmoji={getCustomEmoji}
navToRoomInfo={navToRoomInfo}
@ -297,6 +303,7 @@ class MessageContainer extends React.Component {
blockAction={blockAction}
theme={theme}
/>
</MessageContext.Provider>
);
}
}

View File

@ -10,6 +10,7 @@ export default {
'error-could-not-change-email': 'E-Mail konnte nicht geändert werden',
'error-could-not-change-name': 'Name konnte nicht geändert werden',
'error-could-not-change-username': 'Benutzername konnte nicht geändert werden',
'error-could-not-change-status': 'Status konnte nicht geändert werden',
'error-delete-protected-role': 'Eine geschützte Rolle kann nicht gelöscht werden',
'error-department-not-found': 'Abteilung nicht gefunden',
'error-direct-message-file-upload-not-allowed': 'Dateifreigabe in direkten Nachrichten nicht zulässig',
@ -17,6 +18,7 @@ export default {
'error-email-domain-blacklisted': 'Die E-Mail-Domain wird auf die schwarze Liste gesetzt',
'error-email-send-failed': 'Fehler beim Versuch, eine E-Mail zu senden: {{message}}',
'error-save-image': 'Fehler beim Speichern des Bildes',
'error-save-video': 'Fehler beim Speichern des Videos',
'error-field-unavailable': '{{field}} wird bereits verwendet :(',
'error-file-too-large': 'Datei ist zu groß',
'error-importer-not-defined': 'Der Import wurde nicht korrekt definiert, es fehlt die Importklasse.',
@ -81,12 +83,14 @@ export default {
Activity: 'Aktivität',
Add_Reaction: 'Reaktion hinzufügen',
Add_Server: 'Server hinzufügen',
Add_users: 'Nutzer hinzufügen',
Add_users: 'Benutzer hinzufügen',
Admin_Panel: 'Admin-Panel',
Agent: 'Agent',
Alert: 'Benachrichtigung',
alert: 'Benachrichtigung',
alerts: 'Benachrichtigungen',
All_users_in_the_channel_can_write_new_messages: 'Alle Benutzer im Kanal können neue Nachrichten schreiben',
A_meaningful_name_for_the_discussion_room: 'Ein aussagekräftiger Name für den Diskussionsraum',
All: 'Alles',
All_Messages: 'Alle Nachrichten',
Allow_Reactions: 'Reaktionen zulassen',
@ -130,12 +134,15 @@ export default {
Click_to_join: 'Klicken um teilzunehmen!',
Close: 'Schließen',
Close_emoji_selector: 'Schließen Sie die Emoji-Auswahl',
Closing_chat: 'Chat schließen',
Change_language_loading: 'Ändere Sprache.',
Chat_closed_by_agent: 'Chat durch den Agenten geschlossen',
Choose: 'Wählen',
Choose_from_library: 'Aus der Bibliothek auswählen',
Choose_file: 'Datei auswählen',
Choose_where_you_want_links_be_opened: 'Entscheide, wie Links geöffnet werden sollen',
Code: 'Code',
Code_or_password_invalid: 'Code oder Passwort sind falsch',
Collaborative: 'Kollaborativ',
Confirm: 'Bestätigen',
Connect: 'Verbinden',
@ -147,6 +154,7 @@ export default {
Continue_with: 'Weitermachen mit',
Copied_to_clipboard: 'In die Zwischenablage kopiert!',
Copy: 'Kopieren',
Conversation: 'Konversationen',
Permalink: 'Permalink',
Certificate_password: 'Zertifikats-Passwort',
Clear_cache: 'Lokalen Server-Cache leeren',
@ -154,14 +162,18 @@ export default {
Whats_the_password_for_your_certificate: 'Wie lautet das Passwort für Ihr Zertifikat?',
Create_account: 'Ein Konto erstellen',
Create_Channel: 'Kanal erstellen',
Create_Direct_Messages: 'Direkt-Nachricht erstellen',
Create_Discussion: 'Diskussion erstellen',
Created_snippet: 'Erstellt ein Snippet',
Create_a_new_workspace: 'Erstellen Sie einen neuen Arbeitsbereich',
Create: 'Erstellen',
Custom_Status: 'eigener Status',
Dark: 'Dunkel',
Dark_level: 'Dunkelstufe',
Default: 'Standard',
Default_browser: 'Standard-Browser',
Delete_Room_Warning: 'Durch das Löschen eines Raums werden alle Nachrichten gelöscht, die im Raum gepostet wurden. Das kann nicht rückgängig gemacht werden.',
Department: 'Abteilung',
delete: 'löschen',
Delete: 'Löschen',
DELETE: 'LÖSCHEN',
@ -173,17 +185,23 @@ export default {
Direct_Messages: 'Direkte Nachrichten',
Disable_notifications: 'Benachrichtigungen deaktiveren',
Discussions: 'Diskussionen',
Dont_Have_An_Account: 'Sie haben noch kein Konto?',
Discussion_Desc: 'Hilft dir die Übersicht zu behalten! Durch das Erstellen einer Diskussion wird ein Unter-Kanal im ausgewählten Raum erzeugt und beide verknüpft.',
Discussion_name: 'Diskussions-Name',
Done: 'Erledigt',
Dont_Have_An_Account: 'Du hast noch kein Konto?',
Do_you_have_an_account: 'Du hast schon ein Konto?',
Do_you_have_a_certificate: 'Haben Sie ein Zertifikat?',
Do_you_really_want_to_key_this_room_question_mark: 'Möchten Sie diesen Raum wirklich {{key}}?',
edit: 'bearbeiten',
edited: 'bearbeitet',
Edit: 'Bearbeiten',
Edit_Status: 'Status ändern',
Edit_Invite: 'Einladung bearbeiten',
Email_or_password_field_is_empty: 'Das E-Mail- oder Passwortfeld ist leer',
Email: 'Email',
EMAIL: 'EMAIL',
email: 'Email',
Empty_title: 'leerer Titel',
Enable_Auto_Translate: 'Automatische Übersetzung aktivieren',
Enable_notifications: 'Benachrichtigungen aktivieren',
Everyone_can_access_this_channel: 'Jeder kann auf diesen Kanal zugreifen',
@ -200,6 +218,10 @@ export default {
Forgot_password_If_this_email_is_registered: 'Wenn diese E-Mail registriert ist, senden wir Anweisungen zum Zurücksetzen Ihres Passworts. Wenn Sie in Kürze keine E-Mail erhalten, kommen Sie bitte zurück und versuchen Sie es erneut.',
Forgot_password: 'Passwort vergessen',
Forgot_Password: 'Passwort vergessen',
Forward: 'Weiterleiten',
Forward_Chat: 'Chat weiterleiten',
Forward_to_department: 'Weiterleiten an Abteilung',
Forward_to_user: 'Weiterleiten an Benutzer',
Full_table: 'Klicken um die ganze Tabelle anzuzeigen',
Generate_New_Link: 'Neuen Link erstellen',
Group_by_favorites: 'Nach Favoriten gruppieren',
@ -210,19 +232,20 @@ export default {
Has_left_the_channel: 'Hat den Kanal verlassen',
Hide_System_Messages: 'Systemnachrichten verstecken',
Hide_type_messages: 'Verstecke "{{type}}"-Nachrichten',
Message_HideType_uj: 'Nutzer beigetreten',
Message_HideType_ul: 'Nutzer verlassen',
Message_HideType_ru: 'Nutzer entfernt',
Message_HideType_au: 'Nutzer hinzugefügt',
Message_HideType_mute_unmute: 'Nutzer stummgeschaltet / freigegeben',
Message_HideType_uj: 'Benutzer beigetreten',
Message_HideType_ul: 'Benutzer verlassen',
Message_HideType_ru: 'Benutzer entfernt',
Message_HideType_au: 'Benutzer hinzugefügt',
Message_HideType_mute_unmute: 'Benutzer stummgeschaltet / freigegeben',
Message_HideType_r: 'Raumname geändert',
Message_HideType_ut: 'Nutzer ist der Unterhaltung beigetreten',
Message_HideType_ut: 'Benutzer ist der Unterhaltung beigetreten',
Message_HideType_wm: 'Willkommen',
Message_HideType_rm: 'Nachricht entfernt',
Message_HideType_subscription_role_added: 'Rolle wurde gesetzt',
Message_HideType_subscription_role_removed: 'Rolle nicht länger definiert',
Message_HideType_room_archived: 'Raum archiviert',
Message_HideType_room_unarchived: 'Raum nicht mehr archiviert',
IP: 'IP',
In_app: 'In-App-Browser',
IN_APP_AND_DESKTOP: 'IN-APP UND DESKTOP',
In_App_and_Desktop_Alert_info: 'Zeigt ein Banner oben am Bildschirm, wenn die App geöffnet ist und eine Benachrichtigung auf dem Desktop.',
@ -230,12 +253,14 @@ export default {
Invite: 'Einladen',
is_a_valid_RocketChat_instance: 'ist eine gültige Rocket.Chat-Instanz',
is_not_a_valid_RocketChat_instance: 'ist keine gültige Rocket.Chat-Instanz',
is_typing: 'tippt',
is_typing: 'schreibt',
Invalid_or_expired_invite_token: 'Ungültiger oder abgelaufener Einladungscode',
Invalid_server_version: 'Der Server, zu dem Sie eine Verbindung herstellen möchten, verwendet eine Version, die von der App nicht mehr unterstützt wird: {{currentVersion}}.\n\nWir benötigen Version {{MinVersion}}.',
Invite_Link: 'Einladungs-Link',
Invite_users: 'Benutzer einladen',
Join: 'Beitreten',
Join_our_open_workspace: 'Tritt unserem offenen Arbeitsbereich bei',
Join_your_workspace: 'Tritt deinem Arbeitsbereich bei',
Just_invited_people_can_access_this_channel: 'Nur eingeladene Personen können auf diesen Kanal zugreifen',
Language: 'Sprache',
last_message: 'letzte Nachricht',
@ -246,12 +271,14 @@ export default {
Light: 'Hell',
License: 'Lizenz',
Livechat: 'Live-Chat',
Livechat_edit: 'Livechat bearbeiten',
Login: 'Anmeldung',
Login_error: 'Ihre Zugangsdaten wurden abgelehnt! Bitte versuchen Sie es erneut.',
Login_with: 'Einloggen mit',
Logging_out: 'Abmelden.',
Logout: 'Abmelden',
Max_number_of_uses: 'Maximale Anzahl der Benutzungen',
Max_number_of_users_allowed_is_number: 'Maximale Anzahl von erlaubten Benutzern ist {{maxUsers}}',
members: 'Mitglieder',
Members: 'Mitglieder',
Mentioned_Messages: 'Erwähnte Nachrichten',
@ -277,6 +304,7 @@ export default {
N_users: '{{n}} Benutzer',
name: 'Name',
Name: 'Name',
Navigation_history: 'Navigations-Verlauf',
Never: 'Niemals',
New_Message: 'Neue Nachricht',
New_Password: 'Neues Kennwort',
@ -303,20 +331,34 @@ export default {
Notifications: 'Benachrichtigungen',
Notification_Duration: 'Benachrichtigungsdauer',
Notification_Preferences: 'Benachrichtigungseinstellungen',
No_available_agents_to_transfer: 'Keine Agenten für den Transfer verfügbar',
Offline: 'Offline',
Oops: 'Hoppla!',
Onboarding_description: 'Ein Arbeitsbereich ist der Ort für die Zusammenarbeit deines Teams oder Organisation. Bitte den Admin des Arbeitsbereichs um eine Adresse, um ihm beizutreten, oder erstelle einen Arbeitsbereich für dein Team.',
Onboarding_join_workspace: 'Tritt einem Arbeitsbereich bei',
Onboarding_subtitle: 'Mehr als Team-Zusammenarbeit',
Onboarding_title: 'Willkommen bei Rocket.Chat',
Onboarding_join_open_description: 'Tritt unserem Arbeitsbereich bei um mit dem Rocket.Chat-Team oder der Gemeinschaft zu chatten.',
Onboarding_agree_terms: 'Durch fortfahren stimmst du Rocket.Chats Bedingungen zu',
Onboarding_less_options: 'Weniger Optionen',
Onboarding_more_options: 'Mehr Optionen',
Online: 'Online',
Only_authorized_users_can_write_new_messages: 'Nur autorisierte Benutzer können neue Nachrichten schreiben',
Open_emoji_selector: 'Öffne die Emoji-Auswahl',
Open_Source_Communication: 'Open-Source-Kommunikation',
Open_your_authentication_app_and_enter_the_code: 'Öffne deine Authentifizierungsanwendung und gib den Code ein.',
OR: 'ODER',
OS: 'OS',
Overwrites_the_server_configuration_and_use_room_config: 'Übergeht die Servereinstellungen und nutzt Einstellung für den Raum',
Password: 'Passwort',
Parent_channel_or_group: 'Übergeordneter Kanal oder Gruppe',
Permalink_copied_to_clipboard: 'Permalink in die Zwischenablage kopiert!',
Phone: 'Telefon',
Pin: 'Anheften',
Pinned_Messages: 'Angeheftete Nachrichten',
pinned: 'angeheftet',
Pinned: 'Angeheftet',
Please_add_a_comment: 'Bitte Kommentar hinzufügen',
Please_enter_your_password: 'Bitte geben Sie Ihr Passwort ein',
Please_wait: 'Bitte warten.',
Preferences: 'Einstellungen',
@ -355,6 +397,7 @@ export default {
Reset_password: 'Passwort zurücksetzen',
resetting_password: 'Passwort zurücksetzen',
RESET: 'ZURÜCKSETZEN',
Return: 'Zurück',
Review_app_title: 'Gefällt dir diese App?',
Review_app_desc: 'Gib uns 5 Sterne im {{store}}',
Review_app_yes: 'Sicher!',
@ -375,7 +418,8 @@ export default {
Room_name_changed: 'Raumname geändert in {{name}} von {{userBy}}',
SAVE: 'SPEICHERN',
Save_Changes: 'Änderungen speichern',
Save: 'sparen',
Save: 'speichern',
Saved: 'gespeichert',
saving_preferences: 'Präferenzen speichern',
saving_profile: 'Profil speichern',
saving_settings: 'Einstellungen speichern',
@ -388,17 +432,25 @@ export default {
Seconds: '{{second}} Sekunden',
Select_Avatar: 'Wählen Sie einen Avatar aus',
Select_Server: 'Server auswählen',
Select_Users: 'Wählen Sie einen Benutzer aus',
Select_Users: 'Benutzer auswählen',
Select_a_Channel: 'Kanal auswählen',
Select_a_Department: 'Abteilung auswählen',
Select_an_option: 'Option auswählen',
Select_a_User: 'Benutzer auswählen',
Send: 'Senden',
Send_audio_message: 'Audio-Nachricht senden',
Send_crash_report: 'Absturzbericht senden',
Send_message: 'Nachricht senden',
Send_me_the_code_again: 'Den Code neu versenden',
Send_to: 'Senden an …',
Sent_an_attachment: 'Sende einen Anhang',
Server: 'Server',
Servers: 'Server',
Server_version: 'Server version: {{version}}',
Set_username_subtitle: 'Der Benutzername wird verwendet, damit andere Personen Sie in Nachrichten erwähnen können',
Set_custom_status: 'Individuellen Status setzen',
Set_status: 'Status setzen',
Status_saved_successfully: 'Status erfolgreich gesetzt!',
Settings: 'Einstellungen',
Settings_succesfully_changed: 'Einstellungen erfolgreich geändert!',
Share: 'Teilen',
@ -407,7 +459,7 @@ export default {
Show_more: 'Mehr anzeigen …',
Show_Unread_Counter: 'Zähler anzeigen',
Show_Unread_Counter_Info: 'Anzahl der ungelesenen Nachrichten anzeigen',
Sign_in_your_server: 'Melden Sie sich bei Ihrem Server an',
Sign_in_your_server: 'Melde dich bei deinem Server an',
Sign_Up: 'Anmelden',
Some_field_is_invalid_or_empty: 'Ein Feld ist ungültig oder leer',
Sorting_by: 'Sortierung nach {{key}}',
@ -422,6 +474,7 @@ export default {
Started_call: 'Anruf gestartet von {{userBy}}',
Submit: 'einreichen',
Table: 'Tabelle',
Tags: 'Tags',
Take_a_photo: 'Foto aufnehmen',
Take_a_video: 'Video aufnehmen',
tap_to_change_status: 'Tippen um den Status zu ändern',
@ -441,10 +494,10 @@ export default {
Translate: 'Übersetzen',
Try_again: 'Versuchen Sie es nochmal',
Two_Factor_Authentication: 'Zwei-Faktor-Authentifizierung',
Type_the_channel_name_here: 'Geben Sie hier den Kanalnamen ein',
Type_the_channel_name_here: 'Gib hier den Kanalnamen ein',
unarchive: 'wiederherstellen',
UNARCHIVE: 'WIEDERHERSTELLEN',
Unblock_user: 'Nutzer entsperren',
Unblock_user: 'Benutzer entsperren',
Unfavorite: 'Nicht mehr favorisieren',
Unfollowed_thread: 'Thread nicht mehr folgen',
Unmute: 'Stummschaltung aufheben',
@ -457,9 +510,10 @@ export default {
Updating: 'Aktualisierung …',
Uploading: 'Hochladen',
Upload_file_question_mark: 'Datei hochladen?',
User: 'Benutzer',
Users: 'Benutzer',
User_added_by: 'Benutzer {{userAdded}} hinzugefügt von {{userBy}}',
User_Info: 'Nutzerinfo',
User_Info: 'Benutzerinfo',
User_has_been_key: 'Benutzer wurde {{key}}!',
User_is_no_longer_role_by_: '{{user}} ist nicht länger {{role}} von {{userBy}}',
User_muted_by: 'Benutzer {{userMuted}} von {{userBy}} stummgeschaltet',
@ -471,18 +525,26 @@ export default {
Username: 'Benutzername',
Username_or_email: 'Benutzername oder E-Mail-Adresse',
Uses_server_configuration: 'Nutzt Servereinstellungen',
Usually_a_discussion_starts_with_a_question_like_How_do_I_upload_a_picture: 'Üblicherweise beginnt eine Diskussion mit einer Frage, beispielsweise: "Wie lade ich ein Bild hoch?"',
Validating: 'Validierung',
Registration_Succeeded: 'Registrierung erfolgreich!',
Verify: 'Überprüfen',
Verify_email_title: 'Registrierung erfolgreich!',
Verify_email_desc: 'Wir haben dir eine Email geschickt um deine Anmeldung zu bestätigen. Wenn du keine Email erhältst, komme bitte wieder und versuche es noch einmal.',
Verify_your_email_for_the_code_we_sent: 'Prüfe deine Mails für den Code, den wir dir eben geschickt haben.',
Video_call: 'Videoanruf',
View_Original: 'Original anzeigen',
Voice_call: 'Sprachanruf',
Websocket_disabled: 'Websockets sind auf diesem Server nicht aktiviert.\n{{contact}}',
Welcome: 'Herzlich willkommen',
What_are_you_doing_right_now: 'Was machst du gerade?',
Whats_your_2fa: 'Wie lautet Ihr 2FA-Code?',
Without_Servers: 'Ohne Server',
Workspaces: 'Arbeitsbereiche',
Would_you_like_to_return_the_inquiry: 'Willst du zur Anfrage zurück?',
Write_External_Permission_Message: 'Rocket.Chat benötigt Zugriff auf Ihre Galerie um Bilder speichern zu können.',
Write_External_Permission: 'Galerie-Zugriff',
Yes: 'Ja',
Yes_action_it: 'Ja, {{action}}!',
Yesterday: 'Gestern',
You_are_in_preview_mode: 'Sie befinden sich im Vorschaumodus',
@ -495,11 +557,13 @@ export default {
You: 'Sie',
Logged_out_by_server: 'Du bist vom Server abgemeldet worden. Bitte melde dich wieder an.',
You_need_to_access_at_least_one_RocketChat_server_to_share_something: 'Sie benötigen Zugang zu mindestens einem Rocket.Chat-Server um etwas zu teilen.',
Your_certificate: 'Ihr Zertifikat',
Your_certificate: 'Dein Zertifikat',
Your_message: 'Deine Nachricht',
Your_invite_link_will_expire_after__usesLeft__uses: 'Dein Einladungs-Link wird nach {{usesLeft}} Benutzungen ablaufen.',
Your_invite_link_will_expire_on__date__or_after__usesLeft__uses: 'Dein Einladungs-Link wird am {{date}} oder nach {{usesLeft}} Benutzungen ablaufen.',
Your_invite_link_will_expire_on__date__: 'Dein Einladungs-Link wird am {{date}} ablaufen.',
Your_invite_link_will_never_expire: 'Dein Einladungs-Link wird niemals ablaufen.',
Your_workspace: 'Dein Arbeitsbereich',
Version_no: 'Version: {{version}}',
You_will_not_be_able_to_recover_this_message: 'Sie können diese Nachricht nicht wiederherstellen!',
Change_Language: 'Sprache ändern',
@ -521,5 +585,30 @@ export default {
You_will_be_logged_out_of_this_application: 'Du wirst in dieser Anwendung vom Server abgemeldet.',
Clear: 'Löschen',
This_will_clear_all_your_offline_data: 'Dies wird deine Offline-Daten löschen.',
Mark_unread: 'Als ungelesen markieren'
This_will_remove_all_data_from_this_server: 'Dies wird alle Daten von diesem Server löschen.',
Mark_unread: 'Als ungelesen markieren',
Wait_activation_warning: 'Bevor du dich anmelden kannst, muss dein Konto durch einen Administrator freigeschaltet werden.',
Screen_lock: 'Zugriffs-Sperre',
Local_authentication_biometry_title: 'Authentifizieren',
Local_authentication_biometry_fallback: 'Sicherheitscode benutzen',
Local_authentication_unlock_option: 'Entsperren mit Sicherheitscode',
Local_authentication_change_passcode: 'Ändere Sicherheitscode',
Local_authentication_info: 'Anmerkung: Wenn du den Sicherheitscode vergisst, musst du diese App löschen und neu installieren.',
Local_authentication_facial_recognition: 'Gesichtserkennung',
Local_authentication_fingerprint: 'Fingerabdruck',
Local_authentication_unlock_with_label: 'Entsperren mit {{label}}',
Local_authentication_auto_lock_60: 'Nach 1 Minute',
Local_authentication_auto_lock_300: 'Nach 5 Minuten',
Local_authentication_auto_lock_900: 'Nach 15 Minuten',
Local_authentication_auto_lock_1800: 'Nach 30 Minuten',
Local_authentication_auto_lock_3600: 'Nach 1 Stunde',
Passcode_enter_title: 'Gib deinen Sicherheitscode ein',
Passcode_choose_title: 'Setze deinen neuen Sicherheitscode',
Passcode_choose_confirm_title: 'Bestätige deinen neuen Sicherheitscode',
Passcode_choose_error: 'Sicherheitscodes stimmen nicht überein. Probiere es noch einmal.',
Passcode_choose_force_set: 'Sicherheitscode wird vom Admin verlangt',
Passcode_app_locked_title: 'App gesperrt',
Passcode_app_locked_subtitle: 'Versuche es in {{timeLeft}} Sekunden noch einmal.',
After_seconds_set_by_admin: 'Nach {{seconds}} Sekunden (durch den Admin gesetzt)',
Dont_activate: 'Jetzt nicht aktivieren'
};

View File

@ -18,6 +18,7 @@ export default {
'error-email-domain-blacklisted': 'The email domain is blacklisted',
'error-email-send-failed': 'Error trying to send email: {{message}}',
'error-save-image': 'Error while saving image',
'error-save-video': 'Error while saving video',
'error-field-unavailable': '{{field}} is already in use :(',
'error-file-too-large': 'File is too large',
'error-importer-not-defined': 'The importer was not defined correctly, it is missing the Import class.',
@ -84,6 +85,7 @@ export default {
Add_Server: 'Add Server',
Add_users: 'Add users',
Admin_Panel: 'Admin Panel',
Agent: 'Agent',
Alert: 'Alert',
alert: 'alert',
alerts: 'alerts',
@ -132,7 +134,9 @@ export default {
Click_to_join: 'Click to Join!',
Close: 'Close',
Close_emoji_selector: 'Close emoji selector',
Closing_chat: 'Closing chat',
Change_language_loading: 'Changing language.',
Chat_closed_by_agent: 'Chat closed by agent',
Choose: 'Choose',
Choose_from_library: 'Choose from library',
Choose_file: 'Choose file',
@ -150,6 +154,7 @@ export default {
Continue_with: 'Continue with',
Copied_to_clipboard: 'Copied to clipboard!',
Copy: 'Copy',
Conversation: 'Conversation',
Permalink: 'Permalink',
Certificate_password: 'Certificate Password',
Clear_cache: 'Clear local server cache',
@ -168,6 +173,7 @@ export default {
Default: 'Default',
Default_browser: 'Default browser',
Delete_Room_Warning: 'Deleting a room will delete all messages posted within the room. This cannot be undone.',
Department: 'Department',
delete: 'delete',
Delete: 'Delete',
DELETE: 'DELETE',
@ -195,6 +201,7 @@ export default {
Email: 'Email',
EMAIL: 'EMAIL',
email: 'e-mail',
Empty_title: 'Empty title',
Enable_Auto_Translate: 'Enable Auto-Translate',
Enable_notifications: 'Enable notifications',
Everyone_can_access_this_channel: 'Everyone can access this channel',
@ -211,6 +218,10 @@ export default {
Forgot_password_If_this_email_is_registered: 'If this email is registered, we\'ll send instructions on how to reset your password. If you do not receive an email shortly, please come back and try again.',
Forgot_password: 'Forgot your password?',
Forgot_Password: 'Forgot Password',
Forward: 'Forward',
Forward_Chat: 'Forward Chat',
Forward_to_department: 'Forward to department',
Forward_to_user: 'Forward to user',
Full_table: 'Click to see full table',
Generate_New_Link: 'Generate New Link',
Group_by_favorites: 'Group favorites',
@ -234,6 +245,7 @@ export default {
Message_HideType_subscription_role_removed: 'Role No Longer Defined',
Message_HideType_room_archived: 'Room Archived',
Message_HideType_room_unarchived: 'Room Unarchived',
IP: 'IP',
In_app: 'In-app',
IN_APP_AND_DESKTOP: 'IN-APP AND DESKTOP',
In_App_and_Desktop_Alert_info: 'Displays a banner at the top of the screen when app is open, and displays a notification on desktop',
@ -259,6 +271,7 @@ export default {
Light: 'Light',
License: 'License',
Livechat: 'Livechat',
Livechat_edit: 'Livechat edit',
Login: 'Login',
Login_error: 'Your credentials were rejected! Please try again.',
Login_with: 'Login with',
@ -291,6 +304,7 @@ export default {
N_users: '{{n}} users',
name: 'name',
Name: 'Name',
Navigation_history: 'Navigation history',
Never: 'Never',
New_Message: 'New Message',
New_Password: 'New Password',
@ -317,6 +331,7 @@ export default {
Notifications: 'Notifications',
Notification_Duration: 'Notification Duration',
Notification_Preferences: 'Notification Preferences',
No_available_agents_to_transfer: 'No available agents to transfer',
Offline: 'Offline',
Oops: 'Oops!',
Onboarding_description: 'A workspace is your team or organizations space to collaborate. Ask the workspace admin for address to join or create one for your team.',
@ -333,14 +348,17 @@ export default {
Open_Source_Communication: 'Open Source Communication',
Open_your_authentication_app_and_enter_the_code: 'Open your authentication app and enter the code.',
OR: 'OR',
OS: 'OS',
Overwrites_the_server_configuration_and_use_room_config: 'Overwrites the server configuration and use room config',
Password: 'Password',
Parent_channel_or_group: 'Parent channel or group',
Permalink_copied_to_clipboard: 'Permalink copied to clipboard!',
Phone: 'Phone',
Pin: 'Pin',
Pinned_Messages: 'Pinned Messages',
pinned: 'pinned',
Pinned: 'Pinned',
Please_add_a_comment: 'Please add a comment',
Please_enter_your_password: 'Please enter your password',
Please_wait: 'Please wait.',
Preferences: 'Preferences',
@ -379,6 +397,7 @@ export default {
Reset_password: 'Reset password',
resetting_password: 'resetting password',
RESET: 'RESET',
Return: 'Return',
Review_app_title: 'Are you enjoying this app?',
Review_app_desc: 'Give us 5 stars on {{store}}',
Review_app_yes: 'Sure!',
@ -400,6 +419,7 @@ export default {
SAVE: 'SAVE',
Save_Changes: 'Save Changes',
Save: 'Save',
Saved: 'Saved',
saving_preferences: 'saving preferences',
saving_profile: 'saving profile',
saving_settings: 'saving settings',
@ -414,7 +434,9 @@ export default {
Select_Server: 'Select Server',
Select_Users: 'Select Users',
Select_a_Channel: 'Select a Channel',
Select_a_Department: 'Select a Department',
Select_an_option: 'Select an option',
Select_a_User: 'Select a User',
Send: 'Send',
Send_audio_message: 'Send audio message',
Send_crash_report: 'Send crash report',
@ -452,6 +474,7 @@ export default {
Started_call: 'Call started by {{userBy}}',
Submit: 'Submit',
Table: 'Table',
Tags: 'Tags',
Take_a_photo: 'Take a photo',
Take_a_video: 'Take a video',
tap_to_change_status: 'tap to change status',
@ -487,6 +510,7 @@ export default {
Updating: 'Updating...',
Uploading: 'Uploading',
Upload_file_question_mark: 'Upload file?',
User: 'User',
Users: 'Users',
User_added_by: 'User {{userAdded}} added by {{userBy}}',
User_Info: 'User Info',
@ -517,8 +541,10 @@ export default {
Whats_your_2fa: 'What\'s your 2FA code?',
Without_Servers: 'Without Servers',
Workspaces: 'Workspaces',
Would_you_like_to_return_the_inquiry: 'Would you like to return the inquiry?',
Write_External_Permission_Message: 'Rocket Chat needs access to your gallery so you can save images.',
Write_External_Permission: 'Gallery Permission',
Yes: 'Yes',
Yes_action_it: 'Yes, {{action}} it!',
Yesterday: 'Yesterday',
You_are_in_preview_mode: 'You are in preview mode',
@ -559,6 +585,30 @@ export default {
You_will_be_logged_out_of_this_application: 'You will be logged out of this application.',
Clear: 'Clear',
This_will_clear_all_your_offline_data: 'This will clear all your offline data.',
This_will_remove_all_data_from_this_server: 'This will remove all data from this server.',
Mark_unread: 'Mark Unread',
Wait_activation_warning: 'Before you can login, your account must be manually activated by an administrator.'
Wait_activation_warning: 'Before you can login, your account must be manually activated by an administrator.',
Screen_lock: 'Screen lock',
Local_authentication_biometry_title: 'Authenticate',
Local_authentication_biometry_fallback: 'Use passcode',
Local_authentication_unlock_option: 'Unlock with Passcode',
Local_authentication_change_passcode: 'Change Passcode',
Local_authentication_info: 'Note: if you forget the Passcode, you\'ll need to delete and reinstall the app.',
Local_authentication_facial_recognition: 'facial recognition',
Local_authentication_fingerprint: 'fingerprint',
Local_authentication_unlock_with_label: 'Unlock with {{label}}',
Local_authentication_auto_lock_60: 'After 1 minute',
Local_authentication_auto_lock_300: 'After 5 minutes',
Local_authentication_auto_lock_900: 'After 15 minutes',
Local_authentication_auto_lock_1800: 'After 30 minutes',
Local_authentication_auto_lock_3600: 'After 1 hour',
Passcode_enter_title: 'Enter your passcode',
Passcode_choose_title: 'Choose your new passcode',
Passcode_choose_confirm_title: 'Confirm your new passcode',
Passcode_choose_error: 'Passcodes don\'t match. Try again.',
Passcode_choose_force_set: 'Passcode required by admin',
Passcode_app_locked_title: 'App locked',
Passcode_app_locked_subtitle: 'Try again in {{timeLeft}} seconds',
After_seconds_set_by_admin: 'After {{seconds}} seconds (set by admin)',
Dont_activate: 'Don\'t activate now'
};

View File

@ -89,6 +89,7 @@ export default {
Add_Reaction: 'Reagir',
Add_Server: 'Adicionar servidor',
Add_users: 'Adicionar usuário',
Agent: 'Agente',
Alert: 'Alerta',
alert: 'alerta',
alerts: 'alertas',
@ -135,7 +136,9 @@ export default {
Click_to_join: 'Clique para participar!',
Close: 'Fechar',
Close_emoji_selector: 'Fechar seletor de emojis',
Closing_chat: 'Fechando conversa',
Choose: 'Escolher',
Chat_closed_by_agent: 'Conversa fechada por agente',
Choose_from_library: 'Escolha da biblioteca',
Choose_file: 'Enviar arquivo',
Choose_where_you_want_links_be_opened: 'Escolha onde deseja que os links sejam abertos',
@ -145,6 +148,7 @@ export default {
Confirm: 'Confirmar',
Connect: 'Conectar',
Connected: 'Conectado',
Conversation: 'Conversação',
connecting_server: 'conectando no servidor',
Connecting: 'Conectando...',
Continue_with: 'Entrar com',
@ -187,6 +191,7 @@ export default {
Email_or_password_field_is_empty: 'Email ou senha estão vazios',
Email: 'Email',
email: 'e-mail',
Empty_title: 'Título vazio',
Enable_notifications: 'Habilitar notificações',
Everyone_can_access_this_channel: 'Todos podem acessar este canal',
Error_uploading: 'Erro subindo',
@ -201,6 +206,10 @@ export default {
Forgot_password_If_this_email_is_registered: 'Se este e-mail estiver cadastrado, enviaremos instruções sobre como redefinir sua senha. Se você não receber um e-mail em breve, volte e tente novamente.',
Forgot_password: 'Esqueceu sua senha?',
Forgot_Password: 'Esqueci minha senha',
Forward: 'Encaminhar',
Forward_Chat: 'Encaminhar Conversa',
Forward_to_department: 'Encaminhar para departamento',
Forward_to_user: 'Encaminhar para usuário',
Full_table: 'Clique para ver a tabela completa',
Generate_New_Link: 'Gerar novo convite',
Group_by_favorites: 'Agrupar favoritos',
@ -223,6 +232,7 @@ export default {
Message_HideType_subscription_role_removed: 'Papel removido',
Message_HideType_room_archived: 'Sala arquivada',
Message_HideType_room_unarchived: 'Sala desarquivada',
IP: 'IP',
In_app: 'No app',
Invisible: 'Invisível',
Invite: 'Convidar',
@ -269,6 +279,7 @@ export default {
N_users: '{{n}} usuários',
name: 'nome',
Name: 'Nome',
Navigation_history: 'Histórico de navegação',
Never: 'Nunca',
New_in_RocketChat_question_mark: 'Novo no Rocket.Chat?',
New_Message: 'Nova Mensagem',
@ -289,6 +300,7 @@ export default {
Notify_active_in_this_room: 'Notificar usuários ativos nesta sala',
Notify_all_in_this_room: 'Notificar todos nesta sala',
Not_RC_Server: 'Este não é um servidor Rocket.Chat.\n{{contact}}',
No_available_agents_to_transfer: 'Nenhum agente disponível para transferência',
Offline: 'Offline',
Oops: 'Ops!',
Onboarding_description: 'Workspace é o espaço de colaboração do seu time ou organização. Peça um convite ou o endereço ao seu administrador ou crie uma workspace para o seu time.',
@ -305,6 +317,7 @@ export default {
Open_Source_Communication: 'Comunicação Open Source',
Open_your_authentication_app_and_enter_the_code: 'Abra seu aplicativo de autenticação e digite o código.',
OR: 'OU',
OS: 'SO',
Overwrites_the_server_configuration_and_use_room_config: 'Substituir a configuração do servidor e usar a configuração da sala',
Password: 'Senha',
Parent_channel_or_group: 'Canal ou grupo pai',
@ -315,6 +328,7 @@ export default {
Pinned: 'Mensagens Fixadas',
Please_wait: 'Por favor, aguarde.',
Please_enter_your_password: 'Por favor, digite sua senha',
Please_add_a_comment: 'Por favor, adicione um comentário',
Preferences: 'Preferências',
Preferences_saved: 'Preferências salvas!',
Privacy_Policy: ' Política de Privacidade',
@ -343,6 +357,7 @@ export default {
Reset_password: 'Resetar senha',
resetting_password: 'redefinindo senha',
RESET: 'RESETAR',
Return: 'Retornar',
Review_app_title: 'Você está gostando do app?',
Review_app_desc: 'Nos dê 5 estrelas na {{store}}',
Review_app_yes: 'Claro!',
@ -377,7 +392,9 @@ export default {
Select_Server: 'Selecionar Servidor',
Select_Users: 'Selecionar Usuários',
Select_a_Channel: 'Selecione um canal',
Select_a_Department: 'Selecione um Departamento',
Select_an_option: 'Selecione uma opção',
Select_a_User: 'Selecione um Usuário',
Send: 'Enviar',
Send_audio_message: 'Enviar mensagem de áudio',
Send_message: 'Enviar mensagem',
@ -436,6 +453,7 @@ export default {
Updating: 'Atualizando...',
Uploading: 'Subindo arquivo',
Upload_file_question_mark: 'Enviar arquivo?',
User: 'Usuário',
Users: 'Usuários',
User_added_by: 'Usuário {{userAdded}} adicionado por {{userBy}}',
User_has_been_key: 'Usuário foi {{key}}!',
@ -479,8 +497,10 @@ export default {
Your_invite_link_will_never_expire: 'Seu link de convite nunca irá vencer.',
Your_workspace: 'Sua workspace',
You_will_not_be_able_to_recover_this_message: 'Você não será capaz de recuperar essa mensagem!',
Would_you_like_to_return_the_inquiry: 'Deseja retornar a consulta?',
Write_External_Permission_Message: 'Rocket Chat precisa de acesso à sua galeria para salvar imagens',
Write_External_Permission: 'Acesso à Galeria',
Yes: 'Sim',
Crash_report_disclaimer: 'Nós não rastreamos o conteúdo das suas conversas. O relatório de erros apenas contém informações relevantes para identificarmos problemas e corrigí-los.',
Type_message: 'Digitar mensagem',
Room_search: 'Busca de sala',
@ -499,6 +519,30 @@ export default {
You_will_be_logged_out_of_this_application: 'Você sairá deste aplicativo.',
Clear: 'Limpar',
This_will_clear_all_your_offline_data: 'Isto limpará todos os seus dados offline.',
This_will_remove_all_data_from_this_server: 'Isto removerá todos os dados desse servidor.',
Mark_unread: 'Marcar como não Lida',
Wait_activation_warning: 'Antes que você possa fazer o login, sua conta deve ser manualmente ativada por um administrador.'
Wait_activation_warning: 'Antes que você possa fazer o login, sua conta deve ser manualmente ativada por um administrador.',
Screen_lock: 'Bloqueio de Tela',
Local_authentication_biometry_title: 'Autenticar',
Local_authentication_biometry_fallback: 'Usar senha',
Local_authentication_unlock_option: 'Desbloquear com senha',
Local_authentication_change_passcode: 'Alterar senha',
Local_authentication_info: 'Nota: se você esquecer sua senha, terá de apagar e reinstalar o app.',
Local_authentication_facial_recognition: 'reconhecimento facial',
Local_authentication_fingerprint: 'impressão digital',
Local_authentication_unlock_with_label: 'Desbloquear com {{label}}',
Local_authentication_auto_lock_60: 'Após 1 minuto',
Local_authentication_auto_lock_300: 'Após 5 minutos',
Local_authentication_auto_lock_900: 'Após 15 minutos',
Local_authentication_auto_lock_1800: 'Após 30 minutos',
Local_authentication_auto_lock_3600: 'Após 1 hora',
Passcode_enter_title: 'Digite sua senha',
Passcode_choose_title: 'Insira sua nova senha',
Passcode_choose_confirm_title: 'Confirme sua nova senha',
Passcode_choose_error: 'As senhas não coincidem. Tente novamente.',
Passcode_choose_force_set: 'Senha foi exigida pelo admin',
Passcode_app_locked_title: 'Aplicativo bloqueado',
Passcode_app_locked_subtitle: 'Tente novamente em {{timeLeft}} segundos',
After_seconds_set_by_admin: 'Após {{seconds}} segundos (Configurado pelo adm)',
Dont_activate: 'Não ativar agora'
};

View File

@ -46,6 +46,8 @@ import TwoFactor from './containers/TwoFactor';
import RoomsListView from './views/RoomsListView';
import RoomView from './views/RoomView';
import ScreenLockedView from './views/ScreenLockedView';
import ChangePasscodeView from './views/ChangePasscodeView';
if (isIOS) {
const RNScreens = require('react-native-screens');
@ -166,6 +168,15 @@ const ChatsStack = createStackNavigator({
NotificationPrefView: {
getScreen: () => require('./views/NotificationPreferencesView').default
},
VisitorNavigationView: {
getScreen: () => require('./views/VisitorNavigationView').default
},
ForwardLivechatView: {
getScreen: () => require('./views/ForwardLivechatView').default
},
LivechatEditView: {
getScreen: () => require('./views/LivechatEditView').default
},
PickerView: {
getScreen: () => require('./views/PickerView').default
},
@ -224,6 +235,9 @@ const SettingsStack = createStackNavigator({
},
DefaultBrowserView: {
getScreen: () => require('./views/DefaultBrowserView').default
},
ScreenLockConfigView: {
getScreen: () => require('./views/ScreenLockConfigView').default
}
}, {
defaultNavigationOptions: defaultHeader,
@ -514,7 +528,7 @@ class CustomModalStack extends React.Component {
const pageSheetViews = ['AttachmentView'];
const pageSheet = pageSheetViews.includes(getActiveRouteName(navigation.state));
const androidProps = isAndroid && {
const androidProps = isAndroid && !pageSheet && {
style: { marginBottom: 0 }
};
@ -524,7 +538,7 @@ class CustomModalStack extends React.Component {
</View>
);
if (isAndroid) {
if (isAndroid && !pageSheet) {
content = (
<ScrollView overScrollMode='never'>
{content}
@ -729,6 +743,8 @@ export default class Root extends React.Component {
>
{content}
<TwoFactor />
<ScreenLockedView />
<ChangePasscodeView />
</ThemeContext.Provider>
</Provider>
</AppearanceProvider>

View File

@ -0,0 +1,35 @@
// https://github.com/bamlab/redux-enhancer-react-native-appstate
import { AppState } from 'react-native';
import { APP_STATE } from '../actions/actionsTypes';
export default () => createStore => (...args) => {
const store = createStore(...args);
let currentState = '';
const handleAppStateChange = (nextAppState) => {
if (nextAppState !== 'inactive') {
if (currentState !== nextAppState) {
let type;
if (nextAppState === 'active') {
type = APP_STATE.FOREGROUND;
} else if (nextAppState === 'background') {
type = APP_STATE.BACKGROUND;
}
if (type) {
store.dispatch({
type
});
}
}
currentState = nextAppState;
}
};
AppState.addEventListener('change', handleAppStateChange);
// setTimeout to allow redux-saga to catch the initial state fired by redux-enhancer-react-native-appstate library
setTimeout(() => handleAppStateChange(AppState.currentState));
return store;
};

View File

@ -1,9 +1,9 @@
import { createStore, applyMiddleware, compose } from 'redux';
import createSagaMiddleware from 'redux-saga';
import applyAppStateListener from 'redux-enhancer-react-native-appstate';
import reducers from '../reducers';
import sagas from '../sagas';
import applyAppStateMiddleware from './appStateMiddleware';
let sagaMiddleware;
let enhancers;
@ -16,7 +16,7 @@ if (__DEV__) {
});
enhancers = compose(
applyAppStateListener(),
applyAppStateMiddleware(),
applyMiddleware(reduxImmutableStateInvariant),
applyMiddleware(sagaMiddleware),
Reactotron.createEnhancer()
@ -24,7 +24,7 @@ if (__DEV__) {
} else {
sagaMiddleware = createSagaMiddleware();
enhancers = compose(
applyAppStateListener(),
applyAppStateMiddleware(),
applyMiddleware(sagaMiddleware)
);
}

View File

@ -34,6 +34,36 @@ if (__DEV__ && isIOS) {
console.log(appGroupPath);
}
export const getDatabase = (database = '') => {
const path = database.replace(/(^\w+:|^)\/\//, '').replace(/\//g, '.');
const dbName = `${ appGroupPath }${ path }.db`;
const adapter = new SQLiteAdapter({
dbName,
schema: appSchema,
migrations
});
return new Database({
adapter,
modelClasses: [
Subscription,
Room,
Message,
Thread,
ThreadMessage,
CustomEmoji,
FrequentlyUsedEmoji,
Upload,
Setting,
Role,
Permission,
SlashCommand
],
actionsEnabled: true
});
};
class DB {
databases = {
serversDB: new Database({
@ -87,34 +117,8 @@ class DB {
});
}
setActiveDB(database = '') {
const path = database.replace(/(^\w+:|^)\/\//, '').replace(/\//g, '.');
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
});
setActiveDB(database) {
this.databases.activeDB = getDatabase(database);
}
}

View File

@ -30,6 +30,8 @@ export default class Message extends Model {
@field('avatar') avatar;
@field('emoji') emoji;
@json('attachments', sanitizer) attachments;
@json('urls', sanitizer) urls;

View File

@ -13,4 +13,14 @@ export default class Room extends Model {
@field('encrypted') encrypted;
@field('ro') ro;
@json('v', sanitizer) v;
@json('served_by', sanitizer) servedBy;
@field('department_id') departmentId;
@json('livechat_data', sanitizer) livechatData;
@json('tags', sanitizer) tags;
}

View File

@ -17,4 +17,12 @@ export default class Server extends Model {
@date('rooms_updated_at') roomsUpdatedAt;
@field('version') version;
@date('last_local_authenticated_session') lastLocalAuthenticatedSession;
@field('auto_lock') autoLock;
@field('auto_lock_time') autoLockTime;
@field('biometry') biometry;
}

View File

@ -50,6 +50,8 @@ export default class Subscription extends Model {
@field('announcement') announcement;
@field('banner_closed') bannerClosed;
@field('topic') topic;
@field('blocked') blocked;
@ -95,4 +97,14 @@ export default class Subscription extends Model {
@json('uids', sanitizer) uids;
@json('usernames', sanitizer) usernames;
@json('visitor', sanitizer) visitor;
@field('department_id') departmentId;
@json('served_by', sanitizer) servedBy;
@json('livechat_data', sanitizer) livechatData;
@json('tags', sanitizer) tags;
}

View File

@ -30,6 +30,8 @@ export default class Thread extends Model {
@field('avatar') avatar;
@field('emoji') emoji;
@json('attachments', sanitizer) attachments;
@json('urls', sanitizer) urls;

View File

@ -32,6 +32,8 @@ export default class ThreadMessage extends Model {
@field('avatar') avatar;
@field('emoji') emoji;
@json('attachments', sanitizer) attachments;
@json('urls', sanitizer) urls;

View File

@ -74,6 +74,50 @@ export default schemaMigrations({
]
})
]
},
{
toVersion: 8,
steps: [
addColumns({
table: 'messages',
columns: [
{ name: 'emoji', type: 'string', isOptional: true }
]
}),
addColumns({
table: 'thread_messages',
columns: [
{ name: 'emoji', type: 'string', isOptional: true }
]
}),
addColumns({
table: 'threads',
columns: [
{ name: 'emoji', type: 'string', isOptional: true }
]
}),
addColumns({
table: 'subscriptions',
columns: [
{ name: 'banner_closed', type: 'boolean', isOptional: true },
{ name: 'visitor', type: 'string', isOptional: true },
{ name: 'department_id', type: 'string', isOptional: true },
{ name: 'served_by', type: 'string', isOptional: true },
{ name: 'livechat_data', type: 'string', isOptional: true },
{ name: 'tags', type: 'string', isOptional: true }
]
}),
addColumns({
table: 'rooms',
columns: [
{ name: 'v', type: 'string', isOptional: true },
{ name: 'department_id', type: 'string', isOptional: true },
{ name: 'served_by', type: 'string', isOptional: true },
{ name: 'livechat_data', type: 'string', isOptional: true },
{ name: 'tags', type: 'string', isOptional: true }
]
})
]
}
]
});

View File

@ -12,6 +12,20 @@ export default schemaMigrations({
]
})
]
},
{
toVersion: 4,
steps: [
addColumns({
table: 'servers',
columns: [
{ name: 'last_local_authenticated_session', type: 'number', isOptional: true },
{ name: 'auto_lock', type: 'boolean', isOptional: true },
{ name: 'auto_lock_time', type: 'number', isOptional: true },
{ name: 'biometry', type: 'boolean', isOptional: true }
]
})
]
}
]
});

View File

@ -1,7 +1,7 @@
import { appSchema, tableSchema } from '@nozbe/watermelondb';
export default appSchema({
version: 7,
version: 8,
tables: [
tableSchema({
name: 'subscriptions',
@ -25,6 +25,7 @@ export default appSchema({
{ name: 'last_message', type: 'string', isOptional: true },
{ name: 'description', type: 'string', isOptional: true },
{ name: 'announcement', type: 'string', isOptional: true },
{ name: 'banner_closed', type: 'boolean', isOptional: true },
{ name: 'topic', type: 'string', isOptional: true },
{ name: 'blocked', type: 'boolean', isOptional: true },
{ name: 'blocker', type: 'boolean', isOptional: true },
@ -42,7 +43,12 @@ export default appSchema({
{ name: 'hide_unread_status', type: 'boolean', isOptional: true },
{ name: 'sys_mes', type: 'string', isOptional: true },
{ name: 'uids', type: 'string', isOptional: true },
{ name: 'usernames', type: 'string', isOptional: true }
{ name: 'usernames', type: 'string', isOptional: true },
{ name: 'visitor', type: 'string', isOptional: true },
{ name: 'department_id', type: 'string', isOptional: true },
{ name: 'served_by', type: 'string', isOptional: true },
{ name: 'livechat_data', type: 'string', isOptional: true },
{ name: 'tags', type: 'string', isOptional: true }
]
}),
tableSchema({
@ -51,7 +57,12 @@ export default appSchema({
{ name: 'custom_fields', type: 'string' },
{ name: 'broadcast', type: 'boolean' },
{ name: 'encrypted', type: 'boolean' },
{ name: 'ro', type: 'boolean' }
{ name: 'ro', type: 'boolean' },
{ name: 'v', type: 'string', isOptional: true },
{ name: 'department_id', type: 'string', isOptional: true },
{ name: 'served_by', type: 'string', isOptional: true },
{ name: 'livechat_data', type: 'string', isOptional: true },
{ name: 'tags', type: 'string', isOptional: true }
]
}),
tableSchema({
@ -66,6 +77,7 @@ export default appSchema({
{ name: 'parse_urls', type: 'string' },
{ name: 'groupable', type: 'boolean', isOptional: true },
{ name: 'avatar', type: 'string', isOptional: true },
{ name: 'emoji', type: 'string', isOptional: true },
{ name: 'attachments', type: 'string', isOptional: true },
{ name: 'urls', type: 'string', isOptional: true },
{ name: '_updated_at', type: 'number' },
@ -104,6 +116,7 @@ export default appSchema({
{ name: 'parse_urls', type: 'string', isOptional: true },
{ name: 'groupable', type: 'boolean', isOptional: true },
{ name: 'avatar', type: 'string', isOptional: true },
{ name: 'emoji', type: 'string', isOptional: true },
{ name: 'attachments', type: 'string', isOptional: true },
{ name: 'urls', type: 'string', isOptional: true },
{ name: 'status', type: 'number', isOptional: true },
@ -140,6 +153,7 @@ export default appSchema({
{ name: 'parse_urls', type: 'string', isOptional: true },
{ name: 'groupable', type: 'boolean', isOptional: true },
{ name: 'avatar', type: 'string', isOptional: true },
{ name: 'emoji', type: 'string', isOptional: true },
{ name: 'attachments', type: 'string', isOptional: true },
{ name: 'urls', type: 'string', isOptional: true },
{ name: 'status', type: 'number', isOptional: true },

View File

@ -1,7 +1,7 @@
import { appSchema, tableSchema } from '@nozbe/watermelondb';
export default appSchema({
version: 3,
version: 4,
tables: [
tableSchema({
name: 'users',
@ -24,7 +24,11 @@ export default appSchema({
{ 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 }
{ name: 'version', type: 'string', isOptional: true },
{ name: 'last_local_authenticated_session', type: 'number', isOptional: true },
{ name: 'auto_lock', type: 'boolean', isOptional: true },
{ name: 'auto_lock_time', type: 'number', isOptional: true },
{ name: 'biometry', type: 'boolean', isOptional: true }
]
})
]

View File

@ -10,8 +10,9 @@ import log from '../../utils/log';
import database from '../database';
import protectedFunction from './helpers/protectedFunction';
import fetch from '../../utils/fetch';
import { DEFAULT_AUTO_LOCK } from '../../constants/localAuthentication';
const serverInfoKeys = ['Site_Name', 'UI_Use_Real_Name', 'FileUpload_MediaTypeWhiteList', 'FileUpload_MaxFileSize'];
const serverInfoKeys = ['Site_Name', 'UI_Use_Real_Name', 'FileUpload_MediaTypeWhiteList', 'FileUpload_MaxFileSize', 'Force_Screen_Lock', 'Force_Screen_Lock_After'];
// these settings are used only on onboarding process
const loginSettings = [
@ -32,6 +33,8 @@ const loginSettings = [
const serverInfoUpdate = async(serverInfo, iconSetting) => {
const serversDB = database.servers;
const serverId = reduxStore.getState().server.server;
const serversCollection = serversDB.collections.get('servers');
const server = await serversCollection.find(serverId);
let info = serverInfo.reduce((allSettings, setting) => {
if (setting._id === 'Site_Name') {
@ -46,6 +49,23 @@ const serverInfoUpdate = async(serverInfo, iconSetting) => {
if (setting._id === 'FileUpload_MaxFileSize') {
return { ...allSettings, FileUpload_MaxFileSize: setting.valueAsNumber };
}
if (setting._id === 'Force_Screen_Lock') {
// if this was disabled on server side we must keep this enabled on app
const autoLock = server.autoLock || setting.valueAsBoolean;
return { ...allSettings, autoLock };
}
if (setting._id === 'Force_Screen_Lock_After') {
const forceScreenLock = serverInfo.find(s => s._id === 'Force_Screen_Lock')?.valueAsBoolean;
// if Force_Screen_Lock_After === 0 and autoLockTime is null, set app's default value
if (setting.valueAsNumber === 0 && !server.autoLockTime) {
return { ...allSettings, autoLockTime: DEFAULT_AUTO_LOCK };
}
// if Force_Screen_Lock_After > 0 and forceScreenLock is enabled, use it
if (setting.valueAsNumber > 0 && forceScreenLock) {
return { ...allSettings, autoLockTime: setting.valueAsNumber };
}
}
return allSettings;
}, {});
@ -56,9 +76,6 @@ const serverInfoUpdate = async(serverInfo, iconSetting) => {
await serversDB.action(async() => {
try {
const serversCollection = serversDB.collections.get('servers');
const server = await serversCollection.find(serverId);
await server.update((record) => {
Object.assign(record, info);
});

View File

@ -27,6 +27,7 @@ export default async(subscriptions = [], rooms = []) => {
lastOpen: s.lastOpen,
description: s.description,
announcement: s.announcement,
bannerClosed: s.bannerClosed,
topic: s.topic,
blocked: s.blocked,
blocker: s.blocker,
@ -43,7 +44,12 @@ export default async(subscriptions = [], rooms = []) => {
autoTranslateLanguage: s.autoTranslateLanguage,
lastMessage: s.lastMessage,
usernames: s.usernames,
uids: s.uids
uids: s.uids,
visitor: s.visitor,
departmentId: s.departmentId,
servedBy: s.servedBy,
livechatData: s.livechatData,
tags: s.tags
}));
subscriptions = subscriptions.concat(existingSubs);
@ -64,7 +70,12 @@ export default async(subscriptions = [], rooms = []) => {
ro: r.ro,
broadcast: r.broadcast,
muted: r.muted,
sysMes: r.sysMes
sysMes: r.sysMes,
v: r.v,
departmentId: r.departmentId,
servedBy: r.servedBy,
livechatData: r.livechatData,
tags: r.tags
}));
rooms = rooms.concat(existingRooms);
} catch {

View File

@ -35,6 +35,21 @@ export const merge = (subscription, room) => {
} else {
subscription.muted = [];
}
if (room.v) {
subscription.visitor = room.v;
}
if (room.departmentId) {
subscription.departmentId = room.departmentId;
}
if (room.servedBy) {
subscription.servedBy = room.servedBy;
}
if (room.livechatData) {
subscription.livechatData = room.livechatData;
}
if (room.tags) {
subscription.tags = room.tags;
}
subscription.sysMes = room.sysMes;
}

127
app/lib/methods/logout.js Normal file
View File

@ -0,0 +1,127 @@
import RNUserDefaults from 'rn-user-defaults';
import * as FileSystem from 'expo-file-system';
import { Rocketchat as RocketchatClient } from '@rocket.chat/sdk';
import { SERVERS, SERVER_URL } from '../../constants/userDefaults';
import { getDeviceToken } from '../../notifications/push';
import { extractHostname } from '../../utils/server';
import { BASIC_AUTH_KEY } from '../../utils/fetch';
import database, { getDatabase } from '../database';
import RocketChat from '../rocketchat';
import { useSsl } from '../../utils/url';
async function removeServerKeys({ server, userId }) {
await RNUserDefaults.clear(`${ RocketChat.TOKEN_KEY }-${ server }`);
await RNUserDefaults.clear(`${ RocketChat.TOKEN_KEY }-${ userId }`);
await RNUserDefaults.clear(`${ BASIC_AUTH_KEY }-${ server }`);
}
async function removeSharedCredentials({ server }) {
try {
const servers = await RNUserDefaults.objectForKey(SERVERS);
await RNUserDefaults.setObjectForKey(SERVERS, servers && servers.filter(srv => srv[SERVER_URL] !== server));
// clear certificate for server - SSL Pinning
const certificate = await RNUserDefaults.objectForKey(extractHostname(server));
if (certificate && certificate.path) {
await RNUserDefaults.clear(extractHostname(server));
await FileSystem.deleteAsync(certificate.path);
}
} catch (e) {
console.log('removeSharedCredentials', e);
}
}
async function removeServerData({ server }) {
try {
const batch = [];
const serversDB = database.servers;
const userId = await RNUserDefaults.get(`${ RocketChat.TOKEN_KEY }-${ server }`);
const usersCollection = serversDB.collections.get('users');
if (userId) {
const userRecord = await usersCollection.find(userId);
batch.push(userRecord.prepareDestroyPermanently());
}
const serverCollection = serversDB.collections.get('servers');
const serverRecord = await serverCollection.find(server);
batch.push(serverRecord.prepareDestroyPermanently());
await serversDB.action(() => serversDB.batch(...batch));
await removeSharedCredentials({ server });
await removeServerKeys({ server });
} catch (e) {
console.log('removeServerData', e);
}
}
async function removeCurrentServer() {
await RNUserDefaults.clear('currentServer');
await RNUserDefaults.clear(RocketChat.TOKEN_KEY);
}
async function removeServerDatabase({ server }) {
try {
const db = getDatabase(server);
await db.action(() => db.unsafeResetDatabase());
} catch (e) {
console.log(e);
}
}
export async function removeServer({ server }) {
try {
const userId = await RNUserDefaults.get(`${ RocketChat.TOKEN_KEY }-${ server }`);
if (userId) {
const resume = await RNUserDefaults.get(`${ RocketChat.TOKEN_KEY }-${ userId }`);
const sdk = new RocketchatClient({ host: server, protocol: 'ddp', useSsl: useSsl(server) });
await sdk.login({ resume });
const token = getDeviceToken();
if (token) {
await sdk.del('push.token', { token });
}
await sdk.logout();
}
await removeServerData({ server });
await removeServerDatabase({ server });
} catch (e) {
console.log('removePush', e);
}
}
export default async function logout({ server }) {
if (this.roomsSub) {
this.roomsSub.stop();
this.roomsSub = null;
}
if (this.activeUsersSubTimeout) {
clearTimeout(this.activeUsersSubTimeout);
this.activeUsersSubTimeout = false;
}
try {
await this.removePushToken();
} catch (e) {
console.log('removePushToken', e);
}
try {
// RC 0.60.0
await this.sdk.logout();
} catch (e) {
console.log('logout', e);
}
if (this.sdk) {
this.sdk = null;
}
await removeServerData({ server });
await removeCurrentServer();
await removeServerDatabase({ server });
}

View File

@ -62,7 +62,7 @@ export function sendFileMessage(rid, fileInfo, tmid, server, user) {
formData.append('file', {
uri: fileInfo.path,
type: fileInfo.type,
name: fileInfo.name || 'fileMessage'
name: encodeURI(fileInfo.name) || 'fileMessage'
});
if (fileInfo.description) {

View File

@ -10,6 +10,7 @@ import reduxStore from '../../createStore';
import { addUserTyping, removeUserTyping, clearUserTyping } from '../../../actions/usersTyping';
import debounce from '../../../utils/debounce';
import RocketChat from '../../rocketchat';
import { subscribeRoom, unsubscribeRoom } from '../../../actions/room';
const WINDOW_TIME = 1000;
@ -38,6 +39,8 @@ export default class RoomSubscription {
if (!this.isAlive) {
this.unsubscribe();
}
reduxStore.dispatch(subscribeRoom(this.rid));
}
unsubscribe = async() => {
@ -59,6 +62,8 @@ export default class RoomSubscription {
if (this.timer) {
clearTimeout(this.timer);
}
reduxStore.dispatch(unsubscribeRoom(this.rid));
}
removeListener = async(promise) => {
@ -155,22 +160,17 @@ export default class RoomSubscription {
const msgCollection = db.collections.get('messages');
const threadsCollection = db.collections.get('threads');
const threadMessagesCollection = db.collections.get('thread_messages');
let messageRecord;
let threadRecord;
let threadMessageRecord;
// Create or update message
try {
messageRecord = await msgCollection.find(message._id);
} catch (error) {
// Do nothing
}
if (messageRecord) {
const update = messageRecord.prepareUpdate((m) => {
const messageRecord = await msgCollection.find(message._id);
if (!messageRecord._hasPendingUpdate) {
const update = messageRecord.prepareUpdate(protectedFunction((m) => {
Object.assign(m, message);
});
}));
this._messagesBatch[message._id] = update;
} else {
}
} catch {
const create = msgCollection.prepareCreate(protectedFunction((m) => {
m._raw = sanitizedRaw({ id: message._id }, msgCollection.schema);
m.subscription.id = this.rid;
@ -182,17 +182,14 @@ export default class RoomSubscription {
// Create or update thread
if (message.tlm) {
try {
threadRecord = await threadsCollection.find(message._id);
} catch (error) {
// Do nothing
}
if (threadRecord) {
const threadRecord = await threadsCollection.find(message._id);
if (!threadRecord._hasPendingUpdate) {
const updateThread = threadRecord.prepareUpdate(protectedFunction((t) => {
Object.assign(t, message);
}));
this._threadsBatch[message._id] = updateThread;
} else {
}
} catch {
const createThread = threadsCollection.prepareCreate(protectedFunction((t) => {
t._raw = sanitizedRaw({ id: message._id }, threadsCollection.schema);
t.subscription.id = this.rid;
@ -205,19 +202,16 @@ export default class RoomSubscription {
// Create or update thread message
if (message.tmid) {
try {
threadMessageRecord = await threadMessagesCollection.find(message._id);
} catch (error) {
// Do nothing
}
if (threadMessageRecord) {
const threadMessageRecord = await threadMessagesCollection.find(message._id);
if (!threadMessageRecord._hasPendingUpdate) {
const updateThreadMessage = threadMessageRecord.prepareUpdate(protectedFunction((tm) => {
Object.assign(tm, message);
tm.rid = message.tmid;
delete tm.tmid;
}));
this._threadMessagesBatch[message._id] = updateThreadMessage;
} else {
}
} catch {
const createThreadMessage = threadMessagesCollection.prepareCreate(protectedFunction((tm) => {
tm._raw = sanitizedRaw({ id: message._id }, threadMessagesCollection.schema);
Object.assign(tm, message);

View File

@ -57,6 +57,7 @@ const createOrUpdateSubscription = async(subscription, room) => {
lastOpen: s.lastOpen,
description: s.description,
announcement: s.announcement,
bannerClosed: s.bannerClosed,
topic: s.topic,
blocked: s.blocked,
blocker: s.blocker,
@ -74,7 +75,12 @@ const createOrUpdateSubscription = async(subscription, room) => {
lastMessage: s.lastMessage,
roles: s.roles,
usernames: s.usernames,
uids: s.uids
uids: s.uids,
visitor: s.visitor,
departmentId: s.departmentId,
servedBy: s.servedBy,
livechatData: s.livechatData,
tags: s.tags
};
} catch (error) {
try {
@ -97,10 +103,15 @@ const createOrUpdateSubscription = async(subscription, room) => {
// 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,
v: r.v,
ro: r.ro,
tags: r.tags,
servedBy: r.servedBy,
encrypted: r.encrypted,
ro: r.ro
broadcast: r.broadcast,
customFields: r.customFields,
departmentId: r.departmentId,
livechatData: r.livechatData
};
} catch (error) {
// Do nothing
@ -121,6 +132,11 @@ const createOrUpdateSubscription = async(subscription, room) => {
try {
const update = sub.prepareUpdate((s) => {
Object.assign(s, tmp);
if (subscription.announcement) {
if (subscription.announcement !== sub.announcement) {
s.bannerClosed = false;
}
}
});
batch.push(update);
} catch (e) {
@ -141,7 +157,8 @@ const createOrUpdateSubscription = async(subscription, room) => {
}
}
if (tmp.lastMessage) {
const { rooms } = store.getState().room;
if (tmp.lastMessage && !rooms.includes(tmp.rid)) {
const lastMessage = buildMessage(tmp.lastMessage);
const messagesCollection = db.collections.get('messages');
let messageRecord;

View File

@ -1,9 +1,9 @@
import { AsyncStorage, InteractionManager } from 'react-native';
import { InteractionManager } from 'react-native';
import semver from 'semver';
import { Rocketchat as RocketchatClient } from '@rocket.chat/sdk';
import RNUserDefaults from 'rn-user-defaults';
import { Q } from '@nozbe/watermelondb';
import * as FileSystem from 'expo-file-system';
import AsyncStorage from '@react-native-community/async-storage';
import reduxStore from './createStore';
import defaultSettings from '../constants/settings';
@ -11,8 +11,7 @@ import messagesStatus from '../constants/messagesStatus';
import database from './database';
import log from '../utils/log';
import { isIOS, getBundleId } from '../utils/deviceInfo';
import { extractHostname } from '../utils/server';
import fetch, { BASIC_AUTH_KEY } from '../utils/fetch';
import fetch from '../utils/fetch';
import { setUser, setLoginServices, loginRequest } from '../actions/login';
import { disconnect, connectSuccess, connectRequest } from '../actions/connect';
@ -43,12 +42,14 @@ import sendMessage, { sendMessageCall } from './methods/sendMessage';
import { sendFileMessage, cancelUpload, isUploadActive } from './methods/sendFileMessage';
import callJitsi from './methods/callJitsi';
import logout, { removeServer } from './methods/logout';
import { getDeviceToken } from '../notifications/push';
import { SERVERS, SERVER_URL } from '../constants/userDefaults';
import { setActiveUsers } from '../actions/activeUsers';
import I18n from '../i18n';
import { twoFactor } from '../utils/twoFactor';
import { selectServerFailure } from '../actions/server';
import { useSsl } from '../utils/url';
const TOKEN_KEY = 'reactnativemeteor_usertoken';
const SORT_PREFS_KEY = 'RC_SORT_PREFS_KEY';
@ -86,10 +87,7 @@ const RocketChat = {
}
},
async getWebsocketInfo({ server }) {
// Use useSsl: false only if server url starts with http://
const useSsl = !/http:\/\//.test(server);
const sdk = new RocketchatClient({ host: server, protocol: 'ddp', useSsl });
const sdk = new RocketchatClient({ host: server, protocol: 'ddp', useSsl: useSsl(server) });
try {
await sdk.connect();
@ -146,6 +144,10 @@ const RocketChat = {
}
return result;
} catch (e) {
if (e.message === 'Aborted') {
reduxStore.dispatch(selectServerFailure());
throw e;
}
log(e);
}
return {
@ -159,6 +161,16 @@ const RocketChat = {
stopListener(listener) {
return listener && listener.stop();
},
// Abort all requests and create a new AbortController
abort() {
if (this.controller) {
this.controller.abort();
if (this.sdk) {
this.sdk.abort();
}
}
this.controller = new AbortController();
},
connect({ server, user, logoutOnError = false }) {
return new Promise((resolve) => {
if (!this.sdk || this.sdk.client.host !== server) {
@ -200,15 +212,13 @@ const RocketChat = {
this.code = null;
}
// Use useSsl: false only if server url starts with http://
const useSsl = !/http:\/\//.test(server);
this.sdk = new RocketchatClient({ host: server, protocol: 'ddp', useSsl });
this.sdk = new RocketchatClient({ host: server, protocol: 'ddp', useSsl: useSsl(server) });
this.getSettings();
const sdkConnect = () => this.sdk.connect()
.then(() => {
if (user && user.token) {
const { server: currentServer } = reduxStore.getState().server;
if (user && user.token && server === currentServer) {
reduxStore.dispatch(loginRequest({ resume: user.token }, logoutOnError));
}
})
@ -217,7 +227,9 @@ const RocketChat = {
// when `connect` raises an error, we try again in 10 seconds
this.connectTimeout = setTimeout(() => {
if (this.sdk?.client?.host === server) {
sdkConnect();
}
}, 10000);
});
@ -270,10 +282,7 @@ const RocketChat = {
this.shareSDK = null;
}
// Use useSsl: false only if server url starts with http://
const useSsl = !/http:\/\//.test(server);
this.shareSDK = new RocketchatClient({ host: server, protocol: 'ddp', useSsl });
this.shareSDK = new RocketchatClient({ host: server, protocol: 'ddp', useSsl: useSsl(server) });
// set Server
const serversDB = database.servers;
@ -306,7 +315,7 @@ const RocketChat = {
}
database.share = null;
reduxStore.dispatch(shareSetUser(null));
reduxStore.dispatch(shareSetUser({}));
},
updateJitsiTimeout(rid) {
@ -369,24 +378,15 @@ const RocketChat = {
};
}
try {
return this.loginTOTP(params);
} catch (error) {
throw error;
}
},
async loginOAuthOrSso(params) {
try {
const result = await this.login(params);
reduxStore.dispatch(loginRequest({ resume: result.token }));
} catch (error) {
throw error;
}
},
async login(params) {
try {
const sdk = this.shareSDK || this.sdk;
// RC 0.64.0
await sdk.login(params);
@ -404,77 +404,9 @@ const RocketChat = {
roles: result.me.roles
};
return user;
} catch (e) {
throw e;
}
},
async logout({ server }) {
if (this.roomsSub) {
this.roomsSub.stop();
this.roomsSub = null;
}
if (this.activeUsersSubTimeout) {
clearTimeout(this.activeUsersSubTimeout);
this.activeUsersSubTimeout = false;
}
try {
await this.removePushToken();
} catch (error) {
console.log('logout -> removePushToken -> catch -> error', error);
}
try {
// RC 0.60.0
await this.sdk.logout();
} catch (error) {
console.log('logout -> api logout -> catch -> error', error);
}
this.sdk = null;
try {
const servers = await RNUserDefaults.objectForKey(SERVERS);
await RNUserDefaults.setObjectForKey(SERVERS, servers && servers.filter(srv => srv[SERVER_URL] !== server));
// clear certificate for server - SSL Pinning
const certificate = await RNUserDefaults.objectForKey(extractHostname(server));
if (certificate && certificate.path) {
await RNUserDefaults.clear(extractHostname(server));
await FileSystem.deleteAsync(certificate.path);
}
} catch (error) {
console.log('logout_rn_user_defaults', error);
}
const userId = await RNUserDefaults.get(`${ TOKEN_KEY }-${ server }`);
try {
const serversDB = database.servers;
await serversDB.action(async() => {
const usersCollection = serversDB.collections.get('users');
const userRecord = await usersCollection.find(userId);
const serverCollection = serversDB.collections.get('servers');
const serverRecord = await serverCollection.find(server);
await serversDB.batch(
userRecord.prepareDestroyPermanently(),
serverRecord.prepareDestroyPermanently()
);
});
} catch (error) {
// Do nothing
}
await RNUserDefaults.clear('currentServer');
await RNUserDefaults.clear(TOKEN_KEY);
await RNUserDefaults.clear(`${ TOKEN_KEY }-${ server }`);
await RNUserDefaults.clear(`${ BASIC_AUTH_KEY }-${ server }`);
try {
const db = database.active;
await db.action(() => db.unsafeResetDatabase());
} catch (error) {
console.log(error);
}
},
logout,
removeServer,
async clearCache({ server }) {
try {
const serversDB = database.servers;
@ -573,9 +505,9 @@ const RocketChat = {
).fetch();
if (filterUsers && !filterRooms) {
data = data.filter(item => item.t === 'd');
data = data.filter(item => item.t === 'd' && !RocketChat.isGroupChat(item));
} else if (!filterUsers && filterRooms) {
data = data.filter(item => item.t !== 'd');
data = data.filter(item => item.t !== 'd' || RocketChat.isGroupChat(item));
}
data = data.slice(0, 7);
@ -824,6 +756,59 @@ const RocketChat = {
return this.sdk.get('rooms.info', { roomId });
},
getVisitorInfo(visitorId) {
// RC 2.3.0
return this.sdk.get('livechat/visitors.info', { visitorId });
},
closeLivechat(rid, comment) {
// RC 0.29.0
return this.methodCall('livechat:closeRoom', rid, comment, { clientAction: true });
},
editLivechat(userData, roomData) {
// RC 0.55.0
return this.methodCall('livechat:saveInfo', userData, roomData);
},
returnLivechat(rid) {
// RC 0.72.0
return this.methodCall('livechat:returnAsInquiry', rid);
},
forwardLivechat(transferData) {
// RC 0.36.0
return this.methodCall('livechat:transfer', transferData);
},
getPagesLivechat(rid, offset) {
// RC 2.3.0
return this.sdk.get(`livechat/visitors.pagesVisited/${ rid }?count=50&offset=${ offset }`);
},
getDepartmentInfo(departmentId) {
// RC 2.2.0
return this.sdk.get(`livechat/department/${ departmentId }?includeAgents=false`);
},
getDepartments() {
// RC 2.2.0
return this.sdk.get('livechat/department');
},
usersAutoComplete(selector) {
// RC 2.4.0
return this.sdk.get('users.autocomplete', { selector });
},
getRoutingConfig() {
// RC 2.0.0
return this.methodCall('livechat:getRoutingConfig');
},
getTagsList() {
// RC 2.0.0
return this.methodCall('livechat:getTagsList');
},
getAgentDepartments(uid) {
// RC 2.4.0
return this.sdk.get(`livechat/agents/${ uid }/departments`);
},
getCustomFields() {
// RC 2.2.0
return this.sdk.get('livechat/custom-fields');
},
getUidDirectMessage(room) {
const { id: userId } = reduxStore.getState().login.user;
@ -909,7 +894,7 @@ const RocketChat = {
methodCall(...args) {
return new Promise(async(resolve, reject) => {
try {
const result = await this.sdk.methodCall(...args, this.code);
const result = await this.sdk.methodCall(...args, this.code || '');
return resolve(result);
} catch (e) {
if (e.error && (e.error === 'totp-required' || e.error === 'totp-invalid')) {
@ -977,7 +962,7 @@ const RocketChat = {
const shareUser = reduxStore.getState().share.user;
const loginUser = reduxStore.getState().login.user;
// get user roles on the server from redux
const userRoles = (shareUser.roles || loginUser.roles) || [];
const userRoles = (shareUser?.roles || loginUser?.roles) || [];
// merge both roles
const mergedRoles = [...new Set([...roomRoles, ...userRoles])];
@ -1226,16 +1211,19 @@ const RocketChat = {
return this.methodCall('autoTranslate.translateMessage', message, targetLanguage);
},
getRoomTitle(room) {
const { UI_Use_Real_Name: useRealName } = reduxStore.getState().settings;
const { UI_Use_Real_Name: useRealName, UI_Allow_room_names_with_special_chars: allowSpecialChars } = reduxStore.getState().settings;
const { username } = reduxStore.getState().login.user;
if (RocketChat.isGroupChat(room) && !(room.name && room.name.length)) {
return room.usernames.filter(u => u !== username).sort((u1, u2) => u1.localeCompare(u2)).join(', ');
}
if (allowSpecialChars && room.t !== 'd') {
return room.fname || room.name;
}
return ((room.prid || useRealName) && room.fname) || room.name;
},
getRoomAvatar(room) {
if (RocketChat.isGroupChat(room)) {
return room.uids.length + room.usernames.join();
return room.uids?.length + room.usernames?.join();
}
return room.prid ? room.fname : room.name;
},

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