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

View File

@ -87,6 +87,7 @@ module.exports = {
"no-regex-spaces": 2, "no-regex-spaces": 2,
"no-undef": 2, "no-undef": 2,
"no-unreachable": 2, "no-unreachable": 2,
"no-unused-expressions": 0,
"no-unused-vars": [2, { "no-unused-vars": [2, {
"vars": "all", "vars": "all",
"args": "after-used" "args": "after-used"
@ -131,7 +132,23 @@ module.exports = {
"react-native/no-unused-styles": 2, "react-native/no-unused-styles": 2,
"react/jsx-one-expression-per-line": 0, "react/jsx-one-expression-per-line": 0,
"require-await": 2, "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": { "globals": {
"__DEV__": true "__DEV__": true

1
.gitignore vendored
View File

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

View File

@ -208,13 +208,15 @@ Readme will guide you on how to config.
- Build your app - Build your app
```bash ```bash
$ detox build --configuration ios.sim.release $ npx detox build --configuration ios.sim.release
``` ```
- Run tests - Run tests
```bash ```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 ## 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: () => '', getModel: () => '',
getReadableVersion: () => '', getReadableVersion: () => '',
getBundleId: () => '', 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 * // the name of the generated asset file containing your JS bundle
* bundleAssetName: "index.android.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", * entryFile: "index.android.js",
* *
* // https://facebook.github.io/react-native/docs/performance#enable-the-ram-format * // https://facebook.github.io/react-native/docs/performance#enable-the-ram-format
@ -80,7 +82,6 @@ import com.android.build.OutputFile
*/ */
project.ext.react = [ project.ext.react = [
entryFile: "index.js",
bundleAssetName: "app.bundle", bundleAssetName: "app.bundle",
iconFontNames: [ 'custom.ttf' ], iconFontNames: [ 'custom.ttf' ],
enableHermes: true, // clean and rebuild if changing enableHermes: true, // clean and rebuild if changing
@ -141,6 +142,7 @@ android {
versionName VERSIONNAME as String versionName VERSIONNAME as String
vectorDrawables.useSupportLibrary = true vectorDrawables.useSupportLibrary = true
manifestPlaceholders = [BugsnagAPIKey: BugsnagAPIKey as String] manifestPlaceholders = [BugsnagAPIKey: BugsnagAPIKey as String]
missingDimensionStrategy "RNNotifications.reactNativeVersion", "reactNative60" // See note below!
} }
signingConfigs { signingConfigs {
@ -168,6 +170,14 @@ android {
signingConfig signingConfigs.release 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 are e.g. debug, release
applicationVariants.all { variant -> applicationVariants.all { variant ->
variant.outputs.each { output -> variant.outputs.each { output ->
@ -202,6 +212,7 @@ dependencies {
implementation project(":reactnativekeyboardinput") implementation project(":reactnativekeyboardinput")
implementation project(':@react-native-community_viewpager') implementation project(':@react-native-community_viewpager')
implementation fileTree(dir: "libs", include: ["*.jar"]) implementation fileTree(dir: "libs", include: ["*.jar"])
//noinspection GradleDynamicVersion
implementation "com.facebook.react:react-native:+" // From node_modules implementation "com.facebook.react:react-native:+" // From node_modules
implementation "com.google.firebase:firebase-messaging:18.0.0" implementation "com.google.firebase:firebase-messaging:18.0.0"
implementation "com.google.firebase:firebase-core:16.0.9" implementation "com.google.firebase:firebase-core:16.0.9"
@ -209,6 +220,16 @@ dependencies {
implementation('com.crashlytics.sdk.android:crashlytics:2.9.9@aar') { implementation('com.crashlytics.sdk.android:crashlytics:2.9.9@aar') {
transitive = true 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) { if (enableHermes) {
def hermesPath = "../../node_modules/hermes-engine/android/"; 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"> package="chat.rocket.reactnative">
<uses-permission android:name="android.permission.INTERNET" /> <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.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_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.RECORD_AUDIO" />
<uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.DOWNLOAD_WITHOUT_NOTIFICATION" /> <uses-permission android:name="android.permission.DOWNLOAD_WITHOUT_NOTIFICATION" /> -->
<uses-permission-sdk-23 android:name="android.permission.VIBRATE"/> <!-- <uses-permission-sdk-23 android:name="android.permission.VIBRATE"/> -->
<application <application
android:name=".MainApplication" android:name=".MainApplication"
@ -30,7 +30,7 @@
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:label="@string/app_name" android:label="@string/app_name"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize" android:configChanges="keyboard|keyboardHidden|orientation|screenSize|uiMode"
android:launchMode="singleTask" android:launchMode="singleTask"
android:windowSoftInputMode="adjustResize" android:windowSoftInputMode="adjustResize"
android:exported="true"> android:exported="true">

View File

@ -9,9 +9,11 @@ import com.facebook.react.PackageList;
import com.facebook.hermes.reactexecutor.HermesExecutorFactory; import com.facebook.hermes.reactexecutor.HermesExecutorFactory;
import com.facebook.react.bridge.JavaScriptExecutorFactory; import com.facebook.react.bridge.JavaScriptExecutorFactory;
import com.facebook.react.ReactApplication; import com.facebook.react.ReactApplication;
import com.facebook.react.ReactInstanceManager;
import com.facebook.react.ReactNativeHost; import com.facebook.react.ReactNativeHost;
import com.facebook.react.ReactPackage; import com.facebook.react.ReactPackage;
import com.facebook.soloader.SoLoader; import com.facebook.soloader.SoLoader;
import java.lang.reflect.InvocationTargetException;
import chat.rocket.reactnative.generated.BasePackageList; import chat.rocket.reactnative.generated.BasePackageList;
@ -39,7 +41,7 @@ import java.util.List;
public class MainApplication extends Application implements ReactApplication, INotificationsApplication { 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) { private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) {
@Override @Override
@ -58,7 +60,11 @@ public class MainApplication extends Application implements ReactApplication, IN
packages.add(new RNNotificationsPackage(MainApplication.this)); packages.add(new RNNotificationsPackage(MainApplication.this));
packages.add(new WatermelonDBPackage()); packages.add(new WatermelonDBPackage());
packages.add(new RNCViewPagerPackage()); 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; return packages;
} }
@ -82,6 +88,38 @@ public class MainApplication extends Application implements ReactApplication, IN
public void onCreate() { public void onCreate() {
super.onCreate(); super.onCreate();
SoLoader.init(this, /* native exopackage */ false); 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 @Override

View File

@ -11,6 +11,9 @@ public class BasePackageList {
new expo.modules.constants.ConstantsPackage(), new expo.modules.constants.ConstantsPackage(),
new expo.modules.filesystem.FileSystemPackage(), new expo.modules.filesystem.FileSystemPackage(),
new expo.modules.haptics.HapticsPackage(), 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.permissions.PermissionsPackage(),
new expo.modules.webbrowser.WebBrowserPackage() new expo.modules.webbrowser.WebBrowserPackage()
); );

View File

@ -18,7 +18,7 @@ buildscript {
} }
} }
dependencies { 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 'com.google.gms:google-services:4.2.0'
classpath 'io.fabric.tools:gradle:1.28.1' classpath 'io.fabric.tools:gradle:1.28.1'
classpath 'com.google.firebase:perf-plugin:1.2.1' classpath 'com.google.firebase:perf-plugin:1.2.1'
@ -42,16 +42,14 @@ allprojects {
url("$rootDir/../node_modules/jsc-android/dist") url("$rootDir/../node_modules/jsc-android/dist")
} }
maven { maven {
// We should change it when Jitsi-SDK release v2.4 url "https://github.com/jitsi/jitsi-maven-repository/raw/master/releases"
url("$rootDir/../node_modules/react-native-jitsi-meet/jitsi-sdk")
// url "https://github.com/jitsi/jitsi-maven-repository/raw/master/releases"
} }
google() google()
jcenter() jcenter()
maven { url 'https://maven.google.com' } maven { url 'https://maven.google.com' }
maven { url "https://jitpack.io" } maven { url 'https://www.jitpack.io' }
} }
} }
@ -64,6 +62,12 @@ subprojects { subproject ->
defaultConfig { defaultConfig {
targetSdkVersion 28 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.enableAapt2=false # commenting this makes notifications to stop working
# android.useDeprecatedNdk=true # android.useDeprecatedNdk=true
org.gradle.jvmargs=-Xmx2048M -XX\:MaxHeapSize\=32g 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 android.useAndroidX=true
# Automatically convert third-party libraries to use AndroidX
android.enableJetifier=true android.enableJetifier=true
APPLICATIONID=chat.rocket.reactnative APPLICATIONID=chat.rocket.reactnative
VERSIONNAME=4.5.1 VERSIONNAME=4.5.1
@ -29,3 +33,6 @@ KEYSTORE=my-upload-key.keystore
KEY_ALIAS=my-key-alias KEY_ALIAS=my-key-alias
KEYSTORE_PASSWORD= KEYSTORE_PASSWORD=
KEY_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 distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists 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 zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists 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 not use this file except in compliance with the License.
# You may obtain a copy of the License at # 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 # Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, # distributed under the License is distributed on an "AS IS" BASIS,
@ -44,7 +44,7 @@ APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"` 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. # 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. # Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum" 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\"" GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi fi
# For Cygwin, switch paths to Windows format before running java # For Cygwin or MSYS, switch paths to Windows format before running java
if $cygwin ; then if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"` APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"` JAVACMD=`cygpath --unix "$JAVACMD"`

View File

@ -31,7 +31,7 @@ export const ROOMS = createRequestTypes('ROOMS', [
'OPEN_SEARCH_HEADER', 'OPEN_SEARCH_HEADER',
'CLOSE_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 APP = createRequestTypes('APP', ['START', 'READY', 'INIT', 'INIT_LOCAL_SETTINGS']);
export const MESSAGES = createRequestTypes('MESSAGES', ['REPLY_BROADCAST']); export const MESSAGES = createRequestTypes('MESSAGES', ['REPLY_BROADCAST']);
export const CREATE_CHANNEL = createRequestTypes('CREATE_CHANNEL', [...defaultTypes]); export const CREATE_CHANNEL = createRequestTypes('CREATE_CHANNEL', [...defaultTypes]);
@ -64,3 +64,4 @@ export const INVITE_LINKS = createRequestTypes('INVITE_LINKS', [
...defaultTypes ...defaultTypes
]); ]);
export const SETTINGS = createRequestTypes('SETTINGS', ['CLEAR', 'ADD']); 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'; 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) { export function leaveRoom(rid, t) {
return { return {
type: types.ROOM.LEAVE, 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() { export function removedRoom() {
return { return {
type: types.ROOM.REMOVED type: types.ROOM.REMOVED

View File

@ -46,7 +46,14 @@ export const themes = {
messageboxBackground: '#ffffff', messageboxBackground: '#ffffff',
searchboxBackground: '#E6E6E7', searchboxBackground: '#E6E6E7',
buttonBackground: '#414852', buttonBackground: '#414852',
buttonText: '#ffffff' buttonText: '#ffffff',
passcodeBackground: '#EEEFF1',
passcodeButtonActive: '#E4E7EA',
passcodeLockIcon: '#6C727A',
passcodePrimary: '#2F343D',
passcodeSecondary: '#6C727A',
passcodeDotEmpty: '#CBCED1',
passcodeDotFull: '#6C727A'
}, },
dark: { dark: {
backgroundColor: '#030b1b', backgroundColor: '#030b1b',
@ -81,7 +88,14 @@ export const themes = {
messageboxBackground: '#0b182c', messageboxBackground: '#0b182c',
searchboxBackground: '#192d4d', searchboxBackground: '#192d4d',
buttonBackground: '#414852', buttonBackground: '#414852',
buttonText: '#ffffff' buttonText: '#ffffff',
passcodeBackground: '#030C1B',
passcodeButtonActive: '#0B182C',
passcodeLockIcon: '#6C727A',
passcodePrimary: '#FFFFFF',
passcodeSecondary: '#CBCED1',
passcodeDotEmpty: '#CBCED1',
passcodeDotFull: '#6C727A'
}, },
black: { black: {
backgroundColor: '#000000', backgroundColor: '#000000',
@ -100,7 +114,7 @@ export const themes = {
infoText: '#6d6d72', infoText: '#6d6d72',
tintColor: '#1e9bfe', tintColor: '#1e9bfe',
auxiliaryTintColor: '#cdcdcd', auxiliaryTintColor: '#cdcdcd',
actionTintColor: '#1ea1fe', actionTintColor: '#1e9bfe',
separatorColor: '#272728', separatorColor: '#272728',
navbarBackground: '#0d0d0d', navbarBackground: '#0d0d0d',
headerBorder: '#323232', headerBorder: '#323232',
@ -116,6 +130,13 @@ export const themes = {
messageboxBackground: '#0d0d0d', messageboxBackground: '#0d0d0d',
searchboxBackground: '#1f1f1f', searchboxBackground: '#1f1f1f',
buttonBackground: '#414852', 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: { LDAP_Enable: {
type: 'valueAsBoolean' type: 'valueAsBoolean'
}, },
Livechat_request_comment_when_closing_conversation: {
type: 'valueAsBoolean'
},
Jitsi_Enabled: { Jitsi_Enabled: {
type: 'valueAsBoolean' type: 'valueAsBoolean'
}, },
@ -125,6 +128,9 @@ export default {
uniqueID: { uniqueID: {
type: 'valueAsString' type: 'valueAsString'
}, },
UI_Allow_room_names_with_special_chars: {
type: 'valueAsBoolean'
},
UI_Use_Real_Name: { UI_Use_Real_Name: {
type: 'valueAsBoolean' type: 'valueAsBoolean'
}, },
@ -157,5 +163,11 @@ export default {
}, },
CAS_login_url: { CAS_login_url: {
type: 'valueAsString' 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 PropTypes from 'prop-types';
import { View } from 'react-native'; import { View } from 'react-native';
import FastImage from 'react-native-fast-image'; import FastImage from 'react-native-fast-image';
import Touchable from 'react-native-platform-touchable';
import { settings as RocketChatSettings } from '@rocket.chat/sdk'; import { settings as RocketChatSettings } from '@rocket.chat/sdk';
import Touch from '../utils/touch';
import { avatarURL } from '../utils/avatar'; import { avatarURL } from '../utils/avatar';
import Emoji from './markdown/Emoji';
const Avatar = React.memo(({ 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 = { const avatarStyle = {
width: size, width: size,
@ -23,7 +25,15 @@ const Avatar = React.memo(({
type, text, size, userId, token, avatar, baseUrl type, text, size, userId, token, avatar, baseUrl
}); });
let image = ( let image = emoji ? (
<Emoji
theme={theme}
baseUrl={baseUrl}
getCustomEmoji={getCustomEmoji}
isMessageContainsOnlyEmoji
literal={emoji}
/>
) : (
<FastImage <FastImage
style={avatarStyle} style={avatarStyle}
source={{ source={{
@ -36,9 +46,9 @@ const Avatar = React.memo(({
if (onPress) { if (onPress) {
image = ( image = (
<Touch onPress={onPress} theme={theme}> <Touchable onPress={onPress}>
{image} {image}
</Touch> </Touchable>
); );
} }
@ -55,6 +65,7 @@ Avatar.propTypes = {
style: PropTypes.any, style: PropTypes.any,
text: PropTypes.string, text: PropTypes.string,
avatar: PropTypes.string, avatar: PropTypes.string,
emoji: PropTypes.string,
size: PropTypes.number, size: PropTypes.number,
borderRadius: PropTypes.number, borderRadius: PropTypes.number,
type: PropTypes.string, type: PropTypes.string,
@ -62,7 +73,8 @@ Avatar.propTypes = {
userId: PropTypes.string, userId: PropTypes.string,
token: PropTypes.string, token: PropTypes.string,
theme: PropTypes.string, theme: PropTypes.string,
onPress: PropTypes.func onPress: PropTypes.func,
getCustomEmoji: PropTypes.func
}; };
Avatar.defaultProps = { Avatar.defaultProps = {

View File

@ -23,7 +23,7 @@ export const FormContainerInner = ({ children }) => (
</View> </View>
); );
const FormContainer = ({ children, theme }) => ( const FormContainer = ({ children, theme, testID }) => (
<KeyboardView <KeyboardView
style={{ backgroundColor: themes[theme].backgroundColor }} style={{ backgroundColor: themes[theme].backgroundColor }}
contentContainerStyle={sharedStyles.container} contentContainerStyle={sharedStyles.container}
@ -31,7 +31,7 @@ const FormContainer = ({ children, theme }) => (
> >
<StatusBar theme={theme} /> <StatusBar theme={theme} />
<ScrollView {...scrollPersistTaps} style={sharedStyles.container} contentContainerStyle={[sharedStyles.containerScrollView, styles.scrollView]}> <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} {children}
<AppVersion theme={theme} /> <AppVersion theme={theme} />
</SafeAreaView> </SafeAreaView>
@ -41,6 +41,7 @@ const FormContainer = ({ children, theme }) => (
FormContainer.propTypes = { FormContainer.propTypes = {
theme: PropTypes.string, theme: PropTypes.string,
testID: PropTypes.string,
children: PropTypes.element 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 { themes } from '../constants/colors';
import { loginRequest as loginRequestAction } from '../actions/login'; import { loginRequest as loginRequestAction } from '../actions/login';
import Button from './Button'; import Button from './Button';
import OnboardingSeparator from './OnboardingSeparator'; import OrSeparator from './OrSeparator';
import Touch from '../utils/touch'; import Touch from '../utils/touch';
import I18n from '../i18n'; import I18n from '../i18n';
import random from '../utils/random'; import random from '../utils/random';
@ -252,12 +252,12 @@ class LoginServices extends React.PureComponent {
style={styles.options} style={styles.options}
color={themes[theme].actionTintColor} color={themes[theme].actionTintColor}
/> />
<OnboardingSeparator theme={theme} /> <OrSeparator theme={theme} />
</> </>
); );
} }
if (length > 0 && separator) { if (length > 0 && separator) {
return <OnboardingSeparator theme={theme} />; return <OrSeparator theme={theme} />;
} }
return null; return null;
} }

View File

@ -64,7 +64,7 @@ const MentionItem = ({
content = ( content = (
<> <>
<Text style={[styles.slash, { backgroundColor: themes[theme].borderColor, color: themes[theme].tintColor }]}>/</Text> <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'; } from 'react-native';
import { AudioRecorder, AudioUtils } from 'react-native-audio'; import { AudioRecorder, AudioUtils } from 'react-native-audio';
import { BorderlessButton } from 'react-native-gesture-handler'; import { BorderlessButton } from 'react-native-gesture-handler';
import { activateKeepAwake, deactivateKeepAwake } from 'expo-keep-awake';
import RNFetchBlob from 'rn-fetch-blob'; import RNFetchBlob from 'rn-fetch-blob';
import styles from './styles'; import styles from './styles';
@ -59,7 +60,8 @@ export default class extends React.PureComponent {
SampleRate: 22050, SampleRate: 22050,
Channels: 1, Channels: 1,
AudioQuality: 'Low', AudioQuality: 'Low',
AudioEncoding: 'aac' AudioEncoding: 'aac',
OutputFormat: 'aac_adts'
}); });
AudioRecorder.onProgress = (data) => { AudioRecorder.onProgress = (data) => {
@ -74,12 +76,16 @@ export default class extends React.PureComponent {
} }
}; };
AudioRecorder.startRecording(); AudioRecorder.startRecording();
activateKeepAwake();
} }
componentWillUnmount() { componentWillUnmount() {
if (this.recording) { if (this.recording) {
this.cancelAudioMessage(); this.cancelAudioMessage();
} }
deactivateKeepAwake();
} }
finishRecording = (didSucceed, filePath, size) => { finishRecording = (didSucceed, filePath, size) => {

View File

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

View File

@ -190,7 +190,7 @@ class MessageBox extends Component {
}); });
} }
componentWillReceiveProps(nextProps) { UNSAFE_componentWillReceiveProps(nextProps) {
const { isFocused, editing, replying } = this.props; const { isFocused, editing, replying } = this.props;
if (!isFocused()) { if (!isFocused()) {
return; return;
@ -306,9 +306,9 @@ class MessageBox extends Component {
if (!isTextEmpty) { if (!isTextEmpty) {
try { try {
const { start, end } = this.component._lastNativeSelection; const { start, end } = this.component?.lastNativeSelection;
const cursor = Math.max(start, end); const cursor = Math.max(start, end);
const lastNativeText = this.component._lastNativeText || ''; const lastNativeText = this.component?.lastNativeText || '';
// matches if text either starts with '/' or have (@,#,:) then it groups whatever comes next of mention type // matches if text either starts with '/' or have (@,#,:) then it groups whatever comes next of mention type
const regexp = /(#|@|:|^\/)([a-z0-9._-]+)$/im; const regexp = /(#|@|:|^\/)([a-z0-9._-]+)$/im;
const result = lastNativeText.substr(0, cursor).match(regexp); const result = lastNativeText.substr(0, cursor).match(regexp);
@ -339,7 +339,7 @@ class MessageBox extends Component {
} }
const { trackingType } = this.state; const { trackingType } = this.state;
const msg = this.text; const msg = this.text;
const { start, end } = this.component._lastNativeSelection; const { start, end } = this.component?.lastNativeSelection;
const cursor = Math.max(start, end); const cursor = Math.max(start, end);
const regexp = /([a-z0-9._-]+)$/im; const regexp = /([a-z0-9._-]+)$/im;
const result = msg.substr(0, cursor).replace(regexp, ''); const result = msg.substr(0, cursor).replace(regexp, '');
@ -383,8 +383,8 @@ class MessageBox extends Component {
let newText = ''; let newText = '';
// if messagebox has an active cursor // if messagebox has an active cursor
if (this.component && this.component._lastNativeSelection) { if (this.component?.lastNativeSelection) {
const { start, end } = this.component._lastNativeSelection; const { start, end } = this.component.lastNativeSelection;
const cursor = Math.max(start, end); const cursor = Math.max(start, end);
newText = `${ text.substr(0, cursor) }${ emoji }${ text.substr(cursor) }`; newText = `${ text.substr(0, cursor) }${ emoji }${ text.substr(cursor) }`;
} else { } 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 line = { backgroundColor: themes[theme].borderColor };
const text = { color: themes[theme].auxiliaryText }; const text = { color: themes[theme].auxiliaryText };
return ( return (
@ -36,8 +36,8 @@ const DateSeparator = React.memo(({ theme }) => {
); );
}); });
DateSeparator.propTypes = { OrSeparator.propTypes = {
theme: PropTypes.string 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 { Image, StyleSheet } from 'react-native';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { CustomIcon } from '../lib/Icons'; import { CustomIcon } from '../lib/Icons';
import { themes } from '../constants/colors'; import { STATUS_COLORS, themes } from '../constants/colors';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
style: { style: {
@ -15,7 +15,7 @@ const styles = StyleSheet.create({
}); });
const RoomTypeIcon = React.memo(({ const RoomTypeIcon = React.memo(({
type, size, isGroupChat, style, theme type, size, isGroupChat, status, style, theme
}) => { }) => {
if (!type) { if (!type) {
return null; return null;
@ -36,7 +36,7 @@ const RoomTypeIcon = React.memo(({
} }
return <CustomIcon name='at' size={13} style={[styles.style, styles.discussion, { color }]} />; return <CustomIcon name='at' size={13} style={[styles.style, styles.discussion, { color }]} />;
} if (type === 'l') { } 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 }]} />; return <Image source={{ uri: 'lock' }} style={[styles.style, style, { width: size, height: size, tintColor: color }]} />;
}); });
@ -45,6 +45,7 @@ RoomTypeIcon.propTypes = {
theme: PropTypes.string, theme: PropTypes.string,
type: PropTypes.string, type: PropTypes.string,
isGroupChat: PropTypes.bool, isGroupChat: PropTypes.bool,
status: PropTypes.string,
size: PropTypes.number, size: PropTypes.number,
style: PropTypes.object style: PropTypes.object
}; };

View File

@ -64,8 +64,10 @@ export default class RCTextInput extends React.PureComponent {
inputRef: PropTypes.func, inputRef: PropTypes.func,
testID: PropTypes.string, testID: PropTypes.string,
iconLeft: PropTypes.string, iconLeft: PropTypes.string,
iconRight: PropTypes.string,
placeholder: PropTypes.string, placeholder: PropTypes.string,
left: PropTypes.element, left: PropTypes.element,
onIconRightPress: PropTypes.func,
theme: PropTypes.string 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() { get iconPassword() {
const { showPassword } = this.state; const { showPassword } = this.state;
const { testID, theme } = this.props; const { testID, theme } = this.props;
@ -117,7 +132,7 @@ export default class RCTextInput extends React.PureComponent {
render() { render() {
const { showPassword } = this.state; const { showPassword } = this.state;
const { 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; } = this.props;
const { dangerColor } = themes[theme]; const { dangerColor } = themes[theme];
return ( return (
@ -140,7 +155,7 @@ export default class RCTextInput extends React.PureComponent {
style={[ style={[
styles.input, styles.input,
iconLeft && styles.inputIconLeft, iconLeft && styles.inputIconLeft,
secureTextEntry && styles.inputIconRight, (secureTextEntry || iconRight) && styles.inputIconRight,
{ {
backgroundColor: themes[theme].backgroundColor, backgroundColor: themes[theme].backgroundColor,
borderColor: themes[theme].separatorColor, borderColor: themes[theme].separatorColor,
@ -165,6 +180,7 @@ export default class RCTextInput extends React.PureComponent {
{...inputProps} {...inputProps}
/> />
{iconLeft ? this.iconLeft : null} {iconLeft ? this.iconLeft : null}
{iconRight ? this.iconRight : null}
{secureTextEntry ? this.iconPassword : null} {secureTextEntry ? this.iconPassword : null}
{loading ? this.loading : null} {loading ? this.loading : null}
{left} {left}

View File

@ -92,7 +92,7 @@ const TwoFactor = React.memo(({ theme, split }) => {
isVisible={visible} isVisible={visible}
hideModalContentWhileAnimating 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 }]}> <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> <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} {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} keyboardType={method?.keyboardType}
secureTextEntry={method?.secureTextEntry} secureTextEntry={method?.secureTextEntry}
error={data.invalid && { error: 'totp-invalid', reason: I18n.t('Code_or_password_invalid') }} 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>} {isEmail && <Text style={[styles.sendEmail, { color }]} onPress={sendEmail}>{I18n.t('Send_me_the_code_again')}</Text>}
<View style={styles.buttonContainer}> <View style={styles.buttonContainer}>
@ -123,6 +124,7 @@ const TwoFactor = React.memo(({ theme, split }) => {
style={styles.button} style={styles.button}
onPress={onSubmit} onPress={onSubmit}
theme={theme} theme={theme}
testID='two-factor-send'
/> />
</View> </View>
</View> </View>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -8,7 +8,7 @@ import Video from './Video';
import Reply from './Reply'; import Reply from './Reply';
const Attachments = React.memo(({ const Attachments = React.memo(({
attachments, timeFormat, user, baseUrl, showAttachment, getCustomEmoji, theme attachments, timeFormat, showAttachment, getCustomEmoji, theme
}) => { }) => {
if (!attachments || attachments.length === 0) { if (!attachments || attachments.length === 0) {
return null; return null;
@ -16,25 +16,23 @@ const Attachments = React.memo(({
return attachments.map((file, index) => { return attachments.map((file, index) => {
if (file.image_url) { 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) { 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) { 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 // 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); }, (prevProps, nextProps) => isEqual(prevProps.attachments, nextProps.attachments) && prevProps.theme === nextProps.theme);
Attachments.propTypes = { Attachments.propTypes = {
attachments: PropTypes.array, attachments: PropTypes.array,
timeFormat: PropTypes.string, timeFormat: PropTypes.string,
user: PropTypes.object,
baseUrl: PropTypes.string,
showAttachment: PropTypes.func, showAttachment: PropTypes.func,
getCustomEmoji: PropTypes.func, getCustomEmoji: PropTypes.func,
theme: PropTypes.string theme: PropTypes.string

View File

@ -3,18 +3,31 @@ import PropTypes from 'prop-types';
import { import {
View, StyleSheet, Text, Easing, Dimensions View, StyleSheet, Text, Easing, Dimensions
} from 'react-native'; } from 'react-native';
import Video from 'react-native-video'; import { Audio } from 'expo-av';
import Slider from '@react-native-community/slider'; import Slider from '@react-native-community/slider';
import moment from 'moment'; import moment from 'moment';
import equal from 'deep-equal'; import equal from 'deep-equal';
import Touchable from 'react-native-platform-touchable'; import { activateKeepAwake, deactivateKeepAwake } from 'expo-keep-awake';
import Touchable from './Touchable';
import Markdown from '../markdown'; import Markdown from '../markdown';
import { CustomIcon } from '../../lib/Icons'; import { CustomIcon } from '../../lib/Icons';
import sharedStyles from '../../views/Styles'; import sharedStyles from '../../views/Styles';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import { isAndroid, isIOS } from '../../utils/deviceInfo'; import { isAndroid, isIOS } from '../../utils/deviceInfo';
import { withSplit } from '../../split'; 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({ const styles = StyleSheet.create({
audioContainer: { audioContainer: {
@ -31,6 +44,9 @@ const styles = StyleSheet.create({
alignItems: 'center', alignItems: 'center',
backgroundColor: 'transparent' backgroundColor: 'transparent'
}, },
audioLoading: {
marginHorizontal: 8
},
slider: { slider: {
flex: 1 flex: 1
}, },
@ -51,29 +67,36 @@ const sliderAnimationConfig = {
delay: 0 delay: 0
}; };
const Button = React.memo(({ paused, onPress, theme }) => ( const Button = React.memo(({
loading, paused, onPress, theme
}) => (
<Touchable <Touchable
style={styles.playPauseButton} style={styles.playPauseButton}
onPress={onPress} onPress={onPress}
hitSlop={BUTTON_HIT_SLOP} hitSlop={BUTTON_HIT_SLOP}
background={Touchable.SelectableBackgroundBorderless()} 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> </Touchable>
)); ));
Button.propTypes = { Button.propTypes = {
loading: PropTypes.bool,
paused: PropTypes.bool, paused: PropTypes.bool,
theme: PropTypes.string, theme: PropTypes.string,
onPress: PropTypes.func onPress: PropTypes.func
}; };
Button.displayName = 'MessageAudioButton'; Button.displayName = 'MessageAudioButton';
class Audio extends React.Component { class MessageAudio extends React.Component {
static contextType = MessageContext;
static propTypes = { static propTypes = {
file: PropTypes.object.isRequired, file: PropTypes.object.isRequired,
baseUrl: PropTypes.string.isRequired,
user: PropTypes.object.isRequired,
theme: PropTypes.string, theme: PropTypes.string,
split: PropTypes.bool, split: PropTypes.bool,
getCustomEmoji: PropTypes.func getCustomEmoji: PropTypes.func
@ -81,18 +104,39 @@ class Audio extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
const { baseUrl, file, user } = props;
this.state = { this.state = {
loading: false,
currentTime: 0, currentTime: 0,
duration: 0, duration: 0,
paused: true, paused: true
uri: `${ baseUrl }${ file.audio_url }?rc_uid=${ user.id }&rc_token=${ user.token }`
}; };
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) { shouldComponentUpdate(nextProps, nextState) {
const { const {
currentTime, duration, paused, uri currentTime, duration, paused, loading
} = this.state; } = this.state;
const { file, split, theme } = this.props; const { file, split, theme } = this.props;
if (nextProps.theme !== theme) { if (nextProps.theme !== theme) {
@ -107,58 +151,108 @@ class Audio extends React.Component {
if (nextState.paused !== paused) { if (nextState.paused !== paused) {
return true; return true;
} }
if (nextState.uri !== uri) {
return true;
}
if (!equal(nextProps.file, file)) { if (!equal(nextProps.file, file)) {
return true; return true;
} }
if (nextProps.split !== split) { if (nextProps.split !== split) {
return true; return true;
} }
if (nextState.loading !== loading) {
return true;
}
return false; 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) => { 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) => { onProgress = (data) => {
const { duration } = this.state; const { duration } = this.state;
if (data.currentTime <= duration) { const currentTime = data.positionMillis / 1000;
this.setState({ currentTime: data.currentTime }); if (currentTime <= duration) {
this.setState({ currentTime });
} }
} }
onEnd = () => { onEnd = async(data) => {
if (data.didJustFinish) {
try {
await this.sound.stopAsync();
this.setState({ paused: true, currentTime: 0 }); this.setState({ paused: true, currentTime: 0 });
requestAnimationFrame(() => { } catch {
this.player.seek(0); // do nothing
}); }
}
} }
get duration() { get duration() {
const { duration } = this.state; const { currentTime, duration } = this.state;
return formatTime(duration); return formatTime(currentTime || duration);
} }
setRef = ref => this.player = ref;
togglePlayPause = () => { togglePlayPause = () => {
const { paused } = this.state; 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() { render() {
const { const {
uri, paused, currentTime, duration loading, paused, currentTime, duration
} = this.state; } = this.state;
const { const {
user, baseUrl, file, getCustomEmoji, split, theme file, getCustomEmoji, split, theme
} = this.props; } = this.props;
const { description } = file; const { description } = file;
const { baseUrl, user } = this.context;
if (!baseUrl) { if (!baseUrl) {
return null; return null;
@ -173,17 +267,7 @@ class Audio extends React.Component {
split && sharedStyles.tabletContent split && sharedStyles.tabletContent
]} ]}
> >
<Video <Button loading={loading} paused={paused} onPress={this.togglePlayPause} theme={theme} />
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} />
<Slider <Slider
style={styles.slider} style={styles.slider}
value={currentTime} 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 { View, Text } from 'react-native';
import Touchable from 'react-native-platform-touchable';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import Touchable from './Touchable';
import { CustomIcon } from '../../lib/Icons'; import { CustomIcon } from '../../lib/Icons';
import styles from './styles'; import styles from './styles';
import { BUTTON_HIT_SLOP } from './utils'; import { BUTTON_HIT_SLOP } from './utils';
import I18n from '../../i18n'; import I18n from '../../i18n';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import MessageContext from './Context';
const Broadcast = React.memo(({ const Broadcast = React.memo(({
author, user, broadcast, replyBroadcast, theme author, broadcast, theme
}) => { }) => {
const { user, replyBroadcast } = useContext(MessageContext);
const isOwn = author._id === user.id; const isOwn = author._id === user.id;
if (broadcast && !isOwn) { if (broadcast && !isOwn) {
return ( return (
@ -36,10 +38,8 @@ const Broadcast = React.memo(({
Broadcast.propTypes = { Broadcast.propTypes = {
author: PropTypes.object, author: PropTypes.object,
user: PropTypes.object,
broadcast: PropTypes.bool, broadcast: PropTypes.bool,
theme: PropTypes.string, theme: PropTypes.string
replyBroadcast: PropTypes.func
}; };
Broadcast.displayName = 'MessageBroadcast'; Broadcast.displayName = 'MessageBroadcast';

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,16 +1,18 @@
import React from 'react'; import React, { useContext } from 'react';
import Touchable from 'react-native-platform-touchable';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import Touchable from './Touchable';
import { CustomIcon } from '../../lib/Icons'; import { CustomIcon } from '../../lib/Icons';
import styles from './styles'; import styles from './styles';
import { BUTTON_HIT_SLOP } from './utils'; import { BUTTON_HIT_SLOP } from './utils';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import MessageContext from './Context';
const MessageError = React.memo(({ hasError, onErrorPress, theme }) => { const MessageError = React.memo(({ hasError, theme }) => {
if (!hasError) { if (!hasError) {
return null; return null;
} }
const { onErrorPress } = useContext(MessageContext);
return ( return (
<Touchable onPress={onErrorPress} style={styles.errorButton} hitSlop={BUTTON_HIT_SLOP}> <Touchable onPress={onErrorPress} style={styles.errorButton} hitSlop={BUTTON_HIT_SLOP}>
<CustomIcon name='warning' color={themes[theme].dangerColor} size={18} /> <CustomIcon name='warning' color={themes[theme].dangerColor} size={18} />
@ -20,7 +22,6 @@ const MessageError = React.memo(({ hasError, onErrorPress, theme }) => {
MessageError.propTypes = { MessageError.propTypes = {
hasError: PropTypes.bool, hasError: PropTypes.bool,
onErrorPress: PropTypes.func,
theme: PropTypes.string theme: PropTypes.string
}; };
MessageError.displayName = 'MessageError'; 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 { View, Text } from 'react-native';
import Touchable from 'react-native-platform-touchable';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import Touchable from './Touchable';
import { CustomIcon } from '../../lib/Icons'; import { CustomIcon } from '../../lib/Icons';
import styles from './styles'; import styles from './styles';
import Emoji from './Emoji'; import Emoji from './Emoji';
import { BUTTON_HIT_SLOP } from './utils'; import { BUTTON_HIT_SLOP } from './utils';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import { withTheme } from '../../theme'; 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 <Touchable
onPress={reactionInit} onPress={reactionInit}
key='message-add-reaction' key='message-add-reaction'
@ -23,11 +26,15 @@ const AddReaction = React.memo(({ reactionInit, theme }) => (
<CustomIcon name='add-reaction' size={21} color={themes[theme].tintColor} /> <CustomIcon name='add-reaction' size={21} color={themes[theme].tintColor} />
</View> </View>
</Touchable> </Touchable>
)); );
});
const Reaction = React.memo(({ 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; const reacted = reaction.usernames.findIndex(item => item === user.username) !== -1;
return ( return (
<Touchable <Touchable
@ -54,7 +61,7 @@ const Reaction = React.memo(({
}); });
const Reactions = React.memo(({ const Reactions = React.memo(({
reactions, user, baseUrl, onReactionPress, reactionInit, onReactionLongPress, getCustomEmoji, theme reactions, getCustomEmoji, theme
}) => { }) => {
if (!Array.isArray(reactions) || reactions.length === 0) { if (!Array.isArray(reactions) || reactions.length === 0) {
return null; return null;
@ -65,25 +72,17 @@ const Reactions = React.memo(({
<Reaction <Reaction
key={reaction.emoji} key={reaction.emoji}
reaction={reaction} reaction={reaction}
user={user}
baseUrl={baseUrl}
onReactionLongPress={onReactionLongPress}
onReactionPress={onReactionPress}
getCustomEmoji={getCustomEmoji} getCustomEmoji={getCustomEmoji}
theme={theme} theme={theme}
/> />
))} ))}
<AddReaction reactionInit={reactionInit} theme={theme} /> <AddReaction theme={theme} />
</View> </View>
); );
}); });
Reaction.propTypes = { Reaction.propTypes = {
reaction: PropTypes.object, reaction: PropTypes.object,
user: PropTypes.object,
baseUrl: PropTypes.string,
onReactionPress: PropTypes.func,
onReactionLongPress: PropTypes.func,
getCustomEmoji: PropTypes.func, getCustomEmoji: PropTypes.func,
theme: PropTypes.string theme: PropTypes.string
}; };
@ -91,18 +90,12 @@ Reaction.displayName = 'MessageReaction';
Reactions.propTypes = { Reactions.propTypes = {
reactions: PropTypes.oneOfType([PropTypes.array, PropTypes.object]), 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, getCustomEmoji: PropTypes.func,
theme: PropTypes.string theme: PropTypes.string
}; };
Reactions.displayName = 'MessageReactions'; Reactions.displayName = 'MessageReactions';
AddReaction.propTypes = { AddReaction.propTypes = {
reactionInit: PropTypes.func,
theme: PropTypes.string theme: PropTypes.string
}; };
AddReaction.displayName = 'MessageAddReaction'; 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 { View, Text, StyleSheet } from 'react-native';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import moment from 'moment'; import moment from 'moment';
import Touchable from 'react-native-platform-touchable';
import isEqual from 'deep-equal'; import isEqual from 'deep-equal';
import Touchable from './Touchable';
import Markdown from '../markdown'; import Markdown from '../markdown';
import openLink from '../../utils/openLink'; import openLink from '../../utils/openLink';
import sharedStyles from '../../views/Styles'; import sharedStyles from '../../views/Styles';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import { withSplit } from '../../split'; import { withSplit } from '../../split';
import MessageContext from './Context';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
button: { button: {
@ -79,12 +80,13 @@ const Title = React.memo(({ attachment, timeFormat, theme }) => {
}); });
const Description = React.memo(({ const Description = React.memo(({
attachment, baseUrl, user, getCustomEmoji, theme attachment, getCustomEmoji, theme
}) => { }) => {
const text = attachment.text || attachment.title; const text = attachment.text || attachment.title;
if (!text) { if (!text) {
return null; return null;
} }
const { baseUrl, user } = useContext(MessageContext);
return ( return (
<Markdown <Markdown
msg={text} 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); }, (prevProps, nextProps) => isEqual(prevProps.attachment.fields, nextProps.attachment.fields) && prevProps.theme === nextProps.theme);
const Reply = React.memo(({ const Reply = React.memo(({
attachment, timeFormat, baseUrl, user, index, getCustomEmoji, split, theme attachment, timeFormat, index, getCustomEmoji, split, theme
}) => { }) => {
if (!attachment) { if (!attachment) {
return null; return null;
} }
const { baseUrl, user } = useContext(MessageContext);
const onPress = () => { const onPress = () => {
let url = attachment.title_link || attachment.author_link; let url = attachment.title_link || attachment.author_link;
@ -136,7 +139,10 @@ const Reply = React.memo(({
return; return;
} }
if (attachment.type === 'file') { 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); openLink(url, theme);
}; };
@ -160,8 +166,6 @@ const Reply = React.memo(({
<Description <Description
attachment={attachment} attachment={attachment}
timeFormat={timeFormat} timeFormat={timeFormat}
baseUrl={baseUrl}
user={user}
getCustomEmoji={getCustomEmoji} getCustomEmoji={getCustomEmoji}
theme={theme} theme={theme}
/> />
@ -174,8 +178,6 @@ const Reply = React.memo(({
Reply.propTypes = { Reply.propTypes = {
attachment: PropTypes.object, attachment: PropTypes.object,
timeFormat: PropTypes.string, timeFormat: PropTypes.string,
baseUrl: PropTypes.string,
user: PropTypes.object,
index: PropTypes.number, index: PropTypes.number,
theme: PropTypes.string, theme: PropTypes.string,
getCustomEmoji: PropTypes.func, getCustomEmoji: PropTypes.func,
@ -192,8 +194,6 @@ Title.displayName = 'MessageReplyTitle';
Description.propTypes = { Description.propTypes = {
attachment: PropTypes.object, attachment: PropTypes.object,
baseUrl: PropTypes.string,
user: PropTypes.object,
getCustomEmoji: PropTypes.func, getCustomEmoji: PropTypes.func,
theme: PropTypes.string 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 { import {
View, Text, StyleSheet, Clipboard View, Text, StyleSheet, Clipboard
} from 'react-native'; } from 'react-native';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import FastImage from 'react-native-fast-image'; import FastImage from 'react-native-fast-image';
import Touchable from 'react-native-platform-touchable';
import isEqual from 'lodash/isEqual'; import isEqual from 'lodash/isEqual';
import Touchable from './Touchable';
import openLink from '../../utils/openLink'; import openLink from '../../utils/openLink';
import sharedStyles from '../../views/Styles'; import sharedStyles from '../../views/Styles';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
@ -15,6 +15,7 @@ import { withSplit } from '../../split';
import { LISTENER } from '../Toast'; import { LISTENER } from '../Toast';
import EventEmitter from '../../utils/events'; import EventEmitter from '../../utils/events';
import I18n from '../../i18n'; import I18n from '../../i18n';
import MessageContext from './Context';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
button: { button: {
@ -52,10 +53,11 @@ const styles = StyleSheet.create({
} }
}); });
const UrlImage = React.memo(({ image, user, baseUrl }) => { const UrlImage = React.memo(({ image }) => {
if (!image) { if (!image) {
return null; return null;
} }
const { baseUrl, user } = useContext(MessageContext);
image = image.includes('http') ? image : `${ baseUrl }/${ image }?rc_uid=${ user.id }&rc_token=${ user.token }`; 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} />; return <FastImage source={{ uri: image }} style={styles.image} resizeMode={FastImage.resizeMode.cover} />;
}, (prevProps, nextProps) => prevProps.image === nextProps.image); }, (prevProps, nextProps) => prevProps.image === nextProps.image);
@ -79,7 +81,7 @@ const UrlContent = React.memo(({ title, description, theme }) => (
}); });
const Url = React.memo(({ const Url = React.memo(({
url, index, user, baseUrl, split, theme url, index, split, theme
}) => { }) => {
if (!url) { if (!url) {
return null; return null;
@ -109,7 +111,7 @@ const Url = React.memo(({
background={Touchable.Ripple(themes[theme].bannerBackground)} 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} /> <UrlContent title={url.title} description={url.description} theme={theme} />
</> </>
</Touchable> </Touchable>
@ -117,21 +119,19 @@ const Url = React.memo(({
}, (oldProps, newProps) => isEqual(oldProps.url, newProps.url) && oldProps.split === newProps.split && oldProps.theme === newProps.theme); }, (oldProps, newProps) => isEqual(oldProps.url, newProps.url) && oldProps.split === newProps.split && oldProps.theme === newProps.theme);
const Urls = React.memo(({ const Urls = React.memo(({
urls, user, baseUrl, split, theme urls, split, theme
}) => { }) => {
if (!urls || urls.length === 0) { if (!urls || urls.length === 0) {
return null; return null;
} }
return urls.map((url, index) => ( 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); }, (oldProps, newProps) => isEqual(oldProps.urls, newProps.urls) && oldProps.split === newProps.split && oldProps.theme === newProps.theme);
UrlImage.propTypes = { UrlImage.propTypes = {
image: PropTypes.string, image: PropTypes.string
user: PropTypes.object,
baseUrl: PropTypes.string
}; };
UrlImage.displayName = 'MessageUrlImage'; UrlImage.displayName = 'MessageUrlImage';
@ -145,8 +145,6 @@ UrlContent.displayName = 'MessageUrlContent';
Url.propTypes = { Url.propTypes = {
url: PropTypes.object.isRequired, url: PropTypes.object.isRequired,
index: PropTypes.number, index: PropTypes.number,
user: PropTypes.object,
baseUrl: PropTypes.string,
theme: PropTypes.string, theme: PropTypes.string,
split: PropTypes.bool split: PropTypes.bool
}; };
@ -154,8 +152,6 @@ Url.displayName = 'MessageUrl';
Urls.propTypes = { Urls.propTypes = {
urls: PropTypes.array, urls: PropTypes.array,
user: PropTypes.object,
baseUrl: PropTypes.string,
theme: PropTypes.string, theme: PropTypes.string,
split: PropTypes.bool split: PropTypes.bool
}; };

View File

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

View File

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

View File

@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import { KeyboardUtils } from 'react-native-keyboard-input'; import { KeyboardUtils } from 'react-native-keyboard-input';
import Message from './Message'; import Message from './Message';
import MessageContext from './Context';
import debounce from '../../utils/debounce'; import debounce from '../../utils/debounce';
import { SYSTEM_MESSAGES, getMessageTranslation } from './utils'; import { SYSTEM_MESSAGES, getMessageTranslation } from './utils';
import messagesStatus from '../../constants/messagesStatus'; 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 item, user, style, archived, baseUrl, useRealName, broadcast, fetchThreadName, customThreadTimeFormat, showAttachment, timeFormat, isReadReceiptEnabled, autoTranslateRoom, autoTranslateLanguage, navToRoomInfo, getCustomEmoji, isThreadRoom, callJitsi, blockAction, rid, theme
} = this.props; } = this.props;
const { const {
id, msg, ts, attachments, urls, reactions, t, avatar, u, alias, editedBy, role, drid, dcount, dlm, tmid, tcount, tlm, tmsg, mentions, channels, unread, 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; } = item;
let message = msg; let message = msg;
@ -240,6 +241,20 @@ class MessageContainer extends React.Component {
} }
return ( 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 <Message
id={id} id={id}
msg={message} msg={message}
@ -253,13 +268,12 @@ class MessageContainer extends React.Component {
reactions={reactions} reactions={reactions}
alias={alias} alias={alias}
avatar={avatar} avatar={avatar}
user={user} emoji={emoji}
timeFormat={timeFormat} timeFormat={timeFormat}
customThreadTimeFormat={customThreadTimeFormat} customThreadTimeFormat={customThreadTimeFormat}
style={style} style={style}
archived={archived} archived={archived}
broadcast={broadcast} broadcast={broadcast}
baseUrl={baseUrl}
useRealName={useRealName} useRealName={useRealName}
isReadReceiptEnabled={isReadReceiptEnabled} isReadReceiptEnabled={isReadReceiptEnabled}
unread={unread} unread={unread}
@ -282,14 +296,6 @@ class MessageContainer extends React.Component {
isInfo={this.isInfo} isInfo={this.isInfo}
isTemp={this.isTemp} isTemp={this.isTemp}
hasError={this.hasError} 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} showAttachment={showAttachment}
getCustomEmoji={getCustomEmoji} getCustomEmoji={getCustomEmoji}
navToRoomInfo={navToRoomInfo} navToRoomInfo={navToRoomInfo}
@ -297,6 +303,7 @@ class MessageContainer extends React.Component {
blockAction={blockAction} blockAction={blockAction}
theme={theme} 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-email': 'E-Mail konnte nicht geändert werden',
'error-could-not-change-name': 'Name 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-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-delete-protected-role': 'Eine geschützte Rolle kann nicht gelöscht werden',
'error-department-not-found': 'Abteilung nicht gefunden', 'error-department-not-found': 'Abteilung nicht gefunden',
'error-direct-message-file-upload-not-allowed': 'Dateifreigabe in direkten Nachrichten nicht zulässig', '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-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-email-send-failed': 'Fehler beim Versuch, eine E-Mail zu senden: {{message}}',
'error-save-image': 'Fehler beim Speichern des Bildes', 'error-save-image': 'Fehler beim Speichern des Bildes',
'error-save-video': 'Fehler beim Speichern des Videos',
'error-field-unavailable': '{{field}} wird bereits verwendet :(', 'error-field-unavailable': '{{field}} wird bereits verwendet :(',
'error-file-too-large': 'Datei ist zu groß', 'error-file-too-large': 'Datei ist zu groß',
'error-importer-not-defined': 'Der Import wurde nicht korrekt definiert, es fehlt die Importklasse.', 'error-importer-not-defined': 'Der Import wurde nicht korrekt definiert, es fehlt die Importklasse.',
@ -81,12 +83,14 @@ export default {
Activity: 'Aktivität', Activity: 'Aktivität',
Add_Reaction: 'Reaktion hinzufügen', Add_Reaction: 'Reaktion hinzufügen',
Add_Server: 'Server hinzufügen', Add_Server: 'Server hinzufügen',
Add_users: 'Nutzer hinzufügen', Add_users: 'Benutzer hinzufügen',
Admin_Panel: 'Admin-Panel', Admin_Panel: 'Admin-Panel',
Agent: 'Agent',
Alert: 'Benachrichtigung', Alert: 'Benachrichtigung',
alert: 'Benachrichtigung', alert: 'Benachrichtigung',
alerts: 'Benachrichtigungen', alerts: 'Benachrichtigungen',
All_users_in_the_channel_can_write_new_messages: 'Alle Benutzer im Kanal können neue Nachrichten schreiben', 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: 'Alles',
All_Messages: 'Alle Nachrichten', All_Messages: 'Alle Nachrichten',
Allow_Reactions: 'Reaktionen zulassen', Allow_Reactions: 'Reaktionen zulassen',
@ -130,12 +134,15 @@ export default {
Click_to_join: 'Klicken um teilzunehmen!', Click_to_join: 'Klicken um teilzunehmen!',
Close: 'Schließen', Close: 'Schließen',
Close_emoji_selector: 'Schließen Sie die Emoji-Auswahl', Close_emoji_selector: 'Schließen Sie die Emoji-Auswahl',
Closing_chat: 'Chat schließen',
Change_language_loading: 'Ändere Sprache.', Change_language_loading: 'Ändere Sprache.',
Chat_closed_by_agent: 'Chat durch den Agenten geschlossen',
Choose: 'Wählen', Choose: 'Wählen',
Choose_from_library: 'Aus der Bibliothek auswählen', Choose_from_library: 'Aus der Bibliothek auswählen',
Choose_file: 'Datei auswählen', Choose_file: 'Datei auswählen',
Choose_where_you_want_links_be_opened: 'Entscheide, wie Links geöffnet werden sollen', Choose_where_you_want_links_be_opened: 'Entscheide, wie Links geöffnet werden sollen',
Code: 'Code', Code: 'Code',
Code_or_password_invalid: 'Code oder Passwort sind falsch',
Collaborative: 'Kollaborativ', Collaborative: 'Kollaborativ',
Confirm: 'Bestätigen', Confirm: 'Bestätigen',
Connect: 'Verbinden', Connect: 'Verbinden',
@ -147,6 +154,7 @@ export default {
Continue_with: 'Weitermachen mit', Continue_with: 'Weitermachen mit',
Copied_to_clipboard: 'In die Zwischenablage kopiert!', Copied_to_clipboard: 'In die Zwischenablage kopiert!',
Copy: 'Kopieren', Copy: 'Kopieren',
Conversation: 'Konversationen',
Permalink: 'Permalink', Permalink: 'Permalink',
Certificate_password: 'Zertifikats-Passwort', Certificate_password: 'Zertifikats-Passwort',
Clear_cache: 'Lokalen Server-Cache leeren', 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?', Whats_the_password_for_your_certificate: 'Wie lautet das Passwort für Ihr Zertifikat?',
Create_account: 'Ein Konto erstellen', Create_account: 'Ein Konto erstellen',
Create_Channel: 'Kanal erstellen', Create_Channel: 'Kanal erstellen',
Create_Direct_Messages: 'Direkt-Nachricht erstellen',
Create_Discussion: 'Diskussion erstellen',
Created_snippet: 'Erstellt ein Snippet', Created_snippet: 'Erstellt ein Snippet',
Create_a_new_workspace: 'Erstellen Sie einen neuen Arbeitsbereich', Create_a_new_workspace: 'Erstellen Sie einen neuen Arbeitsbereich',
Create: 'Erstellen', Create: 'Erstellen',
Custom_Status: 'eigener Status',
Dark: 'Dunkel', Dark: 'Dunkel',
Dark_level: 'Dunkelstufe', Dark_level: 'Dunkelstufe',
Default: 'Standard', Default: 'Standard',
Default_browser: 'Standard-Browser', 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.', 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', Delete: 'Löschen',
DELETE: 'LÖSCHEN', DELETE: 'LÖSCHEN',
@ -173,17 +185,23 @@ export default {
Direct_Messages: 'Direkte Nachrichten', Direct_Messages: 'Direkte Nachrichten',
Disable_notifications: 'Benachrichtigungen deaktiveren', Disable_notifications: 'Benachrichtigungen deaktiveren',
Discussions: 'Diskussionen', 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_have_a_certificate: 'Haben Sie ein Zertifikat?',
Do_you_really_want_to_key_this_room_question_mark: 'Möchten Sie diesen Raum wirklich {{key}}?', Do_you_really_want_to_key_this_room_question_mark: 'Möchten Sie diesen Raum wirklich {{key}}?',
edit: 'bearbeiten', edit: 'bearbeiten',
edited: 'bearbeitet', edited: 'bearbeitet',
Edit: 'Bearbeiten', Edit: 'Bearbeiten',
Edit_Status: 'Status ändern',
Edit_Invite: 'Einladung bearbeiten', Edit_Invite: 'Einladung bearbeiten',
Email_or_password_field_is_empty: 'Das E-Mail- oder Passwortfeld ist leer', Email_or_password_field_is_empty: 'Das E-Mail- oder Passwortfeld ist leer',
Email: 'Email', Email: 'Email',
EMAIL: 'EMAIL', EMAIL: 'EMAIL',
email: 'Email', email: 'Email',
Empty_title: 'leerer Titel',
Enable_Auto_Translate: 'Automatische Übersetzung aktivieren', Enable_Auto_Translate: 'Automatische Übersetzung aktivieren',
Enable_notifications: 'Benachrichtigungen aktivieren', Enable_notifications: 'Benachrichtigungen aktivieren',
Everyone_can_access_this_channel: 'Jeder kann auf diesen Kanal zugreifen', 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_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',
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', Full_table: 'Klicken um die ganze Tabelle anzuzeigen',
Generate_New_Link: 'Neuen Link erstellen', Generate_New_Link: 'Neuen Link erstellen',
Group_by_favorites: 'Nach Favoriten gruppieren', Group_by_favorites: 'Nach Favoriten gruppieren',
@ -210,19 +232,20 @@ export default {
Has_left_the_channel: 'Hat den Kanal verlassen', Has_left_the_channel: 'Hat den Kanal verlassen',
Hide_System_Messages: 'Systemnachrichten verstecken', Hide_System_Messages: 'Systemnachrichten verstecken',
Hide_type_messages: 'Verstecke "{{type}}"-Nachrichten', Hide_type_messages: 'Verstecke "{{type}}"-Nachrichten',
Message_HideType_uj: 'Nutzer beigetreten', Message_HideType_uj: 'Benutzer beigetreten',
Message_HideType_ul: 'Nutzer verlassen', Message_HideType_ul: 'Benutzer verlassen',
Message_HideType_ru: 'Nutzer entfernt', Message_HideType_ru: 'Benutzer entfernt',
Message_HideType_au: 'Nutzer hinzugefügt', Message_HideType_au: 'Benutzer hinzugefügt',
Message_HideType_mute_unmute: 'Nutzer stummgeschaltet / freigegeben', Message_HideType_mute_unmute: 'Benutzer stummgeschaltet / freigegeben',
Message_HideType_r: 'Raumname geändert', 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_wm: 'Willkommen',
Message_HideType_rm: 'Nachricht entfernt', Message_HideType_rm: 'Nachricht entfernt',
Message_HideType_subscription_role_added: 'Rolle wurde gesetzt', Message_HideType_subscription_role_added: 'Rolle wurde gesetzt',
Message_HideType_subscription_role_removed: 'Rolle nicht länger definiert', Message_HideType_subscription_role_removed: 'Rolle nicht länger definiert',
Message_HideType_room_archived: 'Raum archiviert', Message_HideType_room_archived: 'Raum archiviert',
Message_HideType_room_unarchived: 'Raum nicht mehr archiviert', Message_HideType_room_unarchived: 'Raum nicht mehr archiviert',
IP: 'IP',
In_app: 'In-App-Browser', In_app: 'In-App-Browser',
IN_APP_AND_DESKTOP: 'IN-APP UND DESKTOP', 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.', 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', Invite: 'Einladen',
is_a_valid_RocketChat_instance: 'ist eine gültige Rocket.Chat-Instanz', 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_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_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}}.', 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_Link: 'Einladungs-Link',
Invite_users: 'Benutzer einladen', Invite_users: 'Benutzer einladen',
Join: 'Beitreten', 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', Just_invited_people_can_access_this_channel: 'Nur eingeladene Personen können auf diesen Kanal zugreifen',
Language: 'Sprache', Language: 'Sprache',
last_message: 'letzte Nachricht', last_message: 'letzte Nachricht',
@ -246,12 +271,14 @@ export default {
Light: 'Hell', Light: 'Hell',
License: 'Lizenz', License: 'Lizenz',
Livechat: 'Live-Chat', Livechat: 'Live-Chat',
Livechat_edit: 'Livechat bearbeiten',
Login: 'Anmeldung', Login: 'Anmeldung',
Login_error: 'Ihre Zugangsdaten wurden abgelehnt! Bitte versuchen Sie es erneut.', Login_error: 'Ihre Zugangsdaten wurden abgelehnt! Bitte versuchen Sie es erneut.',
Login_with: 'Einloggen mit', Login_with: 'Einloggen mit',
Logging_out: 'Abmelden.', Logging_out: 'Abmelden.',
Logout: 'Abmelden', Logout: 'Abmelden',
Max_number_of_uses: 'Maximale Anzahl der Benutzungen', 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',
Members: 'Mitglieder', Members: 'Mitglieder',
Mentioned_Messages: 'Erwähnte Nachrichten', Mentioned_Messages: 'Erwähnte Nachrichten',
@ -277,6 +304,7 @@ export default {
N_users: '{{n}} Benutzer', N_users: '{{n}} Benutzer',
name: 'Name', name: 'Name',
Name: 'Name', Name: 'Name',
Navigation_history: 'Navigations-Verlauf',
Never: 'Niemals', Never: 'Niemals',
New_Message: 'Neue Nachricht', New_Message: 'Neue Nachricht',
New_Password: 'Neues Kennwort', New_Password: 'Neues Kennwort',
@ -303,20 +331,34 @@ export default {
Notifications: 'Benachrichtigungen', Notifications: 'Benachrichtigungen',
Notification_Duration: 'Benachrichtigungsdauer', Notification_Duration: 'Benachrichtigungsdauer',
Notification_Preferences: 'Benachrichtigungseinstellungen', Notification_Preferences: 'Benachrichtigungseinstellungen',
No_available_agents_to_transfer: 'Keine Agenten für den Transfer verfügbar',
Offline: 'Offline', Offline: 'Offline',
Oops: 'Hoppla!', 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_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', Online: 'Online',
Only_authorized_users_can_write_new_messages: 'Nur autorisierte Benutzer können neue Nachrichten schreiben', Only_authorized_users_can_write_new_messages: 'Nur autorisierte Benutzer können neue Nachrichten schreiben',
Open_emoji_selector: 'Öffne die Emoji-Auswahl', Open_emoji_selector: 'Öffne die Emoji-Auswahl',
Open_Source_Communication: 'Open-Source-Kommunikation', 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', Overwrites_the_server_configuration_and_use_room_config: 'Übergeht die Servereinstellungen und nutzt Einstellung für den Raum',
Password: 'Passwort', Password: 'Passwort',
Parent_channel_or_group: 'Übergeordneter Kanal oder Gruppe',
Permalink_copied_to_clipboard: 'Permalink in die Zwischenablage kopiert!', Permalink_copied_to_clipboard: 'Permalink in die Zwischenablage kopiert!',
Phone: 'Telefon',
Pin: 'Anheften', Pin: 'Anheften',
Pinned_Messages: 'Angeheftete Nachrichten', Pinned_Messages: 'Angeheftete Nachrichten',
pinned: 'angeheftet', pinned: 'angeheftet',
Pinned: 'Angeheftet', Pinned: 'Angeheftet',
Please_add_a_comment: 'Bitte Kommentar hinzufügen',
Please_enter_your_password: 'Bitte geben Sie Ihr Passwort ein', Please_enter_your_password: 'Bitte geben Sie Ihr Passwort ein',
Please_wait: 'Bitte warten.', Please_wait: 'Bitte warten.',
Preferences: 'Einstellungen', Preferences: 'Einstellungen',
@ -355,6 +397,7 @@ export default {
Reset_password: 'Passwort zurücksetzen', Reset_password: 'Passwort zurücksetzen',
resetting_password: 'Passwort zurücksetzen', resetting_password: 'Passwort zurücksetzen',
RESET: 'ZURÜCKSETZEN', RESET: 'ZURÜCKSETZEN',
Return: 'Zurück',
Review_app_title: 'Gefällt dir diese App?', Review_app_title: 'Gefällt dir diese App?',
Review_app_desc: 'Gib uns 5 Sterne im {{store}}', Review_app_desc: 'Gib uns 5 Sterne im {{store}}',
Review_app_yes: 'Sicher!', Review_app_yes: 'Sicher!',
@ -375,7 +418,8 @@ export default {
Room_name_changed: 'Raumname geändert in {{name}} von {{userBy}}', Room_name_changed: 'Raumname geändert in {{name}} von {{userBy}}',
SAVE: 'SPEICHERN', SAVE: 'SPEICHERN',
Save_Changes: 'Änderungen speichern', Save_Changes: 'Änderungen speichern',
Save: 'sparen', Save: 'speichern',
Saved: 'gespeichert',
saving_preferences: 'Präferenzen speichern', saving_preferences: 'Präferenzen speichern',
saving_profile: 'Profil speichern', saving_profile: 'Profil speichern',
saving_settings: 'Einstellungen speichern', saving_settings: 'Einstellungen speichern',
@ -388,17 +432,25 @@ export default {
Seconds: '{{second}} Sekunden', Seconds: '{{second}} Sekunden',
Select_Avatar: 'Wählen Sie einen Avatar aus', Select_Avatar: 'Wählen Sie einen Avatar aus',
Select_Server: 'Server auswählen', 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: 'Senden',
Send_audio_message: 'Audio-Nachricht senden', Send_audio_message: 'Audio-Nachricht senden',
Send_crash_report: 'Absturzbericht senden', Send_crash_report: 'Absturzbericht senden',
Send_message: 'Nachricht senden', Send_message: 'Nachricht senden',
Send_me_the_code_again: 'Den Code neu versenden',
Send_to: 'Senden an …', Send_to: 'Senden an …',
Sent_an_attachment: 'Sende einen Anhang', Sent_an_attachment: 'Sende einen Anhang',
Server: 'Server', Server: 'Server',
Servers: 'Server', Servers: 'Server',
Server_version: 'Server version: {{version}}', Server_version: 'Server version: {{version}}',
Set_username_subtitle: 'Der Benutzername wird verwendet, damit andere Personen Sie in Nachrichten erwähnen können', 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: 'Einstellungen',
Settings_succesfully_changed: 'Einstellungen erfolgreich geändert!', Settings_succesfully_changed: 'Einstellungen erfolgreich geändert!',
Share: 'Teilen', Share: 'Teilen',
@ -407,7 +459,7 @@ export default {
Show_more: 'Mehr anzeigen …', Show_more: 'Mehr anzeigen …',
Show_Unread_Counter: 'Zähler anzeigen', Show_Unread_Counter: 'Zähler anzeigen',
Show_Unread_Counter_Info: 'Anzahl der ungelesenen Nachrichten 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', Sign_Up: 'Anmelden',
Some_field_is_invalid_or_empty: 'Ein Feld ist ungültig oder leer', Some_field_is_invalid_or_empty: 'Ein Feld ist ungültig oder leer',
Sorting_by: 'Sortierung nach {{key}}', Sorting_by: 'Sortierung nach {{key}}',
@ -422,6 +474,7 @@ export default {
Started_call: 'Anruf gestartet von {{userBy}}', Started_call: 'Anruf gestartet von {{userBy}}',
Submit: 'einreichen', Submit: 'einreichen',
Table: 'Tabelle', Table: 'Tabelle',
Tags: 'Tags',
Take_a_photo: 'Foto aufnehmen', Take_a_photo: 'Foto aufnehmen',
Take_a_video: 'Video aufnehmen', Take_a_video: 'Video aufnehmen',
tap_to_change_status: 'Tippen um den Status zu ändern', tap_to_change_status: 'Tippen um den Status zu ändern',
@ -441,10 +494,10 @@ export default {
Translate: 'Übersetzen', Translate: 'Übersetzen',
Try_again: 'Versuchen Sie es nochmal', Try_again: 'Versuchen Sie es nochmal',
Two_Factor_Authentication: 'Zwei-Faktor-Authentifizierung', 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',
UNARCHIVE: 'WIEDERHERSTELLEN', UNARCHIVE: 'WIEDERHERSTELLEN',
Unblock_user: 'Nutzer entsperren', Unblock_user: 'Benutzer entsperren',
Unfavorite: 'Nicht mehr favorisieren', Unfavorite: 'Nicht mehr favorisieren',
Unfollowed_thread: 'Thread nicht mehr folgen', Unfollowed_thread: 'Thread nicht mehr folgen',
Unmute: 'Stummschaltung aufheben', Unmute: 'Stummschaltung aufheben',
@ -457,9 +510,10 @@ export default {
Updating: 'Aktualisierung …', Updating: 'Aktualisierung …',
Uploading: 'Hochladen', Uploading: 'Hochladen',
Upload_file_question_mark: 'Datei hochladen?', Upload_file_question_mark: 'Datei hochladen?',
User: 'Benutzer',
Users: 'Benutzer', Users: 'Benutzer',
User_added_by: 'Benutzer {{userAdded}} hinzugefügt von {{userBy}}', User_added_by: 'Benutzer {{userAdded}} hinzugefügt von {{userBy}}',
User_Info: 'Nutzerinfo', User_Info: 'Benutzerinfo',
User_has_been_key: 'Benutzer wurde {{key}}!', User_has_been_key: 'Benutzer wurde {{key}}!',
User_is_no_longer_role_by_: '{{user}} ist nicht länger {{role}} von {{userBy}}', User_is_no_longer_role_by_: '{{user}} ist nicht länger {{role}} von {{userBy}}',
User_muted_by: 'Benutzer {{userMuted}} von {{userBy}} stummgeschaltet', User_muted_by: 'Benutzer {{userMuted}} von {{userBy}} stummgeschaltet',
@ -471,18 +525,26 @@ export default {
Username: 'Benutzername', Username: 'Benutzername',
Username_or_email: 'Benutzername oder E-Mail-Adresse', Username_or_email: 'Benutzername oder E-Mail-Adresse',
Uses_server_configuration: 'Nutzt Servereinstellungen', 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', Validating: 'Validierung',
Registration_Succeeded: 'Registrierung erfolgreich!',
Verify: 'Überprüfen',
Verify_email_title: 'Registrierung erfolgreich!', 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_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', Video_call: 'Videoanruf',
View_Original: 'Original anzeigen', View_Original: 'Original anzeigen',
Voice_call: 'Sprachanruf', Voice_call: 'Sprachanruf',
Websocket_disabled: 'Websockets sind auf diesem Server nicht aktiviert.\n{{contact}}', Websocket_disabled: 'Websockets sind auf diesem Server nicht aktiviert.\n{{contact}}',
Welcome: 'Herzlich willkommen', Welcome: 'Herzlich willkommen',
What_are_you_doing_right_now: 'Was machst du gerade?',
Whats_your_2fa: 'Wie lautet Ihr 2FA-Code?', Whats_your_2fa: 'Wie lautet Ihr 2FA-Code?',
Without_Servers: 'Ohne Server', 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_Message: 'Rocket.Chat benötigt Zugriff auf Ihre Galerie um Bilder speichern zu können.',
Write_External_Permission: 'Galerie-Zugriff', Write_External_Permission: 'Galerie-Zugriff',
Yes: 'Ja',
Yes_action_it: 'Ja, {{action}}!', Yes_action_it: 'Ja, {{action}}!',
Yesterday: 'Gestern', Yesterday: 'Gestern',
You_are_in_preview_mode: 'Sie befinden sich im Vorschaumodus', You_are_in_preview_mode: 'Sie befinden sich im Vorschaumodus',
@ -495,11 +557,13 @@ export default {
You: 'Sie', You: 'Sie',
Logged_out_by_server: 'Du bist vom Server abgemeldet worden. Bitte melde dich wieder an.', 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.', 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_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__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_expire_on__date__: 'Dein Einladungs-Link wird am {{date}} ablaufen.',
Your_invite_link_will_never_expire: 'Dein Einladungs-Link wird niemals ablaufen.', Your_invite_link_will_never_expire: 'Dein Einladungs-Link wird niemals ablaufen.',
Your_workspace: 'Dein Arbeitsbereich',
Version_no: 'Version: {{version}}', Version_no: 'Version: {{version}}',
You_will_not_be_able_to_recover_this_message: 'Sie können diese Nachricht nicht wiederherstellen!', You_will_not_be_able_to_recover_this_message: 'Sie können diese Nachricht nicht wiederherstellen!',
Change_Language: 'Sprache ändern', 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.', You_will_be_logged_out_of_this_application: 'Du wirst in dieser Anwendung vom Server abgemeldet.',
Clear: 'Löschen', Clear: 'Löschen',
This_will_clear_all_your_offline_data: 'Dies wird deine Offline-Daten 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-domain-blacklisted': 'The email domain is blacklisted',
'error-email-send-failed': 'Error trying to send email: {{message}}', 'error-email-send-failed': 'Error trying to send email: {{message}}',
'error-save-image': 'Error while saving image', 'error-save-image': 'Error while saving image',
'error-save-video': 'Error while saving video',
'error-field-unavailable': '{{field}} is already in use :(', 'error-field-unavailable': '{{field}} is already in use :(',
'error-file-too-large': 'File is too large', 'error-file-too-large': 'File is too large',
'error-importer-not-defined': 'The importer was not defined correctly, it is missing the Import class.', '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_Server: 'Add Server',
Add_users: 'Add users', Add_users: 'Add users',
Admin_Panel: 'Admin Panel', Admin_Panel: 'Admin Panel',
Agent: 'Agent',
Alert: 'Alert', Alert: 'Alert',
alert: 'alert', alert: 'alert',
alerts: 'alerts', alerts: 'alerts',
@ -132,7 +134,9 @@ export default {
Click_to_join: 'Click to Join!', Click_to_join: 'Click to Join!',
Close: 'Close', Close: 'Close',
Close_emoji_selector: 'Close emoji selector', Close_emoji_selector: 'Close emoji selector',
Closing_chat: 'Closing chat',
Change_language_loading: 'Changing language.', Change_language_loading: 'Changing language.',
Chat_closed_by_agent: 'Chat closed by agent',
Choose: 'Choose', Choose: 'Choose',
Choose_from_library: 'Choose from library', Choose_from_library: 'Choose from library',
Choose_file: 'Choose file', Choose_file: 'Choose file',
@ -150,6 +154,7 @@ export default {
Continue_with: 'Continue with', Continue_with: 'Continue with',
Copied_to_clipboard: 'Copied to clipboard!', Copied_to_clipboard: 'Copied to clipboard!',
Copy: 'Copy', Copy: 'Copy',
Conversation: 'Conversation',
Permalink: 'Permalink', Permalink: 'Permalink',
Certificate_password: 'Certificate Password', Certificate_password: 'Certificate Password',
Clear_cache: 'Clear local server cache', Clear_cache: 'Clear local server cache',
@ -168,6 +173,7 @@ export default {
Default: 'Default', Default: 'Default',
Default_browser: 'Default browser', Default_browser: 'Default browser',
Delete_Room_Warning: 'Deleting a room will delete all messages posted within the room. This cannot be undone.', 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', Delete: 'Delete',
DELETE: 'DELETE', DELETE: 'DELETE',
@ -195,6 +201,7 @@ export default {
Email: 'Email', Email: 'Email',
EMAIL: 'EMAIL', EMAIL: 'EMAIL',
email: 'e-mail', email: 'e-mail',
Empty_title: 'Empty title',
Enable_Auto_Translate: 'Enable Auto-Translate', Enable_Auto_Translate: 'Enable Auto-Translate',
Enable_notifications: 'Enable notifications', Enable_notifications: 'Enable notifications',
Everyone_can_access_this_channel: 'Everyone can access this channel', 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_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 your password?',
Forgot_Password: 'Forgot 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', Full_table: 'Click to see full table',
Generate_New_Link: 'Generate New Link', Generate_New_Link: 'Generate New Link',
Group_by_favorites: 'Group favorites', Group_by_favorites: 'Group favorites',
@ -234,6 +245,7 @@ export default {
Message_HideType_subscription_role_removed: 'Role No Longer Defined', Message_HideType_subscription_role_removed: 'Role No Longer Defined',
Message_HideType_room_archived: 'Room Archived', Message_HideType_room_archived: 'Room Archived',
Message_HideType_room_unarchived: 'Room Unarchived', Message_HideType_room_unarchived: 'Room Unarchived',
IP: 'IP',
In_app: 'In-app', In_app: 'In-app',
IN_APP_AND_DESKTOP: 'IN-APP AND DESKTOP', 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', 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', Light: 'Light',
License: 'License', License: 'License',
Livechat: 'Livechat', Livechat: 'Livechat',
Livechat_edit: 'Livechat edit',
Login: 'Login', Login: 'Login',
Login_error: 'Your credentials were rejected! Please try again.', Login_error: 'Your credentials were rejected! Please try again.',
Login_with: 'Login with', Login_with: 'Login with',
@ -291,6 +304,7 @@ export default {
N_users: '{{n}} users', N_users: '{{n}} users',
name: 'name', name: 'name',
Name: 'Name', Name: 'Name',
Navigation_history: 'Navigation history',
Never: 'Never', Never: 'Never',
New_Message: 'New Message', New_Message: 'New Message',
New_Password: 'New Password', New_Password: 'New Password',
@ -317,6 +331,7 @@ export default {
Notifications: 'Notifications', Notifications: 'Notifications',
Notification_Duration: 'Notification Duration', Notification_Duration: 'Notification Duration',
Notification_Preferences: 'Notification Preferences', Notification_Preferences: 'Notification Preferences',
No_available_agents_to_transfer: 'No available agents to transfer',
Offline: 'Offline', Offline: 'Offline',
Oops: 'Oops!', 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.', 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_Source_Communication: 'Open Source Communication',
Open_your_authentication_app_and_enter_the_code: 'Open your authentication app and enter the code.', Open_your_authentication_app_and_enter_the_code: 'Open your authentication app and enter the code.',
OR: 'OR', OR: 'OR',
OS: 'OS',
Overwrites_the_server_configuration_and_use_room_config: 'Overwrites the server configuration and use room config', Overwrites_the_server_configuration_and_use_room_config: 'Overwrites the server configuration and use room config',
Password: 'Password', Password: 'Password',
Parent_channel_or_group: 'Parent channel or group', Parent_channel_or_group: 'Parent channel or group',
Permalink_copied_to_clipboard: 'Permalink copied to clipboard!', Permalink_copied_to_clipboard: 'Permalink copied to clipboard!',
Phone: 'Phone',
Pin: 'Pin', Pin: 'Pin',
Pinned_Messages: 'Pinned Messages', Pinned_Messages: 'Pinned Messages',
pinned: 'pinned', pinned: 'pinned',
Pinned: 'Pinned', Pinned: 'Pinned',
Please_add_a_comment: 'Please add a comment',
Please_enter_your_password: 'Please enter your password', Please_enter_your_password: 'Please enter your password',
Please_wait: 'Please wait.', Please_wait: 'Please wait.',
Preferences: 'Preferences', Preferences: 'Preferences',
@ -379,6 +397,7 @@ export default {
Reset_password: 'Reset password', Reset_password: 'Reset password',
resetting_password: 'resetting password', resetting_password: 'resetting password',
RESET: 'RESET', RESET: 'RESET',
Return: 'Return',
Review_app_title: 'Are you enjoying this app?', Review_app_title: 'Are you enjoying this app?',
Review_app_desc: 'Give us 5 stars on {{store}}', Review_app_desc: 'Give us 5 stars on {{store}}',
Review_app_yes: 'Sure!', Review_app_yes: 'Sure!',
@ -400,6 +419,7 @@ export default {
SAVE: 'SAVE', SAVE: 'SAVE',
Save_Changes: 'Save Changes', Save_Changes: 'Save Changes',
Save: 'Save', Save: 'Save',
Saved: 'Saved',
saving_preferences: 'saving preferences', saving_preferences: 'saving preferences',
saving_profile: 'saving profile', saving_profile: 'saving profile',
saving_settings: 'saving settings', saving_settings: 'saving settings',
@ -414,7 +434,9 @@ export default {
Select_Server: 'Select Server', Select_Server: 'Select Server',
Select_Users: 'Select Users', Select_Users: 'Select Users',
Select_a_Channel: 'Select a Channel', Select_a_Channel: 'Select a Channel',
Select_a_Department: 'Select a Department',
Select_an_option: 'Select an option', Select_an_option: 'Select an option',
Select_a_User: 'Select a User',
Send: 'Send', Send: 'Send',
Send_audio_message: 'Send audio message', Send_audio_message: 'Send audio message',
Send_crash_report: 'Send crash report', Send_crash_report: 'Send crash report',
@ -452,6 +474,7 @@ export default {
Started_call: 'Call started by {{userBy}}', Started_call: 'Call started by {{userBy}}',
Submit: 'Submit', Submit: 'Submit',
Table: 'Table', Table: 'Table',
Tags: 'Tags',
Take_a_photo: 'Take a photo', Take_a_photo: 'Take a photo',
Take_a_video: 'Take a video', Take_a_video: 'Take a video',
tap_to_change_status: 'tap to change status', tap_to_change_status: 'tap to change status',
@ -487,6 +510,7 @@ export default {
Updating: 'Updating...', Updating: 'Updating...',
Uploading: 'Uploading', Uploading: 'Uploading',
Upload_file_question_mark: 'Upload file?', Upload_file_question_mark: 'Upload file?',
User: 'User',
Users: 'Users', Users: 'Users',
User_added_by: 'User {{userAdded}} added by {{userBy}}', User_added_by: 'User {{userAdded}} added by {{userBy}}',
User_Info: 'User Info', User_Info: 'User Info',
@ -517,8 +541,10 @@ export default {
Whats_your_2fa: 'What\'s your 2FA code?', Whats_your_2fa: 'What\'s your 2FA code?',
Without_Servers: 'Without Servers', Without_Servers: 'Without Servers',
Workspaces: 'Workspaces', 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_Message: 'Rocket Chat needs access to your gallery so you can save images.',
Write_External_Permission: 'Gallery Permission', Write_External_Permission: 'Gallery Permission',
Yes: 'Yes',
Yes_action_it: 'Yes, {{action}} it!', Yes_action_it: 'Yes, {{action}} it!',
Yesterday: 'Yesterday', Yesterday: 'Yesterday',
You_are_in_preview_mode: 'You are in preview mode', 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.', You_will_be_logged_out_of_this_application: 'You will be logged out of this application.',
Clear: 'Clear', Clear: 'Clear',
This_will_clear_all_your_offline_data: 'This will clear all your offline data.', 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', 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_Reaction: 'Reagir',
Add_Server: 'Adicionar servidor', Add_Server: 'Adicionar servidor',
Add_users: 'Adicionar usuário', Add_users: 'Adicionar usuário',
Agent: 'Agente',
Alert: 'Alerta', Alert: 'Alerta',
alert: 'alerta', alert: 'alerta',
alerts: 'alertas', alerts: 'alertas',
@ -135,7 +136,9 @@ export default {
Click_to_join: 'Clique para participar!', Click_to_join: 'Clique para participar!',
Close: 'Fechar', Close: 'Fechar',
Close_emoji_selector: 'Fechar seletor de emojis', Close_emoji_selector: 'Fechar seletor de emojis',
Closing_chat: 'Fechando conversa',
Choose: 'Escolher', Choose: 'Escolher',
Chat_closed_by_agent: 'Conversa fechada por agente',
Choose_from_library: 'Escolha da biblioteca', Choose_from_library: 'Escolha da biblioteca',
Choose_file: 'Enviar arquivo', Choose_file: 'Enviar arquivo',
Choose_where_you_want_links_be_opened: 'Escolha onde deseja que os links sejam abertos', Choose_where_you_want_links_be_opened: 'Escolha onde deseja que os links sejam abertos',
@ -145,6 +148,7 @@ export default {
Confirm: 'Confirmar', Confirm: 'Confirmar',
Connect: 'Conectar', Connect: 'Conectar',
Connected: 'Conectado', Connected: 'Conectado',
Conversation: 'Conversação',
connecting_server: 'conectando no servidor', connecting_server: 'conectando no servidor',
Connecting: 'Conectando...', Connecting: 'Conectando...',
Continue_with: 'Entrar com', Continue_with: 'Entrar com',
@ -187,6 +191,7 @@ export default {
Email_or_password_field_is_empty: 'Email ou senha estão vazios', Email_or_password_field_is_empty: 'Email ou senha estão vazios',
Email: 'Email', Email: 'Email',
email: 'e-mail', email: 'e-mail',
Empty_title: 'Título vazio',
Enable_notifications: 'Habilitar notificações', Enable_notifications: 'Habilitar notificações',
Everyone_can_access_this_channel: 'Todos podem acessar este canal', Everyone_can_access_this_channel: 'Todos podem acessar este canal',
Error_uploading: 'Erro subindo', 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_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: 'Esqueceu sua senha?',
Forgot_Password: 'Esqueci minha 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', Full_table: 'Clique para ver a tabela completa',
Generate_New_Link: 'Gerar novo convite', Generate_New_Link: 'Gerar novo convite',
Group_by_favorites: 'Agrupar favoritos', Group_by_favorites: 'Agrupar favoritos',
@ -223,6 +232,7 @@ export default {
Message_HideType_subscription_role_removed: 'Papel removido', Message_HideType_subscription_role_removed: 'Papel removido',
Message_HideType_room_archived: 'Sala arquivada', Message_HideType_room_archived: 'Sala arquivada',
Message_HideType_room_unarchived: 'Sala desarquivada', Message_HideType_room_unarchived: 'Sala desarquivada',
IP: 'IP',
In_app: 'No app', In_app: 'No app',
Invisible: 'Invisível', Invisible: 'Invisível',
Invite: 'Convidar', Invite: 'Convidar',
@ -269,6 +279,7 @@ export default {
N_users: '{{n}} usuários', N_users: '{{n}} usuários',
name: 'nome', name: 'nome',
Name: 'Nome', Name: 'Nome',
Navigation_history: 'Histórico de navegação',
Never: 'Nunca', Never: 'Nunca',
New_in_RocketChat_question_mark: 'Novo no Rocket.Chat?', New_in_RocketChat_question_mark: 'Novo no Rocket.Chat?',
New_Message: 'Nova Mensagem', New_Message: 'Nova Mensagem',
@ -289,6 +300,7 @@ export default {
Notify_active_in_this_room: 'Notificar usuários ativos nesta sala', Notify_active_in_this_room: 'Notificar usuários ativos nesta sala',
Notify_all_in_this_room: 'Notificar todos nesta sala', Notify_all_in_this_room: 'Notificar todos nesta sala',
Not_RC_Server: 'Este não é um servidor Rocket.Chat.\n{{contact}}', 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', Offline: 'Offline',
Oops: 'Ops!', 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.', 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_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.', Open_your_authentication_app_and_enter_the_code: 'Abra seu aplicativo de autenticação e digite o código.',
OR: 'OU', 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', Overwrites_the_server_configuration_and_use_room_config: 'Substituir a configuração do servidor e usar a configuração da sala',
Password: 'Senha', Password: 'Senha',
Parent_channel_or_group: 'Canal ou grupo pai', Parent_channel_or_group: 'Canal ou grupo pai',
@ -315,6 +328,7 @@ export default {
Pinned: 'Mensagens Fixadas', Pinned: 'Mensagens Fixadas',
Please_wait: 'Por favor, aguarde.', Please_wait: 'Por favor, aguarde.',
Please_enter_your_password: 'Por favor, digite sua senha', Please_enter_your_password: 'Por favor, digite sua senha',
Please_add_a_comment: 'Por favor, adicione um comentário',
Preferences: 'Preferências', Preferences: 'Preferências',
Preferences_saved: 'Preferências salvas!', Preferences_saved: 'Preferências salvas!',
Privacy_Policy: ' Política de Privacidade', Privacy_Policy: ' Política de Privacidade',
@ -343,6 +357,7 @@ export default {
Reset_password: 'Resetar senha', Reset_password: 'Resetar senha',
resetting_password: 'redefinindo senha', resetting_password: 'redefinindo senha',
RESET: 'RESETAR', RESET: 'RESETAR',
Return: 'Retornar',
Review_app_title: 'Você está gostando do app?', Review_app_title: 'Você está gostando do app?',
Review_app_desc: 'Nos dê 5 estrelas na {{store}}', Review_app_desc: 'Nos dê 5 estrelas na {{store}}',
Review_app_yes: 'Claro!', Review_app_yes: 'Claro!',
@ -377,7 +392,9 @@ export default {
Select_Server: 'Selecionar Servidor', Select_Server: 'Selecionar Servidor',
Select_Users: 'Selecionar Usuários', Select_Users: 'Selecionar Usuários',
Select_a_Channel: 'Selecione um canal', Select_a_Channel: 'Selecione um canal',
Select_a_Department: 'Selecione um Departamento',
Select_an_option: 'Selecione uma opção', Select_an_option: 'Selecione uma opção',
Select_a_User: 'Selecione um Usuário',
Send: 'Enviar', Send: 'Enviar',
Send_audio_message: 'Enviar mensagem de áudio', Send_audio_message: 'Enviar mensagem de áudio',
Send_message: 'Enviar mensagem', Send_message: 'Enviar mensagem',
@ -436,6 +453,7 @@ export default {
Updating: 'Atualizando...', Updating: 'Atualizando...',
Uploading: 'Subindo arquivo', Uploading: 'Subindo arquivo',
Upload_file_question_mark: 'Enviar arquivo?', Upload_file_question_mark: 'Enviar arquivo?',
User: 'Usuário',
Users: 'Usuários', Users: 'Usuários',
User_added_by: 'Usuário {{userAdded}} adicionado por {{userBy}}', User_added_by: 'Usuário {{userAdded}} adicionado por {{userBy}}',
User_has_been_key: 'Usuário foi {{key}}!', 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_invite_link_will_never_expire: 'Seu link de convite nunca irá vencer.',
Your_workspace: 'Sua workspace', Your_workspace: 'Sua workspace',
You_will_not_be_able_to_recover_this_message: 'Você não será capaz de recuperar essa mensagem!', 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_Message: 'Rocket Chat precisa de acesso à sua galeria para salvar imagens',
Write_External_Permission: 'Acesso à Galeria', 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.', 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', Type_message: 'Digitar mensagem',
Room_search: 'Busca de sala', Room_search: 'Busca de sala',
@ -499,6 +519,30 @@ export default {
You_will_be_logged_out_of_this_application: 'Você sairá deste aplicativo.', You_will_be_logged_out_of_this_application: 'Você sairá deste aplicativo.',
Clear: 'Limpar', Clear: 'Limpar',
This_will_clear_all_your_offline_data: 'Isto limpará todos os seus dados offline.', 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', 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 RoomsListView from './views/RoomsListView';
import RoomView from './views/RoomView'; import RoomView from './views/RoomView';
import ScreenLockedView from './views/ScreenLockedView';
import ChangePasscodeView from './views/ChangePasscodeView';
if (isIOS) { if (isIOS) {
const RNScreens = require('react-native-screens'); const RNScreens = require('react-native-screens');
@ -166,6 +168,15 @@ const ChatsStack = createStackNavigator({
NotificationPrefView: { NotificationPrefView: {
getScreen: () => require('./views/NotificationPreferencesView').default getScreen: () => require('./views/NotificationPreferencesView').default
}, },
VisitorNavigationView: {
getScreen: () => require('./views/VisitorNavigationView').default
},
ForwardLivechatView: {
getScreen: () => require('./views/ForwardLivechatView').default
},
LivechatEditView: {
getScreen: () => require('./views/LivechatEditView').default
},
PickerView: { PickerView: {
getScreen: () => require('./views/PickerView').default getScreen: () => require('./views/PickerView').default
}, },
@ -224,6 +235,9 @@ const SettingsStack = createStackNavigator({
}, },
DefaultBrowserView: { DefaultBrowserView: {
getScreen: () => require('./views/DefaultBrowserView').default getScreen: () => require('./views/DefaultBrowserView').default
},
ScreenLockConfigView: {
getScreen: () => require('./views/ScreenLockConfigView').default
} }
}, { }, {
defaultNavigationOptions: defaultHeader, defaultNavigationOptions: defaultHeader,
@ -514,7 +528,7 @@ class CustomModalStack extends React.Component {
const pageSheetViews = ['AttachmentView']; const pageSheetViews = ['AttachmentView'];
const pageSheet = pageSheetViews.includes(getActiveRouteName(navigation.state)); const pageSheet = pageSheetViews.includes(getActiveRouteName(navigation.state));
const androidProps = isAndroid && { const androidProps = isAndroid && !pageSheet && {
style: { marginBottom: 0 } style: { marginBottom: 0 }
}; };
@ -524,7 +538,7 @@ class CustomModalStack extends React.Component {
</View> </View>
); );
if (isAndroid) { if (isAndroid && !pageSheet) {
content = ( content = (
<ScrollView overScrollMode='never'> <ScrollView overScrollMode='never'>
{content} {content}
@ -729,6 +743,8 @@ export default class Root extends React.Component {
> >
{content} {content}
<TwoFactor /> <TwoFactor />
<ScreenLockedView />
<ChangePasscodeView />
</ThemeContext.Provider> </ThemeContext.Provider>
</Provider> </Provider>
</AppearanceProvider> </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 { createStore, applyMiddleware, compose } from 'redux';
import createSagaMiddleware from 'redux-saga'; import createSagaMiddleware from 'redux-saga';
import applyAppStateListener from 'redux-enhancer-react-native-appstate';
import reducers from '../reducers'; import reducers from '../reducers';
import sagas from '../sagas'; import sagas from '../sagas';
import applyAppStateMiddleware from './appStateMiddleware';
let sagaMiddleware; let sagaMiddleware;
let enhancers; let enhancers;
@ -16,7 +16,7 @@ if (__DEV__) {
}); });
enhancers = compose( enhancers = compose(
applyAppStateListener(), applyAppStateMiddleware(),
applyMiddleware(reduxImmutableStateInvariant), applyMiddleware(reduxImmutableStateInvariant),
applyMiddleware(sagaMiddleware), applyMiddleware(sagaMiddleware),
Reactotron.createEnhancer() Reactotron.createEnhancer()
@ -24,7 +24,7 @@ if (__DEV__) {
} else { } else {
sagaMiddleware = createSagaMiddleware(); sagaMiddleware = createSagaMiddleware();
enhancers = compose( enhancers = compose(
applyAppStateListener(), applyAppStateMiddleware(),
applyMiddleware(sagaMiddleware) applyMiddleware(sagaMiddleware)
); );
} }

View File

@ -34,6 +34,36 @@ if (__DEV__ && isIOS) {
console.log(appGroupPath); 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 { class DB {
databases = { databases = {
serversDB: new Database({ serversDB: new Database({
@ -87,34 +117,8 @@ class DB {
}); });
} }
setActiveDB(database = '') { setActiveDB(database) {
const path = database.replace(/(^\w+:|^)\/\//, '').replace(/\//g, '.'); this.databases.activeDB = getDatabase(database);
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
});
} }
} }

View File

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

View File

@ -13,4 +13,14 @@ export default class Room extends Model {
@field('encrypted') encrypted; @field('encrypted') encrypted;
@field('ro') ro; @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; @date('rooms_updated_at') roomsUpdatedAt;
@field('version') version; @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('announcement') announcement;
@field('banner_closed') bannerClosed;
@field('topic') topic; @field('topic') topic;
@field('blocked') blocked; @field('blocked') blocked;
@ -95,4 +97,14 @@ export default class Subscription extends Model {
@json('uids', sanitizer) uids; @json('uids', sanitizer) uids;
@json('usernames', sanitizer) usernames; @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('avatar') avatar;
@field('emoji') emoji;
@json('attachments', sanitizer) attachments; @json('attachments', sanitizer) attachments;
@json('urls', sanitizer) urls; @json('urls', sanitizer) urls;

View File

@ -32,6 +32,8 @@ export default class ThreadMessage extends Model {
@field('avatar') avatar; @field('avatar') avatar;
@field('emoji') emoji;
@json('attachments', sanitizer) attachments; @json('attachments', sanitizer) attachments;
@json('urls', sanitizer) urls; @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'; import { appSchema, tableSchema } from '@nozbe/watermelondb';
export default appSchema({ export default appSchema({
version: 7, version: 8,
tables: [ tables: [
tableSchema({ tableSchema({
name: 'subscriptions', name: 'subscriptions',
@ -25,6 +25,7 @@ export default appSchema({
{ name: 'last_message', type: 'string', isOptional: true }, { name: 'last_message', type: 'string', isOptional: true },
{ name: 'description', type: 'string', isOptional: true }, { name: 'description', type: 'string', isOptional: true },
{ name: 'announcement', type: 'string', isOptional: true }, { name: 'announcement', type: 'string', isOptional: true },
{ name: 'banner_closed', type: 'boolean', isOptional: true },
{ name: 'topic', type: 'string', isOptional: true }, { name: 'topic', type: 'string', isOptional: true },
{ name: 'blocked', type: 'boolean', isOptional: true }, { name: 'blocked', type: 'boolean', isOptional: true },
{ name: 'blocker', 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: 'hide_unread_status', type: 'boolean', isOptional: true },
{ name: 'sys_mes', type: 'string', isOptional: true }, { name: 'sys_mes', type: 'string', isOptional: true },
{ name: 'uids', 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({ tableSchema({
@ -51,7 +57,12 @@ export default appSchema({
{ name: 'custom_fields', type: 'string' }, { name: 'custom_fields', type: 'string' },
{ name: 'broadcast', type: 'boolean' }, { name: 'broadcast', type: 'boolean' },
{ name: 'encrypted', 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({ tableSchema({
@ -66,6 +77,7 @@ export default appSchema({
{ name: 'parse_urls', type: 'string' }, { name: 'parse_urls', type: 'string' },
{ name: 'groupable', type: 'boolean', isOptional: true }, { name: 'groupable', type: 'boolean', isOptional: true },
{ name: 'avatar', type: 'string', isOptional: true }, { name: 'avatar', type: 'string', isOptional: true },
{ name: 'emoji', type: 'string', isOptional: true },
{ name: 'attachments', type: 'string', isOptional: true }, { name: 'attachments', type: 'string', isOptional: true },
{ name: 'urls', type: 'string', isOptional: true }, { name: 'urls', type: 'string', isOptional: true },
{ name: '_updated_at', type: 'number' }, { name: '_updated_at', type: 'number' },
@ -104,6 +116,7 @@ export default appSchema({
{ name: 'parse_urls', type: 'string', isOptional: true }, { name: 'parse_urls', type: 'string', isOptional: true },
{ name: 'groupable', type: 'boolean', isOptional: true }, { name: 'groupable', type: 'boolean', isOptional: true },
{ name: 'avatar', type: 'string', isOptional: true }, { name: 'avatar', type: 'string', isOptional: true },
{ name: 'emoji', type: 'string', isOptional: true },
{ name: 'attachments', type: 'string', isOptional: true }, { name: 'attachments', type: 'string', isOptional: true },
{ name: 'urls', type: 'string', isOptional: true }, { name: 'urls', type: 'string', isOptional: true },
{ name: 'status', type: 'number', isOptional: true }, { name: 'status', type: 'number', isOptional: true },
@ -140,6 +153,7 @@ export default appSchema({
{ name: 'parse_urls', type: 'string', isOptional: true }, { name: 'parse_urls', type: 'string', isOptional: true },
{ name: 'groupable', type: 'boolean', isOptional: true }, { name: 'groupable', type: 'boolean', isOptional: true },
{ name: 'avatar', type: 'string', isOptional: true }, { name: 'avatar', type: 'string', isOptional: true },
{ name: 'emoji', type: 'string', isOptional: true },
{ name: 'attachments', type: 'string', isOptional: true }, { name: 'attachments', type: 'string', isOptional: true },
{ name: 'urls', type: 'string', isOptional: true }, { name: 'urls', type: 'string', isOptional: true },
{ name: 'status', type: 'number', isOptional: true }, { name: 'status', type: 'number', isOptional: true },

View File

@ -1,7 +1,7 @@
import { appSchema, tableSchema } from '@nozbe/watermelondb'; import { appSchema, tableSchema } from '@nozbe/watermelondb';
export default appSchema({ export default appSchema({
version: 3, version: 4,
tables: [ tables: [
tableSchema({ tableSchema({
name: 'users', name: 'users',
@ -24,7 +24,11 @@ export default appSchema({
{ name: 'file_upload_media_type_white_list', type: 'string', isOptional: true }, { name: 'file_upload_media_type_white_list', type: 'string', isOptional: true },
{ name: 'file_upload_max_file_size', type: 'number', isOptional: true }, { name: 'file_upload_max_file_size', type: 'number', isOptional: true },
{ name: 'rooms_updated_at', 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 database from '../database';
import protectedFunction from './helpers/protectedFunction'; import protectedFunction from './helpers/protectedFunction';
import fetch from '../../utils/fetch'; 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 // these settings are used only on onboarding process
const loginSettings = [ const loginSettings = [
@ -32,6 +33,8 @@ const loginSettings = [
const serverInfoUpdate = async(serverInfo, iconSetting) => { const serverInfoUpdate = async(serverInfo, iconSetting) => {
const serversDB = database.servers; const serversDB = database.servers;
const serverId = reduxStore.getState().server.server; const serverId = reduxStore.getState().server.server;
const serversCollection = serversDB.collections.get('servers');
const server = await serversCollection.find(serverId);
let info = serverInfo.reduce((allSettings, setting) => { let info = serverInfo.reduce((allSettings, setting) => {
if (setting._id === 'Site_Name') { if (setting._id === 'Site_Name') {
@ -46,6 +49,23 @@ const serverInfoUpdate = async(serverInfo, iconSetting) => {
if (setting._id === 'FileUpload_MaxFileSize') { if (setting._id === 'FileUpload_MaxFileSize') {
return { ...allSettings, FileUpload_MaxFileSize: setting.valueAsNumber }; 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; return allSettings;
}, {}); }, {});
@ -56,9 +76,6 @@ const serverInfoUpdate = async(serverInfo, iconSetting) => {
await serversDB.action(async() => { await serversDB.action(async() => {
try { try {
const serversCollection = serversDB.collections.get('servers');
const server = await serversCollection.find(serverId);
await server.update((record) => { await server.update((record) => {
Object.assign(record, info); Object.assign(record, info);
}); });

View File

@ -27,6 +27,7 @@ export default async(subscriptions = [], rooms = []) => {
lastOpen: s.lastOpen, lastOpen: s.lastOpen,
description: s.description, description: s.description,
announcement: s.announcement, announcement: s.announcement,
bannerClosed: s.bannerClosed,
topic: s.topic, topic: s.topic,
blocked: s.blocked, blocked: s.blocked,
blocker: s.blocker, blocker: s.blocker,
@ -43,7 +44,12 @@ export default async(subscriptions = [], rooms = []) => {
autoTranslateLanguage: s.autoTranslateLanguage, autoTranslateLanguage: s.autoTranslateLanguage,
lastMessage: s.lastMessage, lastMessage: s.lastMessage,
usernames: s.usernames, 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); subscriptions = subscriptions.concat(existingSubs);
@ -64,7 +70,12 @@ export default async(subscriptions = [], rooms = []) => {
ro: r.ro, ro: r.ro,
broadcast: r.broadcast, broadcast: r.broadcast,
muted: r.muted, 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); rooms = rooms.concat(existingRooms);
} catch { } catch {

View File

@ -35,6 +35,21 @@ export const merge = (subscription, room) => {
} else { } else {
subscription.muted = []; 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; 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', { formData.append('file', {
uri: fileInfo.path, uri: fileInfo.path,
type: fileInfo.type, type: fileInfo.type,
name: fileInfo.name || 'fileMessage' name: encodeURI(fileInfo.name) || 'fileMessage'
}); });
if (fileInfo.description) { if (fileInfo.description) {

View File

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

View File

@ -57,6 +57,7 @@ const createOrUpdateSubscription = async(subscription, room) => {
lastOpen: s.lastOpen, lastOpen: s.lastOpen,
description: s.description, description: s.description,
announcement: s.announcement, announcement: s.announcement,
bannerClosed: s.bannerClosed,
topic: s.topic, topic: s.topic,
blocked: s.blocked, blocked: s.blocked,
blocker: s.blocker, blocker: s.blocker,
@ -74,7 +75,12 @@ const createOrUpdateSubscription = async(subscription, room) => {
lastMessage: s.lastMessage, lastMessage: s.lastMessage,
roles: s.roles, roles: s.roles,
usernames: s.usernames, 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) { } catch (error) {
try { try {
@ -97,10 +103,15 @@ const createOrUpdateSubscription = async(subscription, room) => {
// We have to create a plain obj so we can manipulate it on `merge` // We have to create a plain obj so we can manipulate it on `merge`
// Can we do it in a better way? // Can we do it in a better way?
room = { room = {
customFields: r.customFields, v: r.v,
broadcast: r.broadcast, ro: r.ro,
tags: r.tags,
servedBy: r.servedBy,
encrypted: r.encrypted, encrypted: r.encrypted,
ro: r.ro broadcast: r.broadcast,
customFields: r.customFields,
departmentId: r.departmentId,
livechatData: r.livechatData
}; };
} catch (error) { } catch (error) {
// Do nothing // Do nothing
@ -121,6 +132,11 @@ const createOrUpdateSubscription = async(subscription, room) => {
try { try {
const update = sub.prepareUpdate((s) => { const update = sub.prepareUpdate((s) => {
Object.assign(s, tmp); Object.assign(s, tmp);
if (subscription.announcement) {
if (subscription.announcement !== sub.announcement) {
s.bannerClosed = false;
}
}
}); });
batch.push(update); batch.push(update);
} catch (e) { } 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 lastMessage = buildMessage(tmp.lastMessage);
const messagesCollection = db.collections.get('messages'); const messagesCollection = db.collections.get('messages');
let messageRecord; let messageRecord;

View File

@ -1,9 +1,9 @@
import { AsyncStorage, InteractionManager } from 'react-native'; import { InteractionManager } from 'react-native';
import semver from 'semver'; import semver from 'semver';
import { Rocketchat as RocketchatClient } from '@rocket.chat/sdk'; import { Rocketchat as RocketchatClient } from '@rocket.chat/sdk';
import RNUserDefaults from 'rn-user-defaults'; import RNUserDefaults from 'rn-user-defaults';
import { Q } from '@nozbe/watermelondb'; import { Q } from '@nozbe/watermelondb';
import * as FileSystem from 'expo-file-system'; import AsyncStorage from '@react-native-community/async-storage';
import reduxStore from './createStore'; import reduxStore from './createStore';
import defaultSettings from '../constants/settings'; import defaultSettings from '../constants/settings';
@ -11,8 +11,7 @@ import messagesStatus from '../constants/messagesStatus';
import database from './database'; import database from './database';
import log from '../utils/log'; import log from '../utils/log';
import { isIOS, getBundleId } from '../utils/deviceInfo'; import { isIOS, getBundleId } from '../utils/deviceInfo';
import { extractHostname } from '../utils/server'; import fetch from '../utils/fetch';
import fetch, { BASIC_AUTH_KEY } from '../utils/fetch';
import { setUser, setLoginServices, loginRequest } from '../actions/login'; import { setUser, setLoginServices, loginRequest } from '../actions/login';
import { disconnect, connectSuccess, connectRequest } from '../actions/connect'; 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 { sendFileMessage, cancelUpload, isUploadActive } from './methods/sendFileMessage';
import callJitsi from './methods/callJitsi'; import callJitsi from './methods/callJitsi';
import logout, { removeServer } from './methods/logout';
import { getDeviceToken } from '../notifications/push'; import { getDeviceToken } from '../notifications/push';
import { SERVERS, SERVER_URL } from '../constants/userDefaults';
import { setActiveUsers } from '../actions/activeUsers'; import { setActiveUsers } from '../actions/activeUsers';
import I18n from '../i18n'; import I18n from '../i18n';
import { twoFactor } from '../utils/twoFactor'; import { twoFactor } from '../utils/twoFactor';
import { selectServerFailure } from '../actions/server';
import { useSsl } from '../utils/url';
const TOKEN_KEY = 'reactnativemeteor_usertoken'; const TOKEN_KEY = 'reactnativemeteor_usertoken';
const SORT_PREFS_KEY = 'RC_SORT_PREFS_KEY'; const SORT_PREFS_KEY = 'RC_SORT_PREFS_KEY';
@ -86,10 +87,7 @@ const RocketChat = {
} }
}, },
async getWebsocketInfo({ server }) { async getWebsocketInfo({ server }) {
// Use useSsl: false only if server url starts with http:// const sdk = new RocketchatClient({ host: server, protocol: 'ddp', useSsl: useSsl(server) });
const useSsl = !/http:\/\//.test(server);
const sdk = new RocketchatClient({ host: server, protocol: 'ddp', useSsl });
try { try {
await sdk.connect(); await sdk.connect();
@ -146,6 +144,10 @@ const RocketChat = {
} }
return result; return result;
} catch (e) { } catch (e) {
if (e.message === 'Aborted') {
reduxStore.dispatch(selectServerFailure());
throw e;
}
log(e); log(e);
} }
return { return {
@ -159,6 +161,16 @@ const RocketChat = {
stopListener(listener) { stopListener(listener) {
return listener && listener.stop(); 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 }) { connect({ server, user, logoutOnError = false }) {
return new Promise((resolve) => { return new Promise((resolve) => {
if (!this.sdk || this.sdk.client.host !== server) { if (!this.sdk || this.sdk.client.host !== server) {
@ -200,15 +212,13 @@ const RocketChat = {
this.code = null; this.code = null;
} }
// Use useSsl: false only if server url starts with http:// this.sdk = new RocketchatClient({ host: server, protocol: 'ddp', useSsl: useSsl(server) });
const useSsl = !/http:\/\//.test(server);
this.sdk = new RocketchatClient({ host: server, protocol: 'ddp', useSsl });
this.getSettings(); this.getSettings();
const sdkConnect = () => this.sdk.connect() const sdkConnect = () => this.sdk.connect()
.then(() => { .then(() => {
if (user && user.token) { const { server: currentServer } = reduxStore.getState().server;
if (user && user.token && server === currentServer) {
reduxStore.dispatch(loginRequest({ resume: user.token }, logoutOnError)); reduxStore.dispatch(loginRequest({ resume: user.token }, logoutOnError));
} }
}) })
@ -217,7 +227,9 @@ const RocketChat = {
// when `connect` raises an error, we try again in 10 seconds // when `connect` raises an error, we try again in 10 seconds
this.connectTimeout = setTimeout(() => { this.connectTimeout = setTimeout(() => {
if (this.sdk?.client?.host === server) {
sdkConnect(); sdkConnect();
}
}, 10000); }, 10000);
}); });
@ -270,10 +282,7 @@ const RocketChat = {
this.shareSDK = null; this.shareSDK = null;
} }
// Use useSsl: false only if server url starts with http:// this.shareSDK = new RocketchatClient({ host: server, protocol: 'ddp', useSsl: useSsl(server) });
const useSsl = !/http:\/\//.test(server);
this.shareSDK = new RocketchatClient({ host: server, protocol: 'ddp', useSsl });
// set Server // set Server
const serversDB = database.servers; const serversDB = database.servers;
@ -306,7 +315,7 @@ const RocketChat = {
} }
database.share = null; database.share = null;
reduxStore.dispatch(shareSetUser(null)); reduxStore.dispatch(shareSetUser({}));
}, },
updateJitsiTimeout(rid) { updateJitsiTimeout(rid) {
@ -369,24 +378,15 @@ const RocketChat = {
}; };
} }
try {
return this.loginTOTP(params); return this.loginTOTP(params);
} catch (error) {
throw error;
}
}, },
async loginOAuthOrSso(params) { async loginOAuthOrSso(params) {
try {
const result = await this.login(params); const result = await this.login(params);
reduxStore.dispatch(loginRequest({ resume: result.token })); reduxStore.dispatch(loginRequest({ resume: result.token }));
} catch (error) {
throw error;
}
}, },
async login(params) { async login(params) {
try {
const sdk = this.shareSDK || this.sdk; const sdk = this.shareSDK || this.sdk;
// RC 0.64.0 // RC 0.64.0
await sdk.login(params); await sdk.login(params);
@ -404,77 +404,9 @@ const RocketChat = {
roles: result.me.roles roles: result.me.roles
}; };
return user; 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 }) { async clearCache({ server }) {
try { try {
const serversDB = database.servers; const serversDB = database.servers;
@ -573,9 +505,9 @@ const RocketChat = {
).fetch(); ).fetch();
if (filterUsers && !filterRooms) { if (filterUsers && !filterRooms) {
data = data.filter(item => item.t === 'd'); data = data.filter(item => item.t === 'd' && !RocketChat.isGroupChat(item));
} else if (!filterUsers && filterRooms) { } 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); data = data.slice(0, 7);
@ -824,6 +756,59 @@ const RocketChat = {
return this.sdk.get('rooms.info', { roomId }); 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) { getUidDirectMessage(room) {
const { id: userId } = reduxStore.getState().login.user; const { id: userId } = reduxStore.getState().login.user;
@ -909,7 +894,7 @@ const RocketChat = {
methodCall(...args) { methodCall(...args) {
return new Promise(async(resolve, reject) => { return new Promise(async(resolve, reject) => {
try { try {
const result = await this.sdk.methodCall(...args, this.code); const result = await this.sdk.methodCall(...args, this.code || '');
return resolve(result); return resolve(result);
} catch (e) { } catch (e) {
if (e.error && (e.error === 'totp-required' || e.error === 'totp-invalid')) { if (e.error && (e.error === 'totp-required' || e.error === 'totp-invalid')) {
@ -977,7 +962,7 @@ const RocketChat = {
const shareUser = reduxStore.getState().share.user; const shareUser = reduxStore.getState().share.user;
const loginUser = reduxStore.getState().login.user; const loginUser = reduxStore.getState().login.user;
// get user roles on the server from redux // get user roles on the server from redux
const userRoles = (shareUser.roles || loginUser.roles) || []; const userRoles = (shareUser?.roles || loginUser?.roles) || [];
// merge both roles // merge both roles
const mergedRoles = [...new Set([...roomRoles, ...userRoles])]; const mergedRoles = [...new Set([...roomRoles, ...userRoles])];
@ -1226,16 +1211,19 @@ const RocketChat = {
return this.methodCall('autoTranslate.translateMessage', message, targetLanguage); return this.methodCall('autoTranslate.translateMessage', message, targetLanguage);
}, },
getRoomTitle(room) { 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; const { username } = reduxStore.getState().login.user;
if (RocketChat.isGroupChat(room) && !(room.name && room.name.length)) { if (RocketChat.isGroupChat(room) && !(room.name && room.name.length)) {
return room.usernames.filter(u => u !== username).sort((u1, u2) => u1.localeCompare(u2)).join(', '); 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; return ((room.prid || useRealName) && room.fname) || room.name;
}, },
getRoomAvatar(room) { getRoomAvatar(room) {
if (RocketChat.isGroupChat(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; return room.prid ? room.fname : room.name;
}, },

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