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:
commit
3eb4545c6b
|
@ -1,12 +1,132 @@
|
|||
defaults: &defaults
|
||||
working_directory: ~/repo
|
||||
|
||||
version: 2
|
||||
macos: &macos
|
||||
macos:
|
||||
xcode: "11.2.1"
|
||||
|
||||
bash-env: &bash-env
|
||||
BASH_ENV: "~/.nvm/nvm.sh"
|
||||
|
||||
install-npm-modules: &install-npm-modules
|
||||
name: Install NPM modules
|
||||
command: yarn
|
||||
|
||||
restore-npm-cache-linux: &restore-npm-cache-linux
|
||||
name: Restore NPM cache
|
||||
key: node-modules-{{ checksum "yarn.lock" }}
|
||||
|
||||
save-npm-cache-linux: &save-npm-cache-linux
|
||||
key: node-modules-{{ checksum "yarn.lock" }}
|
||||
name: Save NPM cache
|
||||
paths:
|
||||
- ./node_modules
|
||||
|
||||
restore-npm-cache-mac: &restore-npm-cache-mac
|
||||
name: Restore NPM cache
|
||||
key: node-v1-mac-{{ checksum "yarn.lock" }}
|
||||
|
||||
save-npm-cache-mac: &save-npm-cache-mac
|
||||
key: node-v1-mac-{{ checksum "yarn.lock" }}
|
||||
name: Save NPM cache
|
||||
paths:
|
||||
- ./node_modules
|
||||
|
||||
install-node: &install-node
|
||||
name: Install Node 10
|
||||
command: |
|
||||
curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.6/install.sh | bash
|
||||
source ~/.nvm/nvm.sh
|
||||
# https://github.com/creationix/nvm/issues/1394
|
||||
set +e
|
||||
nvm install 10
|
||||
echo 'export PATH="/home/circleci/.nvm/versions/node/v10.20.1/bin:$PATH"' >> ~/.bash_profile
|
||||
source ~/.bash_profile
|
||||
|
||||
restore-gems-cache: &restore-gems-cache
|
||||
name: Restore gems cache
|
||||
key: bundle-v1-{{ checksum "ios/Gemfile.lock" }}
|
||||
|
||||
save-gems-cache: &save-gems-cache
|
||||
name: Save gems cache
|
||||
key: bundle-v1-{{ checksum "ios/Gemfile.lock" }}
|
||||
paths:
|
||||
- vendor/bundle
|
||||
|
||||
update-fastlane: &update-fastlane
|
||||
name: Update Fastlane
|
||||
command: |
|
||||
echo "ruby-2.6.4" > ~/.ruby-version
|
||||
bundle install
|
||||
working_directory: ios
|
||||
|
||||
restore-brew-cache: &restore-brew-cache
|
||||
name: Restore Brew cache
|
||||
key: brew-{{ checksum "yarn.lock" }}-{{ checksum ".circleci/config.yml" }}
|
||||
|
||||
save-brew-cache: &save-brew-cache
|
||||
name: Save brew cache
|
||||
key: brew-{{ checksum "yarn.lock" }}-{{ checksum ".circleci/config.yml" }}
|
||||
paths:
|
||||
- /usr/local/Homebrew
|
||||
|
||||
install-apple-sim-utils: &install-apple-sim-utils
|
||||
name: Install appleSimUtils
|
||||
command: |
|
||||
brew update
|
||||
brew tap wix/brew
|
||||
brew install wix/brew/applesimutils
|
||||
|
||||
rebuild-detox: &rebuild-detox
|
||||
name: Rebuild Detox framework cache
|
||||
command: |
|
||||
npx detox clean-framework-cache
|
||||
npx detox build-framework-cache
|
||||
|
||||
version: 2.1
|
||||
|
||||
# EXECUTORS
|
||||
executors:
|
||||
mac-env:
|
||||
<<: *macos
|
||||
environment:
|
||||
<<: *bash-env
|
||||
|
||||
# COMMANDS
|
||||
commands:
|
||||
detox-test:
|
||||
parameters:
|
||||
folder:
|
||||
type: string
|
||||
steps:
|
||||
- checkout
|
||||
|
||||
- attach_workspace:
|
||||
at: .
|
||||
|
||||
- restore_cache: *restore-npm-cache-mac
|
||||
|
||||
- restore_cache: *restore-brew-cache
|
||||
|
||||
- run: *install-node
|
||||
|
||||
- run: *install-apple-sim-utils
|
||||
|
||||
- run: *install-npm-modules
|
||||
|
||||
- run: *rebuild-detox
|
||||
|
||||
- run:
|
||||
name: Test
|
||||
command: |
|
||||
npx detox test << parameters.folder >> --configuration ios.sim.release --cleanup
|
||||
|
||||
# JOBS
|
||||
jobs:
|
||||
lint-testunit:
|
||||
<<: *defaults
|
||||
docker:
|
||||
- image: circleci/node:8
|
||||
- image: circleci/node:10
|
||||
|
||||
environment:
|
||||
CODECOV_TOKEN: caa771ab-3d45-4756-8e2a-e1f25996fef6
|
||||
|
@ -14,14 +134,9 @@ jobs:
|
|||
steps:
|
||||
- checkout
|
||||
|
||||
- restore_cache:
|
||||
name: Restore NPM cache
|
||||
key: node-modules-{{ checksum "yarn.lock" }}
|
||||
- restore_cache: *restore-npm-cache-linux
|
||||
|
||||
- run:
|
||||
name: Install NPM modules
|
||||
command: |
|
||||
yarn
|
||||
- run: *install-npm-modules
|
||||
|
||||
- run:
|
||||
name: Lint
|
||||
|
@ -38,162 +153,79 @@ jobs:
|
|||
command: |
|
||||
yarn codecov
|
||||
|
||||
- save_cache:
|
||||
key: node-modules-{{ checksum "yarn.lock" }}
|
||||
name: Save NPM cache
|
||||
paths:
|
||||
- ./node_modules
|
||||
- save_cache: *save-npm-cache-linux
|
||||
|
||||
# E2E
|
||||
e2e-build:
|
||||
macos:
|
||||
xcode: "11.2.1"
|
||||
|
||||
environment:
|
||||
BASH_ENV: "~/.nvm/nvm.sh"
|
||||
executor: mac-env
|
||||
|
||||
steps:
|
||||
- checkout
|
||||
|
||||
- restore_cache:
|
||||
name: Restore NPM cache
|
||||
key: node-v1-mac-{{ checksum "yarn.lock" }}
|
||||
- restore_cache: *restore-npm-cache-mac
|
||||
|
||||
- run:
|
||||
name: Install Node 8
|
||||
command: |
|
||||
curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.6/install.sh | bash
|
||||
source ~/.nvm/nvm.sh
|
||||
# https://github.com/creationix/nvm/issues/1394
|
||||
set +e
|
||||
nvm install 8
|
||||
- restore_cache: *restore-brew-cache
|
||||
|
||||
- run:
|
||||
name: Install appleSimUtils
|
||||
command: |
|
||||
brew update
|
||||
brew tap wix/brew
|
||||
brew install wix/brew/applesimutils
|
||||
- run: *install-node
|
||||
|
||||
- run:
|
||||
name: Install NPM modules
|
||||
command: |
|
||||
yarn global add detox-cli
|
||||
yarn
|
||||
- run: *install-apple-sim-utils
|
||||
|
||||
- run:
|
||||
name: Rebuild Detox framework cache
|
||||
command: |
|
||||
detox clean-framework-cache
|
||||
detox build-framework-cache
|
||||
- run: *install-npm-modules
|
||||
|
||||
- run: *rebuild-detox
|
||||
|
||||
- run:
|
||||
name: Build
|
||||
command: |
|
||||
detox build --configuration ios.sim.release
|
||||
npx detox build --configuration ios.sim.release
|
||||
|
||||
- persist_to_workspace:
|
||||
root: .
|
||||
paths:
|
||||
- ios/build/Build/Products/Release-iphonesimulator/RocketChatRN.app
|
||||
|
||||
- save_cache:
|
||||
name: Save NPM cache
|
||||
key: node-v1-mac-{{ checksum "yarn.lock" }}
|
||||
paths:
|
||||
- node_modules
|
||||
- save_cache: *save-npm-cache-mac
|
||||
|
||||
e2e-test:
|
||||
macos:
|
||||
xcode: "11.2.1"
|
||||
|
||||
environment:
|
||||
BASH_ENV: "~/.nvm/nvm.sh"
|
||||
- save_cache: *save-brew-cache
|
||||
|
||||
e2e-test-onboarding:
|
||||
executor: mac-env
|
||||
steps:
|
||||
- checkout
|
||||
- detox-test:
|
||||
folder: "./e2e/tests/onboarding"
|
||||
|
||||
- attach_workspace:
|
||||
at: .
|
||||
e2e-test-room:
|
||||
executor: mac-env
|
||||
steps:
|
||||
- detox-test:
|
||||
folder: "./e2e/tests/room"
|
||||
|
||||
- restore_cache:
|
||||
name: Restore NPM cache
|
||||
key: node-v1-mac-{{ checksum "yarn.lock" }}
|
||||
|
||||
- run:
|
||||
name: Install Node 8
|
||||
command: |
|
||||
curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.6/install.sh | bash
|
||||
source ~/.nvm/nvm.sh
|
||||
# https://github.com/creationix/nvm/issues/1394
|
||||
set +e
|
||||
nvm install 8
|
||||
|
||||
- run:
|
||||
name: Install appleSimUtils
|
||||
command: |
|
||||
brew update
|
||||
brew tap wix/brew
|
||||
brew install wix/brew/applesimutils
|
||||
|
||||
- run:
|
||||
name: Install NPM modules
|
||||
command: |
|
||||
yarn global add detox-cli
|
||||
yarn
|
||||
|
||||
- run:
|
||||
name: Rebuild Detox framework cache
|
||||
command: |
|
||||
detox clean-framework-cache
|
||||
detox build-framework-cache
|
||||
|
||||
- run:
|
||||
name: Test
|
||||
command: |
|
||||
detox test --configuration ios.sim.release --cleanup
|
||||
|
||||
- save_cache:
|
||||
name: Save NPM cache
|
||||
key: node-v1-mac-{{ checksum "yarn.lock" }}
|
||||
paths:
|
||||
- node_modules
|
||||
e2e-test-assorted:
|
||||
executor: mac-env
|
||||
steps:
|
||||
- detox-test:
|
||||
folder: "./e2e/tests/assorted"
|
||||
|
||||
# Android builds
|
||||
android-build:
|
||||
<<: *defaults
|
||||
docker:
|
||||
- image: circleci/android:api-28-node
|
||||
|
||||
environment:
|
||||
# GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx4096m -XX:+HeapDumpOnOutOfMemoryError"
|
||||
# GRADLE_OPTS: -Xmx2048m -Dorg.gradle.daemon=false
|
||||
# JVM_OPTS: -Xmx4096m
|
||||
JAVA_OPTS: '-Xms512m -Xmx2g'
|
||||
GRADLE_OPTS: '-Xmx3g -Dorg.gradle.daemon=false -Dorg.gradle.jvmargs="-Xmx2g -XX:+HeapDumpOnOutOfMemoryError"'
|
||||
TERM: dumb
|
||||
BASH_ENV: "~/.nvm/nvm.sh"
|
||||
<<: *bash-env
|
||||
|
||||
steps:
|
||||
- checkout
|
||||
|
||||
- run:
|
||||
name: Install Node 8
|
||||
command: |
|
||||
curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.6/install.sh | bash
|
||||
source ~/.nvm/nvm.sh
|
||||
# https://github.com/creationix/nvm/issues/1394
|
||||
set +e
|
||||
nvm install 8
|
||||
echo 'export PATH="/home/circleci/.nvm/versions/node/v8.16.0/bin:$PATH"' >> ~/.bash_profile
|
||||
source ~/.bash_profile
|
||||
- run: *install-node
|
||||
|
||||
- restore_cache:
|
||||
name: Restore NPM cache
|
||||
key: node-modules-{{ checksum "yarn.lock" }}
|
||||
- restore_cache: *restore-npm-cache-linux
|
||||
|
||||
- run:
|
||||
name: Install NPM modules
|
||||
command: |
|
||||
yarn
|
||||
- run: *install-npm-modules
|
||||
|
||||
- restore_cache:
|
||||
name: Restore gradle cache
|
||||
|
@ -206,6 +238,7 @@ jobs:
|
|||
# echo -e "android.enableAapt2=false" >> ./gradle.properties
|
||||
echo -e "android.useAndroidX=true" >> ./gradle.properties
|
||||
echo -e "android.enableJetifier=true" >> ./gradle.properties
|
||||
echo -e "FLIPPER_VERSION=0.33.1" >> ./gradle.properties
|
||||
|
||||
if [[ $KEYSTORE ]]; then
|
||||
echo $KEYSTORE_BASE64 | base64 --decode > ./app/$KEYSTORE
|
||||
|
@ -234,8 +267,7 @@ jobs:
|
|||
name: Build Android App
|
||||
command: |
|
||||
if [[ $KEYSTORE ]]; then
|
||||
# TODO: enable app bundle again
|
||||
./gradlew assembleRelease
|
||||
./gradlew bundleRelease
|
||||
else
|
||||
./gradlew assembleDebug
|
||||
fi
|
||||
|
@ -261,11 +293,7 @@ jobs:
|
|||
- store_artifacts:
|
||||
path: /tmp/build/outputs
|
||||
|
||||
- save_cache:
|
||||
name: Save NPM cache
|
||||
key: node-modules-{{ checksum "yarn.lock" }}
|
||||
paths:
|
||||
- ./node_modules
|
||||
- save_cache: *save-npm-cache-linux
|
||||
|
||||
- save_cache:
|
||||
name: Save gradle cache
|
||||
|
@ -273,44 +301,22 @@ jobs:
|
|||
paths:
|
||||
- ~/.gradle
|
||||
|
||||
# iOS builds
|
||||
ios-build:
|
||||
macos:
|
||||
xcode: "11.2.1"
|
||||
|
||||
environment:
|
||||
BASH_ENV: "~/.nvm/nvm.sh"
|
||||
executor: mac-env
|
||||
|
||||
steps:
|
||||
- checkout
|
||||
|
||||
- restore_cache:
|
||||
name: Restore gems cache
|
||||
key: bundle-v1-{{ checksum "ios/Gemfile.lock" }}
|
||||
- restore_cache: *restore-gems-cache
|
||||
|
||||
- restore_cache:
|
||||
name: Restore NPM cache
|
||||
key: node-v1-mac-{{ checksum "yarn.lock" }}
|
||||
- restore_cache: *restore-npm-cache-mac
|
||||
|
||||
- run:
|
||||
name: Install Node 8
|
||||
command: |
|
||||
curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.6/install.sh | bash
|
||||
source ~/.nvm/nvm.sh
|
||||
# https://github.com/creationix/nvm/issues/1394
|
||||
set +e
|
||||
nvm install 8
|
||||
- run: *install-node
|
||||
|
||||
- run:
|
||||
name: Install NPM modules
|
||||
command: |
|
||||
yarn
|
||||
- run: *install-npm-modules
|
||||
|
||||
- run:
|
||||
name: Update Fastlane
|
||||
command: |
|
||||
echo "ruby-2.6.4" > ~/.ruby-version
|
||||
bundle install
|
||||
working_directory: ios
|
||||
- run: *update-fastlane
|
||||
|
||||
- run:
|
||||
name: Set Google Services
|
||||
|
@ -348,17 +354,9 @@ jobs:
|
|||
fi
|
||||
working_directory: ios
|
||||
|
||||
- save_cache:
|
||||
name: Save NPM cache
|
||||
key: node-v1-mac-{{ checksum "yarn.lock" }}
|
||||
paths:
|
||||
- node_modules
|
||||
- save_cache: *save-npm-cache-mac
|
||||
|
||||
- save_cache:
|
||||
name: Save gems cache
|
||||
key: bundle-v1-{{ checksum "ios/Gemfile.lock" }}
|
||||
paths:
|
||||
- vendor/bundle
|
||||
- save_cache: *save-gems-cache
|
||||
|
||||
- store_artifacts:
|
||||
path: ios/RocketChatRN.ipa
|
||||
|
@ -370,8 +368,7 @@ jobs:
|
|||
- ios/fastlane/report.xml
|
||||
|
||||
ios-testflight:
|
||||
macos:
|
||||
xcode: "11.2.1"
|
||||
executor: mac-env
|
||||
|
||||
steps:
|
||||
- checkout
|
||||
|
@ -379,16 +376,9 @@ jobs:
|
|||
- attach_workspace:
|
||||
at: ios
|
||||
|
||||
- restore_cache:
|
||||
name: Restore gems cache
|
||||
key: bundle-v1-{{ checksum "ios/Gemfile.lock" }}
|
||||
- restore_cache: *restore-gems-cache
|
||||
|
||||
- run:
|
||||
name: Update Fastlane
|
||||
command: |
|
||||
echo "ruby-2.4" > ~/.ruby-version
|
||||
bundle install
|
||||
working_directory: ios
|
||||
- run: *update-fastlane
|
||||
|
||||
- run:
|
||||
name: Fastlane Tesflight Upload
|
||||
|
@ -396,14 +386,9 @@ jobs:
|
|||
bundle exec fastlane ios beta
|
||||
working_directory: ios
|
||||
|
||||
- save_cache:
|
||||
name: Save gems cache
|
||||
key: bundle-v1-{{ checksum "ios/Gemfile.lock" }}
|
||||
paths:
|
||||
- vendor/bundle
|
||||
- save_cache: *save-gems-cache
|
||||
|
||||
workflows:
|
||||
version: 2
|
||||
build-and-test:
|
||||
jobs:
|
||||
- lint-testunit
|
||||
|
@ -415,7 +400,13 @@ workflows:
|
|||
- e2e-build:
|
||||
requires:
|
||||
- e2e-hold
|
||||
- e2e-test:
|
||||
- e2e-test-onboarding:
|
||||
requires:
|
||||
- e2e-build
|
||||
- e2e-test-room:
|
||||
requires:
|
||||
- e2e-build
|
||||
- e2e-test-assorted:
|
||||
requires:
|
||||
- e2e-build
|
||||
|
||||
|
|
19
.eslintrc.js
19
.eslintrc.js
|
@ -87,6 +87,7 @@ module.exports = {
|
|||
"no-regex-spaces": 2,
|
||||
"no-undef": 2,
|
||||
"no-unreachable": 2,
|
||||
"no-unused-expressions": 0,
|
||||
"no-unused-vars": [2, {
|
||||
"vars": "all",
|
||||
"args": "after-used"
|
||||
|
@ -131,7 +132,23 @@ module.exports = {
|
|||
"react-native/no-unused-styles": 2,
|
||||
"react/jsx-one-expression-per-line": 0,
|
||||
"require-await": 2,
|
||||
"func-names": 0
|
||||
"func-names": 0,
|
||||
"react/sort-comp": ["error", {
|
||||
"order": [
|
||||
"static-variables",
|
||||
"static-methods",
|
||||
"lifecycle",
|
||||
"everything-else",
|
||||
"render"
|
||||
]
|
||||
}],
|
||||
"react/static-property-placement": [0],
|
||||
"arrow-parens": ["error", "as-needed", { requireForBlockBody: true }],
|
||||
"react/jsx-props-no-spreading": [1],
|
||||
"react/jsx-curly-newline": [0],
|
||||
"react/state-in-constructor": [0],
|
||||
"no-async-promise-executor": [0],
|
||||
"max-classes-per-file": [0]
|
||||
},
|
||||
"globals": {
|
||||
"__DEV__": true
|
||||
|
|
|
@ -42,6 +42,7 @@ coverage/
|
|||
buck-out/
|
||||
\.buckd/
|
||||
*.keystore
|
||||
*.jks
|
||||
|
||||
# fastlane
|
||||
#
|
||||
|
|
|
@ -208,13 +208,15 @@ Readme will guide you on how to config.
|
|||
- Build your app
|
||||
|
||||
```bash
|
||||
$ detox build --configuration ios.sim.release
|
||||
$ npx detox build --configuration ios.sim.release
|
||||
```
|
||||
|
||||
- Run tests
|
||||
|
||||
```bash
|
||||
$ detox test --configuration ios.sim.release
|
||||
$ npx detox test ./e2e/tests/onboarding --configuration ios.sim.release
|
||||
$ npx detox test ./e2e/tests/room --configuration ios.sim.release
|
||||
$ npx detox test ./e2e/tests/assorted --configuration ios.sim.release
|
||||
```
|
||||
|
||||
## Storybook
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
export class Sound {
|
||||
loadAsync = () => {};
|
||||
|
||||
playAsync = () => {};
|
||||
|
||||
pauseAsync = () => {};
|
||||
|
||||
stopAsync = () => {};
|
||||
|
||||
setOnPlaybackStatusUpdate = () => {};
|
||||
|
||||
setPositionAsync = () => {};
|
||||
}
|
||||
export const Audio = { Sound };
|
|
@ -0,0 +1,4 @@
|
|||
export default {
|
||||
activateKeepAwake: () => '',
|
||||
deactivateKeepAwake: () => ''
|
||||
};
|
|
@ -2,5 +2,6 @@ export default {
|
|||
getModel: () => '',
|
||||
getReadableVersion: () => '',
|
||||
getBundleId: () => '',
|
||||
isTablet: () => false
|
||||
isTablet: () => false,
|
||||
hasNotch: () => false
|
||||
};
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -19,7 +19,9 @@ import com.android.build.OutputFile
|
|||
* // the name of the generated asset file containing your JS bundle
|
||||
* bundleAssetName: "index.android.bundle",
|
||||
*
|
||||
* // the entry file for bundle generation
|
||||
* // the entry file for bundle generation. If none specified and
|
||||
* // "index.android.js" exists, it will be used. Otherwise "index.js" is
|
||||
* // default. Can be overridden with ENTRY_FILE environment variable.
|
||||
* entryFile: "index.android.js",
|
||||
*
|
||||
* // https://facebook.github.io/react-native/docs/performance#enable-the-ram-format
|
||||
|
@ -80,7 +82,6 @@ import com.android.build.OutputFile
|
|||
*/
|
||||
|
||||
project.ext.react = [
|
||||
entryFile: "index.js",
|
||||
bundleAssetName: "app.bundle",
|
||||
iconFontNames: [ 'custom.ttf' ],
|
||||
enableHermes: true, // clean and rebuild if changing
|
||||
|
@ -141,6 +142,7 @@ android {
|
|||
versionName VERSIONNAME as String
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
manifestPlaceholders = [BugsnagAPIKey: BugsnagAPIKey as String]
|
||||
missingDimensionStrategy "RNNotifications.reactNativeVersion", "reactNative60" // See note below!
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
|
@ -168,6 +170,14 @@ android {
|
|||
signingConfig signingConfigs.release
|
||||
}
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
pickFirst '**/armeabi-v7a/libc++_shared.so'
|
||||
pickFirst '**/x86/libc++_shared.so'
|
||||
pickFirst '**/arm64-v8a/libc++_shared.so'
|
||||
pickFirst '**/x86_64/libc++_shared.so'
|
||||
}
|
||||
|
||||
// applicationVariants are e.g. debug, release
|
||||
applicationVariants.all { variant ->
|
||||
variant.outputs.each { output ->
|
||||
|
@ -202,6 +212,7 @@ dependencies {
|
|||
implementation project(":reactnativekeyboardinput")
|
||||
implementation project(':@react-native-community_viewpager')
|
||||
implementation fileTree(dir: "libs", include: ["*.jar"])
|
||||
//noinspection GradleDynamicVersion
|
||||
implementation "com.facebook.react:react-native:+" // From node_modules
|
||||
implementation "com.google.firebase:firebase-messaging:18.0.0"
|
||||
implementation "com.google.firebase:firebase-core:16.0.9"
|
||||
|
@ -209,6 +220,16 @@ dependencies {
|
|||
implementation('com.crashlytics.sdk.android:crashlytics:2.9.9@aar') {
|
||||
transitive = true
|
||||
}
|
||||
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.0.0"
|
||||
debugImplementation("com.facebook.flipper:flipper:${FLIPPER_VERSION}") {
|
||||
exclude group:'com.facebook.fbjni'
|
||||
}
|
||||
debugImplementation("com.facebook.flipper:flipper-network-plugin:${FLIPPER_VERSION}") {
|
||||
exclude group:'com.facebook.flipper'
|
||||
}
|
||||
debugImplementation("com.facebook.flipper:flipper-fresco-plugin:${FLIPPER_VERSION}") {
|
||||
exclude group:'com.facebook.flipper'
|
||||
}
|
||||
|
||||
if (enableHermes) {
|
||||
def hermesPath = "../../node_modules/hermes-engine/android/";
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -3,13 +3,13 @@
|
|||
package="chat.rocket.reactnative">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<!-- <uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="android.permission.DOWNLOAD_WITHOUT_NOTIFICATION" />
|
||||
<uses-permission-sdk-23 android:name="android.permission.VIBRATE"/>
|
||||
<uses-permission android:name="android.permission.DOWNLOAD_WITHOUT_NOTIFICATION" /> -->
|
||||
<!-- <uses-permission-sdk-23 android:name="android.permission.VIBRATE"/> -->
|
||||
|
||||
<application
|
||||
android:name=".MainApplication"
|
||||
|
@ -30,7 +30,7 @@
|
|||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:label="@string/app_name"
|
||||
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
|
||||
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|uiMode"
|
||||
android:launchMode="singleTask"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:exported="true">
|
||||
|
|
Binary file not shown.
|
@ -9,9 +9,11 @@ import com.facebook.react.PackageList;
|
|||
import com.facebook.hermes.reactexecutor.HermesExecutorFactory;
|
||||
import com.facebook.react.bridge.JavaScriptExecutorFactory;
|
||||
import com.facebook.react.ReactApplication;
|
||||
import com.facebook.react.ReactInstanceManager;
|
||||
import com.facebook.react.ReactNativeHost;
|
||||
import com.facebook.react.ReactPackage;
|
||||
import com.facebook.soloader.SoLoader;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
|
||||
import chat.rocket.reactnative.generated.BasePackageList;
|
||||
|
||||
|
@ -39,7 +41,7 @@ import java.util.List;
|
|||
|
||||
public class MainApplication extends Application implements ReactApplication, INotificationsApplication {
|
||||
|
||||
private final ReactModuleRegistryProvider mModuleRegistryProvider = new ReactModuleRegistryProvider(new BasePackageList().getPackageList(), Arrays.<SingletonModule>asList());
|
||||
private final ReactModuleRegistryProvider mModuleRegistryProvider = new ReactModuleRegistryProvider(new BasePackageList().getPackageList(), null);
|
||||
|
||||
private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) {
|
||||
@Override
|
||||
|
@ -58,7 +60,11 @@ public class MainApplication extends Application implements ReactApplication, IN
|
|||
packages.add(new RNNotificationsPackage(MainApplication.this));
|
||||
packages.add(new WatermelonDBPackage());
|
||||
packages.add(new RNCViewPagerPackage());
|
||||
packages.add(new ModuleRegistryAdapter(mModuleRegistryProvider));
|
||||
// packages.add(new ModuleRegistryAdapter(mModuleRegistryProvider));
|
||||
List<ReactPackage> unimodules = Arrays.<ReactPackage>asList(
|
||||
new ModuleRegistryAdapter(mModuleRegistryProvider)
|
||||
);
|
||||
packages.addAll(unimodules);
|
||||
return packages;
|
||||
}
|
||||
|
||||
|
@ -82,6 +88,38 @@ public class MainApplication extends Application implements ReactApplication, IN
|
|||
public void onCreate() {
|
||||
super.onCreate();
|
||||
SoLoader.init(this, /* native exopackage */ false);
|
||||
initializeFlipper(this, getReactNativeHost().getReactInstanceManager());
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads Flipper in React Native templates. Call this in the onCreate method with something like
|
||||
* initializeFlipper(this, getReactNativeHost().getReactInstanceManager());
|
||||
*
|
||||
* @param context
|
||||
* @param reactInstanceManager
|
||||
*/
|
||||
private static void initializeFlipper(
|
||||
Context context, ReactInstanceManager reactInstanceManager) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
try {
|
||||
/*
|
||||
We use reflection here to pick up the class that initializes Flipper,
|
||||
since Flipper library is not available in release mode
|
||||
*/
|
||||
Class<?> aClass = Class.forName("chat.rocket.reactnative");
|
||||
aClass
|
||||
.getMethod("initializeFlipper", Context.class, ReactInstanceManager.class)
|
||||
.invoke(null, context, reactInstanceManager);
|
||||
} catch (ClassNotFoundException e) {
|
||||
e.printStackTrace();
|
||||
} catch (NoSuchMethodException e) {
|
||||
e.printStackTrace();
|
||||
} catch (IllegalAccessException e) {
|
||||
e.printStackTrace();
|
||||
} catch (InvocationTargetException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -11,6 +11,9 @@ public class BasePackageList {
|
|||
new expo.modules.constants.ConstantsPackage(),
|
||||
new expo.modules.filesystem.FileSystemPackage(),
|
||||
new expo.modules.haptics.HapticsPackage(),
|
||||
new expo.modules.imageloader.ImageLoaderPackage(),
|
||||
new expo.modules.keepawake.KeepAwakePackage(),
|
||||
new expo.modules.localauthentication.LocalAuthenticationPackage(),
|
||||
new expo.modules.permissions.PermissionsPackage(),
|
||||
new expo.modules.webbrowser.WebBrowserPackage()
|
||||
);
|
||||
|
|
|
@ -18,7 +18,7 @@ buildscript {
|
|||
}
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:3.4.2'
|
||||
classpath 'com.android.tools.build:gradle:3.5.2'
|
||||
classpath 'com.google.gms:google-services:4.2.0'
|
||||
classpath 'io.fabric.tools:gradle:1.28.1'
|
||||
classpath 'com.google.firebase:perf-plugin:1.2.1'
|
||||
|
@ -42,16 +42,14 @@ allprojects {
|
|||
url("$rootDir/../node_modules/jsc-android/dist")
|
||||
}
|
||||
maven {
|
||||
// We should change it when Jitsi-SDK release v2.4
|
||||
url("$rootDir/../node_modules/react-native-jitsi-meet/jitsi-sdk")
|
||||
// url "https://github.com/jitsi/jitsi-maven-repository/raw/master/releases"
|
||||
url "https://github.com/jitsi/jitsi-maven-repository/raw/master/releases"
|
||||
}
|
||||
|
||||
google()
|
||||
jcenter()
|
||||
|
||||
maven { url 'https://maven.google.com' }
|
||||
maven { url "https://jitpack.io" }
|
||||
maven { url 'https://www.jitpack.io' }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -64,6 +62,12 @@ subprojects { subproject ->
|
|||
defaultConfig {
|
||||
targetSdkVersion 28
|
||||
}
|
||||
variantFilter { variant ->
|
||||
def names = variant.flavors*.name
|
||||
if (names.contains("reactNative59")) {
|
||||
setIgnore(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,7 +19,11 @@
|
|||
# android.enableAapt2=false # commenting this makes notifications to stop working
|
||||
# android.useDeprecatedNdk=true
|
||||
org.gradle.jvmargs=-Xmx2048M -XX\:MaxHeapSize\=32g
|
||||
# AndroidX package structure to make it clearer which packages are bundled with the
|
||||
# Android operating system, and which are packaged with your app's APK
|
||||
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
||||
android.useAndroidX=true
|
||||
# Automatically convert third-party libraries to use AndroidX
|
||||
android.enableJetifier=true
|
||||
APPLICATIONID=chat.rocket.reactnative
|
||||
VERSIONNAME=4.5.1
|
||||
|
@ -29,3 +33,6 @@ KEYSTORE=my-upload-key.keystore
|
|||
KEY_ALIAS=my-key-alias
|
||||
KEYSTORE_PASSWORD=
|
||||
KEY_PASSWORD=
|
||||
|
||||
# Version of flipper SDK to use with React Native
|
||||
FLIPPER_VERSION=0.33.1
|
|
@ -1,5 +1,5 @@
|
|||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-5.5-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.0.1-all.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
|
@ -7,7 +7,7 @@
|
|||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
@ -44,7 +44,7 @@ APP_NAME="Gradle"
|
|||
APP_BASE_NAME=`basename "$0"`
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS=''
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD="maximum"
|
||||
|
@ -125,8 +125,8 @@ if $darwin; then
|
|||
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||
fi
|
||||
|
||||
# For Cygwin, switch paths to Windows format before running java
|
||||
if $cygwin ; then
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
|
||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||
|
|
|
@ -31,7 +31,7 @@ export const ROOMS = createRequestTypes('ROOMS', [
|
|||
'OPEN_SEARCH_HEADER',
|
||||
'CLOSE_SEARCH_HEADER'
|
||||
]);
|
||||
export const ROOM = createRequestTypes('ROOM', ['LEAVE', 'DELETE', 'REMOVED', 'USER_TYPING']);
|
||||
export const ROOM = createRequestTypes('ROOM', ['SUBSCRIBE', 'UNSUBSCRIBE', 'LEAVE', 'DELETE', 'REMOVED', 'CLOSE', 'FORWARD', 'USER_TYPING']);
|
||||
export const APP = createRequestTypes('APP', ['START', 'READY', 'INIT', 'INIT_LOCAL_SETTINGS']);
|
||||
export const MESSAGES = createRequestTypes('MESSAGES', ['REPLY_BROADCAST']);
|
||||
export const CREATE_CHANNEL = createRequestTypes('CREATE_CHANNEL', [...defaultTypes]);
|
||||
|
@ -64,3 +64,4 @@ export const INVITE_LINKS = createRequestTypes('INVITE_LINKS', [
|
|||
...defaultTypes
|
||||
]);
|
||||
export const SETTINGS = createRequestTypes('SETTINGS', ['CLEAR', 'ADD']);
|
||||
export const APP_STATE = createRequestTypes('APP_STATE', ['FOREGROUND', 'BACKGROUND']);
|
||||
|
|
|
@ -1,5 +1,19 @@
|
|||
import * as types from './actionsTypes';
|
||||
|
||||
export function subscribeRoom(rid) {
|
||||
return {
|
||||
type: types.ROOM.SUBSCRIBE,
|
||||
rid
|
||||
};
|
||||
}
|
||||
|
||||
export function unsubscribeRoom(rid) {
|
||||
return {
|
||||
type: types.ROOM.UNSUBSCRIBE,
|
||||
rid
|
||||
};
|
||||
}
|
||||
|
||||
export function leaveRoom(rid, t) {
|
||||
return {
|
||||
type: types.ROOM.LEAVE,
|
||||
|
@ -16,6 +30,21 @@ export function deleteRoom(rid, t) {
|
|||
};
|
||||
}
|
||||
|
||||
export function closeRoom(rid) {
|
||||
return {
|
||||
type: types.ROOM.CLOSE,
|
||||
rid
|
||||
};
|
||||
}
|
||||
|
||||
export function forwardRoom(rid, transferData) {
|
||||
return {
|
||||
type: types.ROOM.FORWARD,
|
||||
transferData,
|
||||
rid
|
||||
};
|
||||
}
|
||||
|
||||
export function removedRoom() {
|
||||
return {
|
||||
type: types.ROOM.REMOVED
|
||||
|
|
|
@ -46,7 +46,14 @@ export const themes = {
|
|||
messageboxBackground: '#ffffff',
|
||||
searchboxBackground: '#E6E6E7',
|
||||
buttonBackground: '#414852',
|
||||
buttonText: '#ffffff'
|
||||
buttonText: '#ffffff',
|
||||
passcodeBackground: '#EEEFF1',
|
||||
passcodeButtonActive: '#E4E7EA',
|
||||
passcodeLockIcon: '#6C727A',
|
||||
passcodePrimary: '#2F343D',
|
||||
passcodeSecondary: '#6C727A',
|
||||
passcodeDotEmpty: '#CBCED1',
|
||||
passcodeDotFull: '#6C727A'
|
||||
},
|
||||
dark: {
|
||||
backgroundColor: '#030b1b',
|
||||
|
@ -81,7 +88,14 @@ export const themes = {
|
|||
messageboxBackground: '#0b182c',
|
||||
searchboxBackground: '#192d4d',
|
||||
buttonBackground: '#414852',
|
||||
buttonText: '#ffffff'
|
||||
buttonText: '#ffffff',
|
||||
passcodeBackground: '#030C1B',
|
||||
passcodeButtonActive: '#0B182C',
|
||||
passcodeLockIcon: '#6C727A',
|
||||
passcodePrimary: '#FFFFFF',
|
||||
passcodeSecondary: '#CBCED1',
|
||||
passcodeDotEmpty: '#CBCED1',
|
||||
passcodeDotFull: '#6C727A'
|
||||
},
|
||||
black: {
|
||||
backgroundColor: '#000000',
|
||||
|
@ -100,7 +114,7 @@ export const themes = {
|
|||
infoText: '#6d6d72',
|
||||
tintColor: '#1e9bfe',
|
||||
auxiliaryTintColor: '#cdcdcd',
|
||||
actionTintColor: '#1ea1fe',
|
||||
actionTintColor: '#1e9bfe',
|
||||
separatorColor: '#272728',
|
||||
navbarBackground: '#0d0d0d',
|
||||
headerBorder: '#323232',
|
||||
|
@ -116,6 +130,13 @@ export const themes = {
|
|||
messageboxBackground: '#0d0d0d',
|
||||
searchboxBackground: '#1f1f1f',
|
||||
buttonBackground: '#414852',
|
||||
buttonText: '#ffffff'
|
||||
buttonText: '#ffffff',
|
||||
passcodeBackground: '#000000',
|
||||
passcodeButtonActive: '#0E0D0D',
|
||||
passcodeLockIcon: '#6C727A',
|
||||
passcodePrimary: '#FFFFFF',
|
||||
passcodeSecondary: '#CBCED1',
|
||||
passcodeDotEmpty: '#CBCED1',
|
||||
passcodeDotFull: '#6C727A'
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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;
|
|
@ -68,6 +68,9 @@ export default {
|
|||
LDAP_Enable: {
|
||||
type: 'valueAsBoolean'
|
||||
},
|
||||
Livechat_request_comment_when_closing_conversation: {
|
||||
type: 'valueAsBoolean'
|
||||
},
|
||||
Jitsi_Enabled: {
|
||||
type: 'valueAsBoolean'
|
||||
},
|
||||
|
@ -125,6 +128,9 @@ export default {
|
|||
uniqueID: {
|
||||
type: 'valueAsString'
|
||||
},
|
||||
UI_Allow_room_names_with_special_chars: {
|
||||
type: 'valueAsBoolean'
|
||||
},
|
||||
UI_Use_Real_Name: {
|
||||
type: 'valueAsBoolean'
|
||||
},
|
||||
|
@ -157,5 +163,11 @@ export default {
|
|||
},
|
||||
CAS_login_url: {
|
||||
type: 'valueAsString'
|
||||
},
|
||||
Force_Screen_Lock: {
|
||||
type: 'valueAsBoolean'
|
||||
},
|
||||
Force_Screen_Lock_After: {
|
||||
type: 'valueAsNumber'
|
||||
}
|
||||
};
|
||||
|
|
|
@ -2,12 +2,14 @@ import React from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
import { View } from 'react-native';
|
||||
import FastImage from 'react-native-fast-image';
|
||||
import Touchable from 'react-native-platform-touchable';
|
||||
import { settings as RocketChatSettings } from '@rocket.chat/sdk';
|
||||
import Touch from '../utils/touch';
|
||||
|
||||
import { avatarURL } from '../utils/avatar';
|
||||
import Emoji from './markdown/Emoji';
|
||||
|
||||
const Avatar = React.memo(({
|
||||
text, size, baseUrl, borderRadius, style, avatar, type, children, userId, token, onPress, theme
|
||||
text, size, baseUrl, borderRadius, style, avatar, type, children, userId, token, onPress, theme, emoji, getCustomEmoji
|
||||
}) => {
|
||||
const avatarStyle = {
|
||||
width: size,
|
||||
|
@ -23,7 +25,15 @@ const Avatar = React.memo(({
|
|||
type, text, size, userId, token, avatar, baseUrl
|
||||
});
|
||||
|
||||
let image = (
|
||||
let image = emoji ? (
|
||||
<Emoji
|
||||
theme={theme}
|
||||
baseUrl={baseUrl}
|
||||
getCustomEmoji={getCustomEmoji}
|
||||
isMessageContainsOnlyEmoji
|
||||
literal={emoji}
|
||||
/>
|
||||
) : (
|
||||
<FastImage
|
||||
style={avatarStyle}
|
||||
source={{
|
||||
|
@ -36,9 +46,9 @@ const Avatar = React.memo(({
|
|||
|
||||
if (onPress) {
|
||||
image = (
|
||||
<Touch onPress={onPress} theme={theme}>
|
||||
<Touchable onPress={onPress}>
|
||||
{image}
|
||||
</Touch>
|
||||
</Touchable>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -55,6 +65,7 @@ Avatar.propTypes = {
|
|||
style: PropTypes.any,
|
||||
text: PropTypes.string,
|
||||
avatar: PropTypes.string,
|
||||
emoji: PropTypes.string,
|
||||
size: PropTypes.number,
|
||||
borderRadius: PropTypes.number,
|
||||
type: PropTypes.string,
|
||||
|
@ -62,7 +73,8 @@ Avatar.propTypes = {
|
|||
userId: PropTypes.string,
|
||||
token: PropTypes.string,
|
||||
theme: PropTypes.string,
|
||||
onPress: PropTypes.func
|
||||
onPress: PropTypes.func,
|
||||
getCustomEmoji: PropTypes.func
|
||||
};
|
||||
|
||||
Avatar.defaultProps = {
|
||||
|
|
|
@ -23,7 +23,7 @@ export const FormContainerInner = ({ children }) => (
|
|||
</View>
|
||||
);
|
||||
|
||||
const FormContainer = ({ children, theme }) => (
|
||||
const FormContainer = ({ children, theme, testID }) => (
|
||||
<KeyboardView
|
||||
style={{ backgroundColor: themes[theme].backgroundColor }}
|
||||
contentContainerStyle={sharedStyles.container}
|
||||
|
@ -31,7 +31,7 @@ const FormContainer = ({ children, theme }) => (
|
|||
>
|
||||
<StatusBar theme={theme} />
|
||||
<ScrollView {...scrollPersistTaps} style={sharedStyles.container} contentContainerStyle={[sharedStyles.containerScrollView, styles.scrollView]}>
|
||||
<SafeAreaView style={sharedStyles.container} forceInset={{ top: 'never' }}>
|
||||
<SafeAreaView style={sharedStyles.container} forceInset={{ top: 'never' }} testID={testID}>
|
||||
{children}
|
||||
<AppVersion theme={theme} />
|
||||
</SafeAreaView>
|
||||
|
@ -41,6 +41,7 @@ const FormContainer = ({ children, theme }) => (
|
|||
|
||||
FormContainer.propTypes = {
|
||||
theme: PropTypes.string,
|
||||
testID: PropTypes.string,
|
||||
children: PropTypes.element
|
||||
};
|
||||
|
||||
|
|
|
@ -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;
|
|
@ -12,7 +12,7 @@ import sharedStyles from '../views/Styles';
|
|||
import { themes } from '../constants/colors';
|
||||
import { loginRequest as loginRequestAction } from '../actions/login';
|
||||
import Button from './Button';
|
||||
import OnboardingSeparator from './OnboardingSeparator';
|
||||
import OrSeparator from './OrSeparator';
|
||||
import Touch from '../utils/touch';
|
||||
import I18n from '../i18n';
|
||||
import random from '../utils/random';
|
||||
|
@ -252,12 +252,12 @@ class LoginServices extends React.PureComponent {
|
|||
style={styles.options}
|
||||
color={themes[theme].actionTintColor}
|
||||
/>
|
||||
<OnboardingSeparator theme={theme} />
|
||||
<OrSeparator theme={theme} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
if (length > 0 && separator) {
|
||||
return <OnboardingSeparator theme={theme} />;
|
||||
return <OrSeparator theme={theme} />;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -64,7 +64,7 @@ const MentionItem = ({
|
|||
content = (
|
||||
<>
|
||||
<Text style={[styles.slash, { backgroundColor: themes[theme].borderColor, color: themes[theme].tintColor }]}>/</Text>
|
||||
<Text style={[styles.mentionText, { color: themes[theme].titleText }]}>{ item.command}</Text>
|
||||
<Text style={[styles.mentionText, { color: themes[theme].titleText }]}>{item.id}</Text>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import {
|
|||
} from 'react-native';
|
||||
import { AudioRecorder, AudioUtils } from 'react-native-audio';
|
||||
import { BorderlessButton } from 'react-native-gesture-handler';
|
||||
import { activateKeepAwake, deactivateKeepAwake } from 'expo-keep-awake';
|
||||
import RNFetchBlob from 'rn-fetch-blob';
|
||||
|
||||
import styles from './styles';
|
||||
|
@ -59,7 +60,8 @@ export default class extends React.PureComponent {
|
|||
SampleRate: 22050,
|
||||
Channels: 1,
|
||||
AudioQuality: 'Low',
|
||||
AudioEncoding: 'aac'
|
||||
AudioEncoding: 'aac',
|
||||
OutputFormat: 'aac_adts'
|
||||
});
|
||||
|
||||
AudioRecorder.onProgress = (data) => {
|
||||
|
@ -74,12 +76,16 @@ export default class extends React.PureComponent {
|
|||
}
|
||||
};
|
||||
AudioRecorder.startRecording();
|
||||
|
||||
activateKeepAwake();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.recording) {
|
||||
this.cancelAudioMessage();
|
||||
}
|
||||
|
||||
deactivateKeepAwake();
|
||||
}
|
||||
|
||||
finishRecording = (didSucceed, filePath, size) => {
|
||||
|
|
|
@ -8,7 +8,7 @@ const SendButton = React.memo(({ theme, onPress }) => (
|
|||
onPress={onPress}
|
||||
testID='messagebox-send-message'
|
||||
accessibilityLabel='Send_message'
|
||||
icon='send1'
|
||||
icon='Send-active'
|
||||
theme={theme}
|
||||
/>
|
||||
));
|
||||
|
|
|
@ -190,7 +190,7 @@ class MessageBox extends Component {
|
|||
});
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
UNSAFE_componentWillReceiveProps(nextProps) {
|
||||
const { isFocused, editing, replying } = this.props;
|
||||
if (!isFocused()) {
|
||||
return;
|
||||
|
@ -306,9 +306,9 @@ class MessageBox extends Component {
|
|||
|
||||
if (!isTextEmpty) {
|
||||
try {
|
||||
const { start, end } = this.component._lastNativeSelection;
|
||||
const { start, end } = this.component?.lastNativeSelection;
|
||||
const cursor = Math.max(start, end);
|
||||
const lastNativeText = this.component._lastNativeText || '';
|
||||
const lastNativeText = this.component?.lastNativeText || '';
|
||||
// matches if text either starts with '/' or have (@,#,:) then it groups whatever comes next of mention type
|
||||
const regexp = /(#|@|:|^\/)([a-z0-9._-]+)$/im;
|
||||
const result = lastNativeText.substr(0, cursor).match(regexp);
|
||||
|
@ -339,7 +339,7 @@ class MessageBox extends Component {
|
|||
}
|
||||
const { trackingType } = this.state;
|
||||
const msg = this.text;
|
||||
const { start, end } = this.component._lastNativeSelection;
|
||||
const { start, end } = this.component?.lastNativeSelection;
|
||||
const cursor = Math.max(start, end);
|
||||
const regexp = /([a-z0-9._-]+)$/im;
|
||||
const result = msg.substr(0, cursor).replace(regexp, '');
|
||||
|
@ -383,8 +383,8 @@ class MessageBox extends Component {
|
|||
let newText = '';
|
||||
|
||||
// if messagebox has an active cursor
|
||||
if (this.component && this.component._lastNativeSelection) {
|
||||
const { start, end } = this.component._lastNativeSelection;
|
||||
if (this.component?.lastNativeSelection) {
|
||||
const { start, end } = this.component.lastNativeSelection;
|
||||
const cursor = Math.max(start, end);
|
||||
newText = `${ text.substr(0, cursor) }${ emoji }${ text.substr(cursor) }`;
|
||||
} else {
|
||||
|
|
|
@ -24,7 +24,7 @@ const styles = StyleSheet.create({
|
|||
}
|
||||
});
|
||||
|
||||
const DateSeparator = React.memo(({ theme }) => {
|
||||
const OrSeparator = React.memo(({ theme }) => {
|
||||
const line = { backgroundColor: themes[theme].borderColor };
|
||||
const text = { color: themes[theme].auxiliaryText };
|
||||
return (
|
||||
|
@ -36,8 +36,8 @@ const DateSeparator = React.memo(({ theme }) => {
|
|||
);
|
||||
});
|
||||
|
||||
DateSeparator.propTypes = {
|
||||
OrSeparator.propTypes = {
|
||||
theme: PropTypes.string
|
||||
};
|
||||
|
||||
export default DateSeparator;
|
||||
export default OrSeparator;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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
|
||||
}
|
||||
});
|
|
@ -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);
|
|
@ -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);
|
|
@ -0,0 +1,6 @@
|
|||
export const TYPE = {
|
||||
CHOOSE: 'choose',
|
||||
CONFIRM: 'confirm',
|
||||
ENTER: 'enter',
|
||||
LOCKED: 'locked'
|
||||
};
|
|
@ -0,0 +1,4 @@
|
|||
import PasscodeEnter from './PasscodeEnter';
|
||||
import PasscodeChoose from './PasscodeChoose';
|
||||
|
||||
export { PasscodeEnter, PasscodeChoose };
|
|
@ -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();
|
|
@ -2,7 +2,7 @@ import React from 'react';
|
|||
import { Image, StyleSheet } from 'react-native';
|
||||
import PropTypes from 'prop-types';
|
||||
import { CustomIcon } from '../lib/Icons';
|
||||
import { themes } from '../constants/colors';
|
||||
import { STATUS_COLORS, themes } from '../constants/colors';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
style: {
|
||||
|
@ -15,7 +15,7 @@ const styles = StyleSheet.create({
|
|||
});
|
||||
|
||||
const RoomTypeIcon = React.memo(({
|
||||
type, size, isGroupChat, style, theme
|
||||
type, size, isGroupChat, status, style, theme
|
||||
}) => {
|
||||
if (!type) {
|
||||
return null;
|
||||
|
@ -36,7 +36,7 @@ const RoomTypeIcon = React.memo(({
|
|||
}
|
||||
return <CustomIcon name='at' size={13} style={[styles.style, styles.discussion, { color }]} />;
|
||||
} if (type === 'l') {
|
||||
return <CustomIcon name='livechat' size={13} style={[styles.style, styles.discussion, { color }]} />;
|
||||
return <CustomIcon name='omnichannel' size={13} style={[styles.style, styles.discussion, { color: STATUS_COLORS[status] }]} />;
|
||||
}
|
||||
return <Image source={{ uri: 'lock' }} style={[styles.style, style, { width: size, height: size, tintColor: color }]} />;
|
||||
});
|
||||
|
@ -45,6 +45,7 @@ RoomTypeIcon.propTypes = {
|
|||
theme: PropTypes.string,
|
||||
type: PropTypes.string,
|
||||
isGroupChat: PropTypes.bool,
|
||||
status: PropTypes.string,
|
||||
size: PropTypes.number,
|
||||
style: PropTypes.object
|
||||
};
|
||||
|
|
|
@ -64,8 +64,10 @@ export default class RCTextInput extends React.PureComponent {
|
|||
inputRef: PropTypes.func,
|
||||
testID: PropTypes.string,
|
||||
iconLeft: PropTypes.string,
|
||||
iconRight: PropTypes.string,
|
||||
placeholder: PropTypes.string,
|
||||
left: PropTypes.element,
|
||||
onIconRightPress: PropTypes.func,
|
||||
theme: PropTypes.string
|
||||
}
|
||||
|
||||
|
@ -90,6 +92,19 @@ export default class RCTextInput extends React.PureComponent {
|
|||
);
|
||||
}
|
||||
|
||||
get iconRight() {
|
||||
const { iconRight, onIconRightPress, theme } = this.props;
|
||||
return (
|
||||
<BorderlessButton onPress={onIconRightPress} style={[styles.iconContainer, styles.iconRight]}>
|
||||
<CustomIcon
|
||||
name={iconRight}
|
||||
style={{ color: themes[theme].bodyText }}
|
||||
size={20}
|
||||
/>
|
||||
</BorderlessButton>
|
||||
);
|
||||
}
|
||||
|
||||
get iconPassword() {
|
||||
const { showPassword } = this.state;
|
||||
const { testID, theme } = this.props;
|
||||
|
@ -117,7 +132,7 @@ export default class RCTextInput extends React.PureComponent {
|
|||
render() {
|
||||
const { showPassword } = this.state;
|
||||
const {
|
||||
label, left, error, loading, secureTextEntry, containerStyle, inputRef, iconLeft, inputStyle, testID, placeholder, theme, ...inputProps
|
||||
label, left, error, loading, secureTextEntry, containerStyle, inputRef, iconLeft, iconRight, inputStyle, testID, placeholder, theme, ...inputProps
|
||||
} = this.props;
|
||||
const { dangerColor } = themes[theme];
|
||||
return (
|
||||
|
@ -140,7 +155,7 @@ export default class RCTextInput extends React.PureComponent {
|
|||
style={[
|
||||
styles.input,
|
||||
iconLeft && styles.inputIconLeft,
|
||||
secureTextEntry && styles.inputIconRight,
|
||||
(secureTextEntry || iconRight) && styles.inputIconRight,
|
||||
{
|
||||
backgroundColor: themes[theme].backgroundColor,
|
||||
borderColor: themes[theme].separatorColor,
|
||||
|
@ -165,6 +180,7 @@ export default class RCTextInput extends React.PureComponent {
|
|||
{...inputProps}
|
||||
/>
|
||||
{iconLeft ? this.iconLeft : null}
|
||||
{iconRight ? this.iconRight : null}
|
||||
{secureTextEntry ? this.iconPassword : null}
|
||||
{loading ? this.loading : null}
|
||||
{left}
|
||||
|
|
|
@ -92,7 +92,7 @@ const TwoFactor = React.memo(({ theme, split }) => {
|
|||
isVisible={visible}
|
||||
hideModalContentWhileAnimating
|
||||
>
|
||||
<View style={styles.container}>
|
||||
<View style={styles.container} testID='two-factor'>
|
||||
<View style={[styles.content, split && [sharedStyles.modal, sharedStyles.modalFormSheet], { backgroundColor: themes[theme].backgroundColor }]}>
|
||||
<Text style={[styles.title, { color }]}>{I18n.t(method?.title || 'Two_Factor_Authentication')}</Text>
|
||||
{method?.text ? <Text style={[styles.subtitle, { color }]}>{I18n.t(method.text)}</Text> : null}
|
||||
|
@ -106,6 +106,7 @@ const TwoFactor = React.memo(({ theme, split }) => {
|
|||
keyboardType={method?.keyboardType}
|
||||
secureTextEntry={method?.secureTextEntry}
|
||||
error={data.invalid && { error: 'totp-invalid', reason: I18n.t('Code_or_password_invalid') }}
|
||||
testID='two-factor-input'
|
||||
/>
|
||||
{isEmail && <Text style={[styles.sendEmail, { color }]} onPress={sendEmail}>{I18n.t('Send_me_the_code_again')}</Text>}
|
||||
<View style={styles.buttonContainer}>
|
||||
|
@ -123,6 +124,7 @@ const TwoFactor = React.memo(({ theme, split }) => {
|
|||
style={styles.button}
|
||||
onPress={onSubmit}
|
||||
theme={theme}
|
||||
testID='two-factor-send'
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
|
|
@ -12,11 +12,13 @@ import styles from './styles';
|
|||
|
||||
const keyExtractor = item => item.value.toString();
|
||||
|
||||
const Chip = ({ item, onSelect, theme }) => (
|
||||
const Chip = ({
|
||||
item, onSelect, style, theme
|
||||
}) => (
|
||||
<Touchable
|
||||
key={item.value}
|
||||
onPress={() => onSelect(item)}
|
||||
style={[styles.chip, { backgroundColor: themes[theme].auxiliaryBackground }]}
|
||||
style={[styles.chip, { backgroundColor: themes[theme].auxiliaryBackground }, style]}
|
||||
background={Touchable.Ripple(themes[theme].bannerBackground)}
|
||||
>
|
||||
<>
|
||||
|
@ -29,17 +31,21 @@ const Chip = ({ item, onSelect, theme }) => (
|
|||
Chip.propTypes = {
|
||||
item: PropTypes.object,
|
||||
onSelect: PropTypes.func,
|
||||
style: PropTypes.object,
|
||||
theme: PropTypes.string
|
||||
};
|
||||
|
||||
const Chips = ({ items, onSelect, theme }) => (
|
||||
const Chips = ({
|
||||
items, onSelect, style, theme
|
||||
}) => (
|
||||
<View style={styles.chips}>
|
||||
{items.map(item => <Chip key={keyExtractor(item)} item={item} onSelect={onSelect} theme={theme} />)}
|
||||
{items.map(item => <Chip key={keyExtractor(item)} item={item} onSelect={onSelect} style={style} theme={theme} />)}
|
||||
</View>
|
||||
);
|
||||
Chips.propTypes = {
|
||||
items: PropTypes.array,
|
||||
onSelect: PropTypes.func,
|
||||
style: PropTypes.object,
|
||||
theme: PropTypes.string
|
||||
};
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import React from 'react';
|
||||
import { View } from 'react-native';
|
||||
import { View, Text } from 'react-native';
|
||||
import PropTypes from 'prop-types';
|
||||
import Touchable from 'react-native-platform-touchable';
|
||||
|
||||
|
@ -9,16 +9,16 @@ import ActivityIndicator from '../../ActivityIndicator';
|
|||
import styles from './styles';
|
||||
|
||||
const Input = ({
|
||||
children, open, theme, loading, inputStyle, disabled
|
||||
children, onPress, theme, loading, inputStyle, placeholder, disabled
|
||||
}) => (
|
||||
<Touchable
|
||||
onPress={() => open(true)}
|
||||
onPress={onPress}
|
||||
style={[{ backgroundColor: themes[theme].backgroundColor }, inputStyle]}
|
||||
background={Touchable.Ripple(themes[theme].bannerBackground)}
|
||||
disabled={disabled}
|
||||
>
|
||||
<View style={[styles.input, { borderColor: themes[theme].separatorColor }]}>
|
||||
{children}
|
||||
{placeholder ? <Text style={[styles.pickerText, { color: themes[theme].auxiliaryText }]}>{placeholder}</Text> : children}
|
||||
{
|
||||
loading
|
||||
? <ActivityIndicator style={[styles.loading, styles.icon]} />
|
||||
|
@ -29,10 +29,11 @@ const Input = ({
|
|||
);
|
||||
Input.propTypes = {
|
||||
children: PropTypes.node,
|
||||
open: PropTypes.func,
|
||||
onPress: PropTypes.func,
|
||||
theme: PropTypes.string,
|
||||
inputStyle: PropTypes.object,
|
||||
disabled: PropTypes.bool,
|
||||
placeholder: PropTypes.string,
|
||||
loading: PropTypes.bool
|
||||
};
|
||||
|
||||
|
|
|
@ -43,14 +43,14 @@ export const MultiSelect = React.memo(({
|
|||
inputStyle,
|
||||
theme
|
||||
}) => {
|
||||
const [selected, select] = useState(values || []);
|
||||
const [selected, select] = useState(Array.isArray(values) ? values : []);
|
||||
const [open, setOpen] = useState(false);
|
||||
const [search, onSearchChange] = useState('');
|
||||
const [currentValue, setCurrentValue] = useState('');
|
||||
const [showContent, setShowContent] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (values) {
|
||||
if (Array.isArray(values)) {
|
||||
select(values);
|
||||
}
|
||||
}, [values]);
|
||||
|
@ -136,7 +136,7 @@ export const MultiSelect = React.memo(({
|
|||
/>
|
||||
) : (
|
||||
<Input
|
||||
open={onShow}
|
||||
onPress={onShow}
|
||||
theme={theme}
|
||||
loading={loading}
|
||||
disabled={disabled}
|
||||
|
@ -150,7 +150,7 @@ export const MultiSelect = React.memo(({
|
|||
const items = options.filter(option => selected.includes(option.value));
|
||||
button = (
|
||||
<Input
|
||||
open={onShow}
|
||||
onPress={onShow}
|
||||
theme={theme}
|
||||
loading={loading}
|
||||
disabled={disabled}
|
||||
|
|
|
@ -4,7 +4,6 @@ import PropTypes from 'prop-types';
|
|||
import { BLOCK_CONTEXT } from '@rocket.chat/ui-kit';
|
||||
|
||||
import { themes } from '../../constants/colors';
|
||||
import sharedStyles from '../../views/Styles';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
content: {
|
||||
|
@ -18,11 +17,7 @@ const styles = StyleSheet.create({
|
|||
},
|
||||
text: {
|
||||
flex: 1,
|
||||
padding: 4,
|
||||
fontSize: 16,
|
||||
lineHeight: 22,
|
||||
textAlignVertical: 'center',
|
||||
...sharedStyles.textRegular
|
||||
padding: 4
|
||||
},
|
||||
field: {
|
||||
marginVertical: 6
|
||||
|
@ -54,7 +49,7 @@ export const Section = ({
|
|||
accessory && accessoriesRight.includes(accessory.type) ? styles.row : styles.column
|
||||
]}
|
||||
>
|
||||
{text ? <Text style={[styles.text, { color: themes[theme].bodyText }]}>{parser.text(text)}</Text> : null}
|
||||
{text ? <View style={styles.text}>{parser.text(text)}</View> : null}
|
||||
{fields ? <Fields fields={fields} theme={theme} parser={parser} /> : null}
|
||||
{accessory ? <Accessory element={{ blockId, appId, ...accessory }} parser={parser} /> : null}
|
||||
</View>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/* eslint-disable class-methods-use-this */
|
||||
import React, { useContext } from 'react';
|
||||
import { StyleSheet } from 'react-native';
|
||||
import { StyleSheet, Text } from 'react-native';
|
||||
import {
|
||||
uiKitMessage,
|
||||
UiKitParserMessage,
|
||||
|
@ -13,8 +13,9 @@ import Markdown from '../markdown';
|
|||
import Button from '../Button';
|
||||
import TextInput from '../TextInput';
|
||||
|
||||
import { useBlockContext } from './utils';
|
||||
import { useBlockContext, textParser } from './utils';
|
||||
import { themes } from '../../constants/colors';
|
||||
import sharedStyles from '../../views/Styles';
|
||||
|
||||
import { Divider } from './Divider';
|
||||
import { Section } from './Section';
|
||||
|
@ -37,6 +38,12 @@ const styles = StyleSheet.create({
|
|||
},
|
||||
button: {
|
||||
marginBottom: 16
|
||||
},
|
||||
text: {
|
||||
fontSize: 16,
|
||||
lineHeight: 22,
|
||||
textAlignVertical: 'center',
|
||||
...sharedStyles.textRegular
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -46,7 +53,7 @@ class MessageParser extends UiKitParserMessage {
|
|||
text({ text, type } = { text: '' }, context) {
|
||||
const { theme } = useContext(ThemeContext);
|
||||
if (type !== 'mrkdwn') {
|
||||
return text;
|
||||
return <Text style={[styles.text, { color: themes[theme].bodyText }]}>{text}</Text>;
|
||||
}
|
||||
|
||||
const isContext = context === BLOCK_CONTEXT.CONTEXT;
|
||||
|
@ -70,7 +77,7 @@ class MessageParser extends UiKitParserMessage {
|
|||
<Button
|
||||
key={actionId}
|
||||
type={style}
|
||||
title={this.text(text)}
|
||||
title={textParser([text])}
|
||||
loading={loading}
|
||||
onPress={() => action({ value })}
|
||||
style={styles.button}
|
||||
|
|
|
@ -11,11 +11,10 @@ const AtMention = React.memo(({
|
|||
}) => {
|
||||
let mentionStyle = { ...styles.mention, color: themes[theme].buttonText };
|
||||
if (mention === 'all' || mention === 'here') {
|
||||
mentionStyle = {
|
||||
...mentionStyle,
|
||||
...styles.mentionAll
|
||||
};
|
||||
} else if (mention === username) {
|
||||
return <Text style={[mentionStyle, styles.mentionAll, ...style]}>{mention}</Text>;
|
||||
}
|
||||
|
||||
if (mention === username) {
|
||||
mentionStyle = {
|
||||
...mentionStyle,
|
||||
backgroundColor: themes[theme].actionTintColor
|
||||
|
|
|
@ -9,10 +9,10 @@ import { themes } from '../../constants/colors';
|
|||
import styles from './styles';
|
||||
|
||||
const Emoji = React.memo(({
|
||||
emojiName, literal, isMessageContainsOnlyEmoji, getCustomEmoji, baseUrl, customEmojis, style = [], theme
|
||||
literal, isMessageContainsOnlyEmoji, getCustomEmoji, baseUrl, customEmojis = true, style = [], theme
|
||||
}) => {
|
||||
const emojiUnicode = shortnameToUnicode(literal);
|
||||
const emoji = getCustomEmoji && getCustomEmoji(emojiName);
|
||||
const emoji = getCustomEmoji && getCustomEmoji(literal.replace(/:/g, ''));
|
||||
if (emoji && customEmojis) {
|
||||
return (
|
||||
<CustomEmoji
|
||||
|
@ -36,7 +36,6 @@ const Emoji = React.memo(({
|
|||
});
|
||||
|
||||
Emoji.propTypes = {
|
||||
emojiName: PropTypes.string,
|
||||
literal: PropTypes.string,
|
||||
isMessageContainsOnlyEmoji: PropTypes.bool,
|
||||
getCustomEmoji: PropTypes.func,
|
||||
|
|
|
@ -261,13 +261,12 @@ class Markdown extends PureComponent {
|
|||
);
|
||||
}
|
||||
|
||||
renderEmoji = ({ emojiName, literal }) => {
|
||||
renderEmoji = ({ literal }) => {
|
||||
const {
|
||||
getCustomEmoji, baseUrl, customEmojis = true, style, theme
|
||||
getCustomEmoji, baseUrl, customEmojis, style, theme
|
||||
} = this.props;
|
||||
return (
|
||||
<MarkdownEmoji
|
||||
emojiName={emojiName}
|
||||
literal={literal}
|
||||
isMessageContainsOnlyEmoji={this.isMessageContainsOnlyEmoji}
|
||||
getCustomEmoji={getCustomEmoji}
|
||||
|
|
|
@ -8,7 +8,7 @@ import Video from './Video';
|
|||
import Reply from './Reply';
|
||||
|
||||
const Attachments = React.memo(({
|
||||
attachments, timeFormat, user, baseUrl, showAttachment, getCustomEmoji, theme
|
||||
attachments, timeFormat, showAttachment, getCustomEmoji, theme
|
||||
}) => {
|
||||
if (!attachments || attachments.length === 0) {
|
||||
return null;
|
||||
|
@ -16,25 +16,23 @@ const Attachments = React.memo(({
|
|||
|
||||
return attachments.map((file, index) => {
|
||||
if (file.image_url) {
|
||||
return <Image key={file.image_url} file={file} user={user} baseUrl={baseUrl} showAttachment={showAttachment} getCustomEmoji={getCustomEmoji} theme={theme} />;
|
||||
return <Image key={file.image_url} file={file} showAttachment={showAttachment} getCustomEmoji={getCustomEmoji} theme={theme} />;
|
||||
}
|
||||
if (file.audio_url) {
|
||||
return <Audio key={file.audio_url} file={file} user={user} baseUrl={baseUrl} getCustomEmoji={getCustomEmoji} theme={theme} />;
|
||||
return <Audio key={file.audio_url} file={file} getCustomEmoji={getCustomEmoji} theme={theme} />;
|
||||
}
|
||||
if (file.video_url) {
|
||||
return <Video key={file.video_url} file={file} user={user} baseUrl={baseUrl} showAttachment={showAttachment} getCustomEmoji={getCustomEmoji} theme={theme} />;
|
||||
return <Video key={file.video_url} file={file} showAttachment={showAttachment} getCustomEmoji={getCustomEmoji} theme={theme} />;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
return <Reply key={index} index={index} attachment={file} timeFormat={timeFormat} user={user} baseUrl={baseUrl} getCustomEmoji={getCustomEmoji} theme={theme} />;
|
||||
return <Reply key={index} index={index} attachment={file} timeFormat={timeFormat} getCustomEmoji={getCustomEmoji} theme={theme} />;
|
||||
});
|
||||
}, (prevProps, nextProps) => isEqual(prevProps.attachments, nextProps.attachments) && prevProps.theme === nextProps.theme);
|
||||
|
||||
Attachments.propTypes = {
|
||||
attachments: PropTypes.array,
|
||||
timeFormat: PropTypes.string,
|
||||
user: PropTypes.object,
|
||||
baseUrl: PropTypes.string,
|
||||
showAttachment: PropTypes.func,
|
||||
getCustomEmoji: PropTypes.func,
|
||||
theme: PropTypes.string
|
||||
|
|
|
@ -3,18 +3,31 @@ import PropTypes from 'prop-types';
|
|||
import {
|
||||
View, StyleSheet, Text, Easing, Dimensions
|
||||
} from 'react-native';
|
||||
import Video from 'react-native-video';
|
||||
import { Audio } from 'expo-av';
|
||||
import Slider from '@react-native-community/slider';
|
||||
import moment from 'moment';
|
||||
import equal from 'deep-equal';
|
||||
import Touchable from 'react-native-platform-touchable';
|
||||
import { activateKeepAwake, deactivateKeepAwake } from 'expo-keep-awake';
|
||||
|
||||
import Touchable from './Touchable';
|
||||
import Markdown from '../markdown';
|
||||
import { CustomIcon } from '../../lib/Icons';
|
||||
import sharedStyles from '../../views/Styles';
|
||||
import { themes } from '../../constants/colors';
|
||||
import { isAndroid, isIOS } from '../../utils/deviceInfo';
|
||||
import { withSplit } from '../../split';
|
||||
import MessageContext from './Context';
|
||||
import ActivityIndicator from '../ActivityIndicator';
|
||||
|
||||
const mode = {
|
||||
allowsRecordingIOS: false,
|
||||
playsInSilentModeIOS: true,
|
||||
staysActiveInBackground: false,
|
||||
shouldDuckAndroid: true,
|
||||
playThroughEarpieceAndroid: false,
|
||||
interruptionModeIOS: Audio.INTERRUPTION_MODE_IOS_DO_NOT_MIX,
|
||||
interruptionModeAndroid: Audio.INTERRUPTION_MODE_ANDROID_DO_NOT_MIX
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
audioContainer: {
|
||||
|
@ -31,6 +44,9 @@ const styles = StyleSheet.create({
|
|||
alignItems: 'center',
|
||||
backgroundColor: 'transparent'
|
||||
},
|
||||
audioLoading: {
|
||||
marginHorizontal: 8
|
||||
},
|
||||
slider: {
|
||||
flex: 1
|
||||
},
|
||||
|
@ -51,29 +67,36 @@ const sliderAnimationConfig = {
|
|||
delay: 0
|
||||
};
|
||||
|
||||
const Button = React.memo(({ paused, onPress, theme }) => (
|
||||
const Button = React.memo(({
|
||||
loading, paused, onPress, theme
|
||||
}) => (
|
||||
<Touchable
|
||||
style={styles.playPauseButton}
|
||||
onPress={onPress}
|
||||
hitSlop={BUTTON_HIT_SLOP}
|
||||
background={Touchable.SelectableBackgroundBorderless()}
|
||||
>
|
||||
<CustomIcon name={paused ? 'play' : 'pause'} size={36} color={themes[theme].tintColor} />
|
||||
{
|
||||
loading
|
||||
? <ActivityIndicator style={[styles.playPauseButton, styles.audioLoading]} theme={theme} />
|
||||
: <CustomIcon name={paused ? 'play' : 'pause'} size={36} color={themes[theme].tintColor} />
|
||||
}
|
||||
</Touchable>
|
||||
));
|
||||
|
||||
Button.propTypes = {
|
||||
loading: PropTypes.bool,
|
||||
paused: PropTypes.bool,
|
||||
theme: PropTypes.string,
|
||||
onPress: PropTypes.func
|
||||
};
|
||||
Button.displayName = 'MessageAudioButton';
|
||||
|
||||
class Audio extends React.Component {
|
||||
class MessageAudio extends React.Component {
|
||||
static contextType = MessageContext;
|
||||
|
||||
static propTypes = {
|
||||
file: PropTypes.object.isRequired,
|
||||
baseUrl: PropTypes.string.isRequired,
|
||||
user: PropTypes.object.isRequired,
|
||||
theme: PropTypes.string,
|
||||
split: PropTypes.bool,
|
||||
getCustomEmoji: PropTypes.func
|
||||
|
@ -81,18 +104,39 @@ class Audio extends React.Component {
|
|||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const { baseUrl, file, user } = props;
|
||||
this.state = {
|
||||
loading: false,
|
||||
currentTime: 0,
|
||||
duration: 0,
|
||||
paused: true,
|
||||
uri: `${ baseUrl }${ file.audio_url }?rc_uid=${ user.id }&rc_token=${ user.token }`
|
||||
paused: true
|
||||
};
|
||||
|
||||
this.sound = new Audio.Sound();
|
||||
this.sound.setOnPlaybackStatusUpdate(this.onPlaybackStatusUpdate);
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
const { file } = this.props;
|
||||
const { baseUrl, user } = this.context;
|
||||
|
||||
let url = file.audio_url;
|
||||
if (!url.startsWith('http')) {
|
||||
url = `${ baseUrl }${ file.audio_url }`;
|
||||
}
|
||||
|
||||
this.setState({ loading: true });
|
||||
try {
|
||||
await Audio.setAudioModeAsync(mode);
|
||||
await this.sound.loadAsync({ uri: `${ url }?rc_uid=${ user.id }&rc_token=${ user.token }` });
|
||||
} catch {
|
||||
// Do nothing
|
||||
}
|
||||
this.setState({ loading: false });
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
const {
|
||||
currentTime, duration, paused, uri
|
||||
currentTime, duration, paused, loading
|
||||
} = this.state;
|
||||
const { file, split, theme } = this.props;
|
||||
if (nextProps.theme !== theme) {
|
||||
|
@ -107,58 +151,108 @@ class Audio extends React.Component {
|
|||
if (nextState.paused !== paused) {
|
||||
return true;
|
||||
}
|
||||
if (nextState.uri !== uri) {
|
||||
return true;
|
||||
}
|
||||
if (!equal(nextProps.file, file)) {
|
||||
return true;
|
||||
}
|
||||
if (nextProps.split !== split) {
|
||||
return true;
|
||||
}
|
||||
if (nextState.loading !== loading) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
const { paused } = this.state;
|
||||
if (paused) {
|
||||
deactivateKeepAwake();
|
||||
} else {
|
||||
activateKeepAwake();
|
||||
}
|
||||
}
|
||||
|
||||
async componentWillUnmount() {
|
||||
try {
|
||||
await this.sound.stopAsync();
|
||||
} catch {
|
||||
// Do nothing
|
||||
}
|
||||
}
|
||||
|
||||
onPlaybackStatusUpdate = (status) => {
|
||||
if (status) {
|
||||
this.onLoad(status);
|
||||
this.onProgress(status);
|
||||
this.onEnd(status);
|
||||
}
|
||||
}
|
||||
|
||||
onLoad = (data) => {
|
||||
this.setState({ duration: data.duration > 0 ? data.duration : 0 });
|
||||
const duration = data.durationMillis / 1000;
|
||||
this.setState({ duration: duration > 0 ? duration : 0 });
|
||||
}
|
||||
|
||||
onProgress = (data) => {
|
||||
const { duration } = this.state;
|
||||
if (data.currentTime <= duration) {
|
||||
this.setState({ currentTime: data.currentTime });
|
||||
const currentTime = data.positionMillis / 1000;
|
||||
if (currentTime <= duration) {
|
||||
this.setState({ currentTime });
|
||||
}
|
||||
}
|
||||
|
||||
onEnd = () => {
|
||||
this.setState({ paused: true, currentTime: 0 });
|
||||
requestAnimationFrame(() => {
|
||||
this.player.seek(0);
|
||||
});
|
||||
onEnd = async(data) => {
|
||||
if (data.didJustFinish) {
|
||||
try {
|
||||
await this.sound.stopAsync();
|
||||
this.setState({ paused: true, currentTime: 0 });
|
||||
} catch {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get duration() {
|
||||
const { duration } = this.state;
|
||||
return formatTime(duration);
|
||||
const { currentTime, duration } = this.state;
|
||||
return formatTime(currentTime || duration);
|
||||
}
|
||||
|
||||
setRef = ref => this.player = ref;
|
||||
|
||||
togglePlayPause = () => {
|
||||
const { paused } = this.state;
|
||||
this.setState({ paused: !paused });
|
||||
this.setState({ paused: !paused }, this.playPause);
|
||||
}
|
||||
|
||||
onValueChange = value => this.setState({ currentTime: value });
|
||||
playPause = async() => {
|
||||
const { paused } = this.state;
|
||||
try {
|
||||
if (paused) {
|
||||
await this.sound.pauseAsync();
|
||||
} else {
|
||||
await this.sound.playAsync();
|
||||
}
|
||||
} catch {
|
||||
// Do nothing
|
||||
}
|
||||
}
|
||||
|
||||
onValueChange = async(value) => {
|
||||
try {
|
||||
this.setState({ currentTime: value });
|
||||
await this.sound.setPositionAsync(value * 1000);
|
||||
} catch {
|
||||
// Do nothing
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
uri, paused, currentTime, duration
|
||||
loading, paused, currentTime, duration
|
||||
} = this.state;
|
||||
const {
|
||||
user, baseUrl, file, getCustomEmoji, split, theme
|
||||
file, getCustomEmoji, split, theme
|
||||
} = this.props;
|
||||
const { description } = file;
|
||||
const { baseUrl, user } = this.context;
|
||||
|
||||
if (!baseUrl) {
|
||||
return null;
|
||||
|
@ -173,17 +267,7 @@ class Audio extends React.Component {
|
|||
split && sharedStyles.tabletContent
|
||||
]}
|
||||
>
|
||||
<Video
|
||||
ref={this.setRef}
|
||||
source={{ uri }}
|
||||
onLoad={this.onLoad}
|
||||
onProgress={this.onProgress}
|
||||
onEnd={this.onEnd}
|
||||
paused={paused}
|
||||
repeat={false}
|
||||
ignoreSilentSwitch='ignore'
|
||||
/>
|
||||
<Button paused={paused} onPress={this.togglePlayPause} theme={theme} />
|
||||
<Button loading={loading} paused={paused} onPress={this.togglePlayPause} theme={theme} />
|
||||
<Slider
|
||||
style={styles.slider}
|
||||
value={currentTime}
|
||||
|
@ -205,4 +289,4 @@ class Audio extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
export default withSplit(Audio);
|
||||
export default withSplit(MessageAudio);
|
||||
|
|
|
@ -1,17 +1,19 @@
|
|||
import React from 'react';
|
||||
import React, { useContext } from 'react';
|
||||
import { View, Text } from 'react-native';
|
||||
import Touchable from 'react-native-platform-touchable';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import Touchable from './Touchable';
|
||||
import { CustomIcon } from '../../lib/Icons';
|
||||
import styles from './styles';
|
||||
import { BUTTON_HIT_SLOP } from './utils';
|
||||
import I18n from '../../i18n';
|
||||
import { themes } from '../../constants/colors';
|
||||
import MessageContext from './Context';
|
||||
|
||||
const Broadcast = React.memo(({
|
||||
author, user, broadcast, replyBroadcast, theme
|
||||
author, broadcast, theme
|
||||
}) => {
|
||||
const { user, replyBroadcast } = useContext(MessageContext);
|
||||
const isOwn = author._id === user.id;
|
||||
if (broadcast && !isOwn) {
|
||||
return (
|
||||
|
@ -36,10 +38,8 @@ const Broadcast = React.memo(({
|
|||
|
||||
Broadcast.propTypes = {
|
||||
author: PropTypes.object,
|
||||
user: PropTypes.object,
|
||||
broadcast: PropTypes.bool,
|
||||
theme: PropTypes.string,
|
||||
replyBroadcast: PropTypes.func
|
||||
theme: PropTypes.string
|
||||
};
|
||||
Broadcast.displayName = 'MessageBroadcast';
|
||||
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import React from 'react';
|
||||
import { View, Text } from 'react-native';
|
||||
import Touchable from 'react-native-platform-touchable';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import Touchable from './Touchable';
|
||||
import { formatLastMessage, BUTTON_HIT_SLOP } from './utils';
|
||||
import styles from './styles';
|
||||
import I18n from '../../i18n';
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React from 'react';
|
||||
import React, { useContext } from 'react';
|
||||
import { Text, View } from 'react-native';
|
||||
import PropTypes from 'prop-types';
|
||||
import equal from 'deep-equal';
|
||||
|
@ -8,6 +8,7 @@ import styles from './styles';
|
|||
import Markdown from '../markdown';
|
||||
import { getInfoMessage } from './utils';
|
||||
import { themes } from '../../constants/colors';
|
||||
import MessageContext from './Context';
|
||||
|
||||
const Content = React.memo((props) => {
|
||||
if (props.isInfo) {
|
||||
|
@ -26,12 +27,13 @@ const Content = React.memo((props) => {
|
|||
if (props.tmid && !props.msg) {
|
||||
content = <Text style={[styles.text, { color: themes[props.theme].bodyText }]}>{I18n.t('Sent_an_attachment')}</Text>;
|
||||
} else {
|
||||
const { baseUrl, user } = useContext(MessageContext);
|
||||
content = (
|
||||
<Markdown
|
||||
msg={props.msg}
|
||||
baseUrl={props.baseUrl}
|
||||
baseUrl={baseUrl}
|
||||
getCustomEmoji={props.getCustomEmoji}
|
||||
username={props.user.username}
|
||||
username={user.username}
|
||||
isEdited={props.isEdited}
|
||||
numberOfLines={(props.tmid && !props.isThreadRoom) ? 1 : 0}
|
||||
preview={props.tmid && !props.isThreadRoom}
|
||||
|
@ -77,8 +79,6 @@ Content.propTypes = {
|
|||
msg: PropTypes.string,
|
||||
theme: PropTypes.string,
|
||||
isEdited: PropTypes.bool,
|
||||
baseUrl: PropTypes.string,
|
||||
user: PropTypes.object,
|
||||
getCustomEmoji: PropTypes.func,
|
||||
channels: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
|
||||
mentions: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
import React from 'react';
|
||||
|
||||
const MessageContext = React.createContext();
|
||||
export default MessageContext;
|
|
@ -1,20 +1,22 @@
|
|||
import React from 'react';
|
||||
import React, { useContext } from 'react';
|
||||
import { View, Text } from 'react-native';
|
||||
import Touchable from 'react-native-platform-touchable';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import Touchable from './Touchable';
|
||||
import { formatLastMessage, formatMessageCount, BUTTON_HIT_SLOP } from './utils';
|
||||
import styles from './styles';
|
||||
import I18n from '../../i18n';
|
||||
import { CustomIcon } from '../../lib/Icons';
|
||||
import { DISCUSSION } from './constants';
|
||||
import { themes } from '../../constants/colors';
|
||||
import MessageContext from './Context';
|
||||
|
||||
const Discussion = React.memo(({
|
||||
msg, dcount, dlm, onDiscussionPress, theme
|
||||
msg, dcount, dlm, theme
|
||||
}) => {
|
||||
const time = formatLastMessage(dlm);
|
||||
const buttonText = formatMessageCount(dcount, DISCUSSION);
|
||||
const { onDiscussionPress } = useContext(MessageContext);
|
||||
return (
|
||||
<>
|
||||
<Text style={[styles.startedDiscussion, { color: themes[theme].auxiliaryText }]}>{I18n.t('Started_discussion')}</Text>
|
||||
|
@ -55,8 +57,7 @@ Discussion.propTypes = {
|
|||
msg: PropTypes.string,
|
||||
dcount: PropTypes.number,
|
||||
dlm: PropTypes.string,
|
||||
theme: PropTypes.string,
|
||||
onDiscussionPress: PropTypes.func
|
||||
theme: PropTypes.string
|
||||
};
|
||||
Discussion.displayName = 'MessageDiscussion';
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ import shortnameToUnicode from '../../utils/shortnameToUnicode';
|
|||
import CustomEmoji from '../EmojiPicker/CustomEmoji';
|
||||
|
||||
const Emoji = React.memo(({
|
||||
content, standardEmojiStyle, customEmojiStyle, baseUrl, getCustomEmoji
|
||||
content, baseUrl, standardEmojiStyle, customEmojiStyle, getCustomEmoji
|
||||
}) => {
|
||||
const parsedContent = content.replace(/^:|:$/g, '');
|
||||
const emoji = getCustomEmoji(parsedContent);
|
||||
|
@ -18,9 +18,9 @@ const Emoji = React.memo(({
|
|||
|
||||
Emoji.propTypes = {
|
||||
content: PropTypes.string,
|
||||
baseUrl: PropTypes.string,
|
||||
standardEmojiStyle: PropTypes.object,
|
||||
customEmojiStyle: PropTypes.object,
|
||||
baseUrl: PropTypes.string,
|
||||
getCustomEmoji: PropTypes.func
|
||||
};
|
||||
Emoji.displayName = 'MessageEmoji';
|
||||
|
|
|
@ -1,18 +1,19 @@
|
|||
import React from 'react';
|
||||
import React, { useContext } from 'react';
|
||||
import { View } from 'react-native';
|
||||
import PropTypes from 'prop-types';
|
||||
import FastImage from 'react-native-fast-image';
|
||||
import equal from 'deep-equal';
|
||||
import Touchable from 'react-native-platform-touchable';
|
||||
import { createImageProgress } from 'react-native-image-progress';
|
||||
import * as Progress from 'react-native-progress';
|
||||
|
||||
import Touchable from './Touchable';
|
||||
import Markdown from '../markdown';
|
||||
import styles from './styles';
|
||||
import { formatAttachmentUrl } from '../../lib/utils';
|
||||
import { withSplit } from '../../split';
|
||||
import { themes } from '../../constants/colors';
|
||||
import sharedStyles from '../../views/Styles';
|
||||
import MessageContext from './Context';
|
||||
|
||||
const ImageProgress = createImageProgress(FastImage);
|
||||
|
||||
|
@ -41,8 +42,9 @@ export const MessageImage = React.memo(({ img, theme }) => (
|
|||
));
|
||||
|
||||
const ImageContainer = React.memo(({
|
||||
file, imageUrl, baseUrl, user, showAttachment, getCustomEmoji, split, theme
|
||||
file, imageUrl, showAttachment, getCustomEmoji, split, theme
|
||||
}) => {
|
||||
const { baseUrl, user } = useContext(MessageContext);
|
||||
const img = imageUrl || formatAttachmentUrl(file.image_url, user.id, user.token, baseUrl);
|
||||
if (!img) {
|
||||
return null;
|
||||
|
@ -71,8 +73,6 @@ const ImageContainer = React.memo(({
|
|||
ImageContainer.propTypes = {
|
||||
file: PropTypes.object,
|
||||
imageUrl: PropTypes.string,
|
||||
baseUrl: PropTypes.string,
|
||||
user: PropTypes.object,
|
||||
showAttachment: PropTypes.func,
|
||||
theme: PropTypes.string,
|
||||
getCustomEmoji: PropTypes.func,
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
import React from 'react';
|
||||
import React, { useContext } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { View } from 'react-native';
|
||||
import Touchable from 'react-native-platform-touchable';
|
||||
|
||||
import MessageContext from './Context';
|
||||
|
||||
import User from './User';
|
||||
import styles from './styles';
|
||||
import RepliedThread from './RepliedThread';
|
||||
|
@ -111,10 +113,11 @@ const MessageTouchable = React.memo((props) => {
|
|||
</View>
|
||||
);
|
||||
}
|
||||
const { onPress, onLongPress } = useContext(MessageContext);
|
||||
return (
|
||||
<Touchable
|
||||
onLongPress={props.onLongPress}
|
||||
onPress={props.onPress}
|
||||
onLongPress={onLongPress}
|
||||
onPress={onPress}
|
||||
disabled={props.isInfo || props.archived || props.isTemp}
|
||||
>
|
||||
<View>
|
||||
|
@ -129,9 +132,7 @@ MessageTouchable.propTypes = {
|
|||
hasError: PropTypes.bool,
|
||||
isInfo: PropTypes.bool,
|
||||
isTemp: PropTypes.bool,
|
||||
archived: PropTypes.bool,
|
||||
onLongPress: PropTypes.func,
|
||||
onPress: PropTypes.func
|
||||
archived: PropTypes.bool
|
||||
};
|
||||
|
||||
Message.propTypes = {
|
||||
|
@ -143,7 +144,6 @@ Message.propTypes = {
|
|||
hasError: PropTypes.bool,
|
||||
style: PropTypes.any,
|
||||
onLongPress: PropTypes.func,
|
||||
onPress: PropTypes.func,
|
||||
isReadReceiptEnabled: PropTypes.bool,
|
||||
unread: PropTypes.bool,
|
||||
theme: PropTypes.string
|
||||
|
|
|
@ -1,34 +1,34 @@
|
|||
import React from 'react';
|
||||
import React, { useContext } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { TouchableOpacity } from 'react-native';
|
||||
|
||||
import Avatar from '../Avatar';
|
||||
import styles from './styles';
|
||||
import MessageContext from './Context';
|
||||
|
||||
const MessageAvatar = React.memo(({
|
||||
isHeader, avatar, author, baseUrl, user, small, navToRoomInfo
|
||||
isHeader, avatar, author, small, navToRoomInfo, emoji, getCustomEmoji, theme
|
||||
}) => {
|
||||
const { baseUrl, user } = useContext(MessageContext);
|
||||
if (isHeader && author) {
|
||||
const navParam = {
|
||||
t: 'd',
|
||||
rid: author._id
|
||||
};
|
||||
return (
|
||||
<TouchableOpacity
|
||||
onPress={() => navToRoomInfo(navParam)}
|
||||
disabled={author._id === user.id}
|
||||
>
|
||||
<Avatar
|
||||
style={small ? styles.avatarSmall : styles.avatar}
|
||||
text={avatar ? '' : author.username}
|
||||
size={small ? 20 : 36}
|
||||
borderRadius={small ? 2 : 4}
|
||||
avatar={avatar}
|
||||
baseUrl={baseUrl}
|
||||
userId={user.id}
|
||||
token={user.token}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
<Avatar
|
||||
style={small ? styles.avatarSmall : styles.avatar}
|
||||
text={avatar ? '' : author.username}
|
||||
size={small ? 20 : 36}
|
||||
borderRadius={small ? 2 : 4}
|
||||
onPress={author._id === user.id ? undefined : () => navToRoomInfo(navParam)}
|
||||
getCustomEmoji={getCustomEmoji}
|
||||
avatar={avatar}
|
||||
emoji={emoji}
|
||||
baseUrl={baseUrl}
|
||||
userId={user.id}
|
||||
token={user.token}
|
||||
theme={theme}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
|
@ -37,11 +37,12 @@ const MessageAvatar = React.memo(({
|
|||
MessageAvatar.propTypes = {
|
||||
isHeader: PropTypes.bool,
|
||||
avatar: PropTypes.string,
|
||||
emoji: PropTypes.string,
|
||||
author: PropTypes.obj,
|
||||
baseUrl: PropTypes.string,
|
||||
user: PropTypes.obj,
|
||||
small: PropTypes.bool,
|
||||
navToRoomInfo: PropTypes.func
|
||||
navToRoomInfo: PropTypes.func,
|
||||
getCustomEmoji: PropTypes.func,
|
||||
theme: PropTypes.string
|
||||
};
|
||||
MessageAvatar.displayName = 'MessageAvatar';
|
||||
|
||||
|
|
|
@ -1,16 +1,18 @@
|
|||
import React from 'react';
|
||||
import Touchable from 'react-native-platform-touchable';
|
||||
import React, { useContext } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import Touchable from './Touchable';
|
||||
import { CustomIcon } from '../../lib/Icons';
|
||||
import styles from './styles';
|
||||
import { BUTTON_HIT_SLOP } from './utils';
|
||||
import { themes } from '../../constants/colors';
|
||||
import MessageContext from './Context';
|
||||
|
||||
const MessageError = React.memo(({ hasError, onErrorPress, theme }) => {
|
||||
const MessageError = React.memo(({ hasError, theme }) => {
|
||||
if (!hasError) {
|
||||
return null;
|
||||
}
|
||||
const { onErrorPress } = useContext(MessageContext);
|
||||
return (
|
||||
<Touchable onPress={onErrorPress} style={styles.errorButton} hitSlop={BUTTON_HIT_SLOP}>
|
||||
<CustomIcon name='warning' color={themes[theme].dangerColor} size={18} />
|
||||
|
@ -20,7 +22,6 @@ const MessageError = React.memo(({ hasError, onErrorPress, theme }) => {
|
|||
|
||||
MessageError.propTypes = {
|
||||
hasError: PropTypes.bool,
|
||||
onErrorPress: PropTypes.func,
|
||||
theme: PropTypes.string
|
||||
};
|
||||
MessageError.displayName = 'MessageError';
|
||||
|
|
|
@ -1,33 +1,40 @@
|
|||
import React from 'react';
|
||||
import React, { useContext } from 'react';
|
||||
import { View, Text } from 'react-native';
|
||||
import Touchable from 'react-native-platform-touchable';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import Touchable from './Touchable';
|
||||
import { CustomIcon } from '../../lib/Icons';
|
||||
import styles from './styles';
|
||||
import Emoji from './Emoji';
|
||||
import { BUTTON_HIT_SLOP } from './utils';
|
||||
import { themes } from '../../constants/colors';
|
||||
import { withTheme } from '../../theme';
|
||||
import MessageContext from './Context';
|
||||
|
||||
const AddReaction = React.memo(({ reactionInit, theme }) => (
|
||||
<Touchable
|
||||
onPress={reactionInit}
|
||||
key='message-add-reaction'
|
||||
testID='message-add-reaction'
|
||||
style={[styles.reactionButton, { backgroundColor: themes[theme].backgroundColor }]}
|
||||
background={Touchable.Ripple(themes[theme].bannerBackground)}
|
||||
hitSlop={BUTTON_HIT_SLOP}
|
||||
>
|
||||
<View style={[styles.reactionContainer, { borderColor: themes[theme].borderColor }]}>
|
||||
<CustomIcon name='add-reaction' size={21} color={themes[theme].tintColor} />
|
||||
</View>
|
||||
</Touchable>
|
||||
));
|
||||
const AddReaction = React.memo(({ theme }) => {
|
||||
const { reactionInit } = useContext(MessageContext);
|
||||
return (
|
||||
<Touchable
|
||||
onPress={reactionInit}
|
||||
key='message-add-reaction'
|
||||
testID='message-add-reaction'
|
||||
style={[styles.reactionButton, { backgroundColor: themes[theme].backgroundColor }]}
|
||||
background={Touchable.Ripple(themes[theme].bannerBackground)}
|
||||
hitSlop={BUTTON_HIT_SLOP}
|
||||
>
|
||||
<View style={[styles.reactionContainer, { borderColor: themes[theme].borderColor }]}>
|
||||
<CustomIcon name='add-reaction' size={21} color={themes[theme].tintColor} />
|
||||
</View>
|
||||
</Touchable>
|
||||
);
|
||||
});
|
||||
|
||||
const Reaction = React.memo(({
|
||||
reaction, user, onReactionLongPress, onReactionPress, baseUrl, getCustomEmoji, theme
|
||||
reaction, getCustomEmoji, theme
|
||||
}) => {
|
||||
const {
|
||||
onReactionPress, onReactionLongPress, baseUrl, user
|
||||
} = useContext(MessageContext);
|
||||
const reacted = reaction.usernames.findIndex(item => item === user.username) !== -1;
|
||||
return (
|
||||
<Touchable
|
||||
|
@ -54,7 +61,7 @@ const Reaction = React.memo(({
|
|||
});
|
||||
|
||||
const Reactions = React.memo(({
|
||||
reactions, user, baseUrl, onReactionPress, reactionInit, onReactionLongPress, getCustomEmoji, theme
|
||||
reactions, getCustomEmoji, theme
|
||||
}) => {
|
||||
if (!Array.isArray(reactions) || reactions.length === 0) {
|
||||
return null;
|
||||
|
@ -65,25 +72,17 @@ const Reactions = React.memo(({
|
|||
<Reaction
|
||||
key={reaction.emoji}
|
||||
reaction={reaction}
|
||||
user={user}
|
||||
baseUrl={baseUrl}
|
||||
onReactionLongPress={onReactionLongPress}
|
||||
onReactionPress={onReactionPress}
|
||||
getCustomEmoji={getCustomEmoji}
|
||||
theme={theme}
|
||||
/>
|
||||
))}
|
||||
<AddReaction reactionInit={reactionInit} theme={theme} />
|
||||
<AddReaction theme={theme} />
|
||||
</View>
|
||||
);
|
||||
});
|
||||
|
||||
Reaction.propTypes = {
|
||||
reaction: PropTypes.object,
|
||||
user: PropTypes.object,
|
||||
baseUrl: PropTypes.string,
|
||||
onReactionPress: PropTypes.func,
|
||||
onReactionLongPress: PropTypes.func,
|
||||
getCustomEmoji: PropTypes.func,
|
||||
theme: PropTypes.string
|
||||
};
|
||||
|
@ -91,18 +90,12 @@ Reaction.displayName = 'MessageReaction';
|
|||
|
||||
Reactions.propTypes = {
|
||||
reactions: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
|
||||
user: PropTypes.object,
|
||||
baseUrl: PropTypes.string,
|
||||
onReactionPress: PropTypes.func,
|
||||
reactionInit: PropTypes.func,
|
||||
onReactionLongPress: PropTypes.func,
|
||||
getCustomEmoji: PropTypes.func,
|
||||
theme: PropTypes.string
|
||||
};
|
||||
Reactions.displayName = 'MessageReactions';
|
||||
|
||||
AddReaction.propTypes = {
|
||||
reactionInit: PropTypes.func,
|
||||
theme: PropTypes.string
|
||||
};
|
||||
AddReaction.displayName = 'MessageAddReaction';
|
||||
|
|
|
@ -1,15 +1,16 @@
|
|||
import React from 'react';
|
||||
import React, { useContext } from 'react';
|
||||
import { View, Text, StyleSheet } from 'react-native';
|
||||
import PropTypes from 'prop-types';
|
||||
import moment from 'moment';
|
||||
import Touchable from 'react-native-platform-touchable';
|
||||
import isEqual from 'deep-equal';
|
||||
|
||||
import Touchable from './Touchable';
|
||||
import Markdown from '../markdown';
|
||||
import openLink from '../../utils/openLink';
|
||||
import sharedStyles from '../../views/Styles';
|
||||
import { themes } from '../../constants/colors';
|
||||
import { withSplit } from '../../split';
|
||||
import MessageContext from './Context';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
button: {
|
||||
|
@ -79,12 +80,13 @@ const Title = React.memo(({ attachment, timeFormat, theme }) => {
|
|||
});
|
||||
|
||||
const Description = React.memo(({
|
||||
attachment, baseUrl, user, getCustomEmoji, theme
|
||||
attachment, getCustomEmoji, theme
|
||||
}) => {
|
||||
const text = attachment.text || attachment.title;
|
||||
if (!text) {
|
||||
return null;
|
||||
}
|
||||
const { baseUrl, user } = useContext(MessageContext);
|
||||
return (
|
||||
<Markdown
|
||||
msg={text}
|
||||
|
@ -124,11 +126,12 @@ const Fields = React.memo(({ attachment, theme }) => {
|
|||
}, (prevProps, nextProps) => isEqual(prevProps.attachment.fields, nextProps.attachment.fields) && prevProps.theme === nextProps.theme);
|
||||
|
||||
const Reply = React.memo(({
|
||||
attachment, timeFormat, baseUrl, user, index, getCustomEmoji, split, theme
|
||||
attachment, timeFormat, index, getCustomEmoji, split, theme
|
||||
}) => {
|
||||
if (!attachment) {
|
||||
return null;
|
||||
}
|
||||
const { baseUrl, user } = useContext(MessageContext);
|
||||
|
||||
const onPress = () => {
|
||||
let url = attachment.title_link || attachment.author_link;
|
||||
|
@ -136,7 +139,10 @@ const Reply = React.memo(({
|
|||
return;
|
||||
}
|
||||
if (attachment.type === 'file') {
|
||||
url = `${ baseUrl }${ url }?rc_uid=${ user.id }&rc_token=${ user.token }`;
|
||||
if (!url.startsWith('http')) {
|
||||
url = `${ baseUrl }${ url }`;
|
||||
}
|
||||
url = `${ url }?rc_uid=${ user.id }&rc_token=${ user.token }`;
|
||||
}
|
||||
openLink(url, theme);
|
||||
};
|
||||
|
@ -160,8 +166,6 @@ const Reply = React.memo(({
|
|||
<Description
|
||||
attachment={attachment}
|
||||
timeFormat={timeFormat}
|
||||
baseUrl={baseUrl}
|
||||
user={user}
|
||||
getCustomEmoji={getCustomEmoji}
|
||||
theme={theme}
|
||||
/>
|
||||
|
@ -174,8 +178,6 @@ const Reply = React.memo(({
|
|||
Reply.propTypes = {
|
||||
attachment: PropTypes.object,
|
||||
timeFormat: PropTypes.string,
|
||||
baseUrl: PropTypes.string,
|
||||
user: PropTypes.object,
|
||||
index: PropTypes.number,
|
||||
theme: PropTypes.string,
|
||||
getCustomEmoji: PropTypes.func,
|
||||
|
@ -192,8 +194,6 @@ Title.displayName = 'MessageReplyTitle';
|
|||
|
||||
Description.propTypes = {
|
||||
attachment: PropTypes.object,
|
||||
baseUrl: PropTypes.string,
|
||||
user: PropTypes.object,
|
||||
getCustomEmoji: PropTypes.func,
|
||||
theme: PropTypes.string
|
||||
};
|
||||
|
|
|
@ -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;
|
|
@ -1,12 +1,12 @@
|
|||
import React from 'react';
|
||||
import React, { useContext } from 'react';
|
||||
import {
|
||||
View, Text, StyleSheet, Clipboard
|
||||
} from 'react-native';
|
||||
import PropTypes from 'prop-types';
|
||||
import FastImage from 'react-native-fast-image';
|
||||
import Touchable from 'react-native-platform-touchable';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
|
||||
import Touchable from './Touchable';
|
||||
import openLink from '../../utils/openLink';
|
||||
import sharedStyles from '../../views/Styles';
|
||||
import { themes } from '../../constants/colors';
|
||||
|
@ -15,6 +15,7 @@ import { withSplit } from '../../split';
|
|||
import { LISTENER } from '../Toast';
|
||||
import EventEmitter from '../../utils/events';
|
||||
import I18n from '../../i18n';
|
||||
import MessageContext from './Context';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
button: {
|
||||
|
@ -52,10 +53,11 @@ const styles = StyleSheet.create({
|
|||
}
|
||||
});
|
||||
|
||||
const UrlImage = React.memo(({ image, user, baseUrl }) => {
|
||||
const UrlImage = React.memo(({ image }) => {
|
||||
if (!image) {
|
||||
return null;
|
||||
}
|
||||
const { baseUrl, user } = useContext(MessageContext);
|
||||
image = image.includes('http') ? image : `${ baseUrl }/${ image }?rc_uid=${ user.id }&rc_token=${ user.token }`;
|
||||
return <FastImage source={{ uri: image }} style={styles.image} resizeMode={FastImage.resizeMode.cover} />;
|
||||
}, (prevProps, nextProps) => prevProps.image === nextProps.image);
|
||||
|
@ -79,7 +81,7 @@ const UrlContent = React.memo(({ title, description, theme }) => (
|
|||
});
|
||||
|
||||
const Url = React.memo(({
|
||||
url, index, user, baseUrl, split, theme
|
||||
url, index, split, theme
|
||||
}) => {
|
||||
if (!url) {
|
||||
return null;
|
||||
|
@ -109,7 +111,7 @@ const Url = React.memo(({
|
|||
background={Touchable.Ripple(themes[theme].bannerBackground)}
|
||||
>
|
||||
<>
|
||||
<UrlImage image={url.image} user={user} baseUrl={baseUrl} />
|
||||
<UrlImage image={url.image} />
|
||||
<UrlContent title={url.title} description={url.description} theme={theme} />
|
||||
</>
|
||||
</Touchable>
|
||||
|
@ -117,21 +119,19 @@ const Url = React.memo(({
|
|||
}, (oldProps, newProps) => isEqual(oldProps.url, newProps.url) && oldProps.split === newProps.split && oldProps.theme === newProps.theme);
|
||||
|
||||
const Urls = React.memo(({
|
||||
urls, user, baseUrl, split, theme
|
||||
urls, split, theme
|
||||
}) => {
|
||||
if (!urls || urls.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return urls.map((url, index) => (
|
||||
<Url url={url} key={url.url} index={index} user={user} baseUrl={baseUrl} split={split} theme={theme} />
|
||||
<Url url={url} key={url.url} index={index} split={split} theme={theme} />
|
||||
));
|
||||
}, (oldProps, newProps) => isEqual(oldProps.urls, newProps.urls) && oldProps.split === newProps.split && oldProps.theme === newProps.theme);
|
||||
|
||||
UrlImage.propTypes = {
|
||||
image: PropTypes.string,
|
||||
user: PropTypes.object,
|
||||
baseUrl: PropTypes.string
|
||||
image: PropTypes.string
|
||||
};
|
||||
UrlImage.displayName = 'MessageUrlImage';
|
||||
|
||||
|
@ -145,8 +145,6 @@ UrlContent.displayName = 'MessageUrlContent';
|
|||
Url.propTypes = {
|
||||
url: PropTypes.object.isRequired,
|
||||
index: PropTypes.number,
|
||||
user: PropTypes.object,
|
||||
baseUrl: PropTypes.string,
|
||||
theme: PropTypes.string,
|
||||
split: PropTypes.bool
|
||||
};
|
||||
|
@ -154,8 +152,6 @@ Url.displayName = 'MessageUrl';
|
|||
|
||||
Urls.propTypes = {
|
||||
urls: PropTypes.array,
|
||||
user: PropTypes.object,
|
||||
baseUrl: PropTypes.string,
|
||||
theme: PropTypes.string,
|
||||
split: PropTypes.bool
|
||||
};
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React from 'react';
|
||||
import React, { useContext } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
View, Text, StyleSheet, TouchableOpacity
|
||||
|
@ -11,6 +11,7 @@ import { withTheme } from '../../theme';
|
|||
import MessageError from './MessageError';
|
||||
import sharedStyles from '../../views/Styles';
|
||||
import messageStyles from './styles';
|
||||
import MessageContext from './Context';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
|
@ -35,13 +36,14 @@ const styles = StyleSheet.create({
|
|||
});
|
||||
|
||||
const User = React.memo(({
|
||||
isHeader, useRealName, author, alias, ts, timeFormat, hasError, theme, navToRoomInfo, user, ...props
|
||||
isHeader, useRealName, author, alias, ts, timeFormat, hasError, theme, navToRoomInfo, ...props
|
||||
}) => {
|
||||
if (isHeader || hasError) {
|
||||
const navParam = {
|
||||
t: 'd',
|
||||
rid: author._id
|
||||
};
|
||||
const { user } = useContext(MessageContext);
|
||||
const username = (useRealName && author.name) || author.username;
|
||||
const aliasUsername = alias ? (<Text style={[styles.alias, { color: themes[theme].auxiliaryText }]}> @{username}</Text>) : null;
|
||||
const time = moment(ts).format(timeFormat);
|
||||
|
@ -49,15 +51,14 @@ const User = React.memo(({
|
|||
return (
|
||||
<View style={styles.container}>
|
||||
<TouchableOpacity
|
||||
style={styles.titleContainer}
|
||||
onPress={() => navToRoomInfo(navParam)}
|
||||
disabled={author._id === user.id}
|
||||
>
|
||||
<View style={styles.titleContainer}>
|
||||
<Text style={[styles.username, { color: themes[theme].titleText }]} numberOfLines={1}>
|
||||
{alias || username}
|
||||
{aliasUsername}
|
||||
</Text>
|
||||
</View>
|
||||
<Text style={[styles.username, { color: themes[theme].titleText }]} numberOfLines={1}>
|
||||
{alias || username}
|
||||
{aliasUsername}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<Text style={[messageStyles.time, { color: themes[theme].auxiliaryText }]}>{time}</Text>
|
||||
{ hasError && <MessageError hasError={hasError} theme={theme} {...props} /> }
|
||||
|
@ -76,7 +77,6 @@ User.propTypes = {
|
|||
ts: PropTypes.instanceOf(Date),
|
||||
timeFormat: PropTypes.string,
|
||||
theme: PropTypes.string,
|
||||
user: PropTypes.obj,
|
||||
navToRoomInfo: PropTypes.func
|
||||
};
|
||||
User.displayName = 'MessageUser';
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import React from 'react';
|
||||
import React, { useContext } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { StyleSheet } from 'react-native';
|
||||
import Touchable from 'react-native-platform-touchable';
|
||||
import isEqual from 'deep-equal';
|
||||
|
||||
import Touchable from './Touchable';
|
||||
import Markdown from '../markdown';
|
||||
import openLink from '../../utils/openLink';
|
||||
import { isIOS, isTablet } from '../../utils/deviceInfo';
|
||||
|
@ -11,6 +11,7 @@ import { CustomIcon } from '../../lib/Icons';
|
|||
import { formatAttachmentUrl } from '../../lib/utils';
|
||||
import { themes } from '../../constants/colors';
|
||||
import sharedStyles from '../../views/Styles';
|
||||
import MessageContext from './Context';
|
||||
|
||||
const SUPPORTED_TYPES = ['video/quicktime', 'video/mp4', ...(isIOS ? [] : ['video/3gp', 'video/mkv'])];
|
||||
const isTypeSupported = type => SUPPORTED_TYPES.indexOf(type) !== -1;
|
||||
|
@ -27,12 +28,12 @@ const styles = StyleSheet.create({
|
|||
});
|
||||
|
||||
const Video = React.memo(({
|
||||
file, baseUrl, user, showAttachment, getCustomEmoji, theme
|
||||
file, showAttachment, getCustomEmoji, theme
|
||||
}) => {
|
||||
const { baseUrl, user } = useContext(MessageContext);
|
||||
if (!baseUrl) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const onPress = () => {
|
||||
if (isTypeSupported(file.video_type)) {
|
||||
return showAttachment(file);
|
||||
|
@ -61,8 +62,6 @@ const Video = React.memo(({
|
|||
|
||||
Video.propTypes = {
|
||||
file: PropTypes.object,
|
||||
baseUrl: PropTypes.string,
|
||||
user: PropTypes.object,
|
||||
showAttachment: PropTypes.func,
|
||||
getCustomEmoji: PropTypes.func,
|
||||
theme: PropTypes.string
|
||||
|
|
|
@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
|
|||
import { KeyboardUtils } from 'react-native-keyboard-input';
|
||||
|
||||
import Message from './Message';
|
||||
import MessageContext from './Context';
|
||||
import debounce from '../../utils/debounce';
|
||||
import { SYSTEM_MESSAGES, getMessageTranslation } from './utils';
|
||||
import messagesStatus from '../../constants/messagesStatus';
|
||||
|
@ -229,7 +230,7 @@ class MessageContainer extends React.Component {
|
|||
item, user, style, archived, baseUrl, useRealName, broadcast, fetchThreadName, customThreadTimeFormat, showAttachment, timeFormat, isReadReceiptEnabled, autoTranslateRoom, autoTranslateLanguage, navToRoomInfo, getCustomEmoji, isThreadRoom, callJitsi, blockAction, rid, theme
|
||||
} = this.props;
|
||||
const {
|
||||
id, msg, ts, attachments, urls, reactions, t, avatar, u, alias, editedBy, role, drid, dcount, dlm, tmid, tcount, tlm, tmsg, mentions, channels, unread, blocks, autoTranslate: autoTranslateMessage
|
||||
id, msg, ts, attachments, urls, reactions, t, avatar, emoji, u, alias, editedBy, role, drid, dcount, dlm, tmid, tcount, tlm, tmsg, mentions, channels, unread, blocks, autoTranslate: autoTranslateMessage
|
||||
} = item;
|
||||
|
||||
let message = msg;
|
||||
|
@ -240,63 +241,69 @@ class MessageContainer extends React.Component {
|
|||
}
|
||||
|
||||
return (
|
||||
<Message
|
||||
id={id}
|
||||
msg={message}
|
||||
rid={rid}
|
||||
author={u}
|
||||
ts={ts}
|
||||
type={t}
|
||||
attachments={attachments}
|
||||
blocks={blocks}
|
||||
urls={urls}
|
||||
reactions={reactions}
|
||||
alias={alias}
|
||||
avatar={avatar}
|
||||
user={user}
|
||||
timeFormat={timeFormat}
|
||||
customThreadTimeFormat={customThreadTimeFormat}
|
||||
style={style}
|
||||
archived={archived}
|
||||
broadcast={broadcast}
|
||||
baseUrl={baseUrl}
|
||||
useRealName={useRealName}
|
||||
isReadReceiptEnabled={isReadReceiptEnabled}
|
||||
unread={unread}
|
||||
role={role}
|
||||
drid={drid}
|
||||
dcount={dcount}
|
||||
dlm={dlm}
|
||||
tmid={tmid}
|
||||
tcount={tcount}
|
||||
tlm={tlm}
|
||||
tmsg={tmsg}
|
||||
fetchThreadName={fetchThreadName}
|
||||
mentions={mentions}
|
||||
channels={channels}
|
||||
isEdited={editedBy && !!editedBy.username}
|
||||
isHeader={this.isHeader}
|
||||
isThreadReply={this.isThreadReply}
|
||||
isThreadSequential={this.isThreadSequential}
|
||||
isThreadRoom={isThreadRoom}
|
||||
isInfo={this.isInfo}
|
||||
isTemp={this.isTemp}
|
||||
hasError={this.hasError}
|
||||
onErrorPress={this.onErrorPress}
|
||||
onPress={this.onPress}
|
||||
onLongPress={this.onLongPress}
|
||||
onReactionLongPress={this.onReactionLongPress}
|
||||
onReactionPress={this.onReactionPress}
|
||||
replyBroadcast={this.replyBroadcast}
|
||||
reactionInit={this.reactionInit}
|
||||
onDiscussionPress={this.onDiscussionPress}
|
||||
showAttachment={showAttachment}
|
||||
getCustomEmoji={getCustomEmoji}
|
||||
navToRoomInfo={navToRoomInfo}
|
||||
callJitsi={callJitsi}
|
||||
blockAction={blockAction}
|
||||
theme={theme}
|
||||
/>
|
||||
<MessageContext.Provider
|
||||
value={{
|
||||
user,
|
||||
baseUrl,
|
||||
onPress: this.onPress,
|
||||
onLongPress: this.onLongPress,
|
||||
reactionInit: this.reactionInit,
|
||||
onErrorPress: this.onErrorPress,
|
||||
replyBroadcast: this.replyBroadcast,
|
||||
onReactionPress: this.onReactionPress,
|
||||
onDiscussionPress: this.onDiscussionPress,
|
||||
onReactionLongPress: this.onReactionLongPress
|
||||
}}
|
||||
>
|
||||
<Message
|
||||
id={id}
|
||||
msg={message}
|
||||
rid={rid}
|
||||
author={u}
|
||||
ts={ts}
|
||||
type={t}
|
||||
attachments={attachments}
|
||||
blocks={blocks}
|
||||
urls={urls}
|
||||
reactions={reactions}
|
||||
alias={alias}
|
||||
avatar={avatar}
|
||||
emoji={emoji}
|
||||
timeFormat={timeFormat}
|
||||
customThreadTimeFormat={customThreadTimeFormat}
|
||||
style={style}
|
||||
archived={archived}
|
||||
broadcast={broadcast}
|
||||
useRealName={useRealName}
|
||||
isReadReceiptEnabled={isReadReceiptEnabled}
|
||||
unread={unread}
|
||||
role={role}
|
||||
drid={drid}
|
||||
dcount={dcount}
|
||||
dlm={dlm}
|
||||
tmid={tmid}
|
||||
tcount={tcount}
|
||||
tlm={tlm}
|
||||
tmsg={tmsg}
|
||||
fetchThreadName={fetchThreadName}
|
||||
mentions={mentions}
|
||||
channels={channels}
|
||||
isEdited={editedBy && !!editedBy.username}
|
||||
isHeader={this.isHeader}
|
||||
isThreadReply={this.isThreadReply}
|
||||
isThreadSequential={this.isThreadSequential}
|
||||
isThreadRoom={isThreadRoom}
|
||||
isInfo={this.isInfo}
|
||||
isTemp={this.isTemp}
|
||||
hasError={this.hasError}
|
||||
showAttachment={showAttachment}
|
||||
getCustomEmoji={getCustomEmoji}
|
||||
navToRoomInfo={navToRoomInfo}
|
||||
callJitsi={callJitsi}
|
||||
blockAction={blockAction}
|
||||
theme={theme}
|
||||
/>
|
||||
</MessageContext.Provider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ export default {
|
|||
'error-could-not-change-email': 'E-Mail konnte nicht geändert werden',
|
||||
'error-could-not-change-name': 'Name konnte nicht geändert werden',
|
||||
'error-could-not-change-username': 'Benutzername konnte nicht geändert werden',
|
||||
'error-could-not-change-status': 'Status konnte nicht geändert werden',
|
||||
'error-delete-protected-role': 'Eine geschützte Rolle kann nicht gelöscht werden',
|
||||
'error-department-not-found': 'Abteilung nicht gefunden',
|
||||
'error-direct-message-file-upload-not-allowed': 'Dateifreigabe in direkten Nachrichten nicht zulässig',
|
||||
|
@ -17,6 +18,7 @@ export default {
|
|||
'error-email-domain-blacklisted': 'Die E-Mail-Domain wird auf die schwarze Liste gesetzt',
|
||||
'error-email-send-failed': 'Fehler beim Versuch, eine E-Mail zu senden: {{message}}',
|
||||
'error-save-image': 'Fehler beim Speichern des Bildes',
|
||||
'error-save-video': 'Fehler beim Speichern des Videos',
|
||||
'error-field-unavailable': '{{field}} wird bereits verwendet :(',
|
||||
'error-file-too-large': 'Datei ist zu groß',
|
||||
'error-importer-not-defined': 'Der Import wurde nicht korrekt definiert, es fehlt die Importklasse.',
|
||||
|
@ -81,12 +83,14 @@ export default {
|
|||
Activity: 'Aktivität',
|
||||
Add_Reaction: 'Reaktion hinzufügen',
|
||||
Add_Server: 'Server hinzufügen',
|
||||
Add_users: 'Nutzer hinzufügen',
|
||||
Add_users: 'Benutzer hinzufügen',
|
||||
Admin_Panel: 'Admin-Panel',
|
||||
Agent: 'Agent',
|
||||
Alert: 'Benachrichtigung',
|
||||
alert: 'Benachrichtigung',
|
||||
alerts: 'Benachrichtigungen',
|
||||
All_users_in_the_channel_can_write_new_messages: 'Alle Benutzer im Kanal können neue Nachrichten schreiben',
|
||||
A_meaningful_name_for_the_discussion_room: 'Ein aussagekräftiger Name für den Diskussionsraum',
|
||||
All: 'Alles',
|
||||
All_Messages: 'Alle Nachrichten',
|
||||
Allow_Reactions: 'Reaktionen zulassen',
|
||||
|
@ -130,12 +134,15 @@ export default {
|
|||
Click_to_join: 'Klicken um teilzunehmen!',
|
||||
Close: 'Schließen',
|
||||
Close_emoji_selector: 'Schließen Sie die Emoji-Auswahl',
|
||||
Closing_chat: 'Chat schließen',
|
||||
Change_language_loading: 'Ändere Sprache.',
|
||||
Chat_closed_by_agent: 'Chat durch den Agenten geschlossen',
|
||||
Choose: 'Wählen',
|
||||
Choose_from_library: 'Aus der Bibliothek auswählen',
|
||||
Choose_file: 'Datei auswählen',
|
||||
Choose_where_you_want_links_be_opened: 'Entscheide, wie Links geöffnet werden sollen',
|
||||
Code: 'Code',
|
||||
Code_or_password_invalid: 'Code oder Passwort sind falsch',
|
||||
Collaborative: 'Kollaborativ',
|
||||
Confirm: 'Bestätigen',
|
||||
Connect: 'Verbinden',
|
||||
|
@ -147,6 +154,7 @@ export default {
|
|||
Continue_with: 'Weitermachen mit',
|
||||
Copied_to_clipboard: 'In die Zwischenablage kopiert!',
|
||||
Copy: 'Kopieren',
|
||||
Conversation: 'Konversationen',
|
||||
Permalink: 'Permalink',
|
||||
Certificate_password: 'Zertifikats-Passwort',
|
||||
Clear_cache: 'Lokalen Server-Cache leeren',
|
||||
|
@ -154,14 +162,18 @@ export default {
|
|||
Whats_the_password_for_your_certificate: 'Wie lautet das Passwort für Ihr Zertifikat?',
|
||||
Create_account: 'Ein Konto erstellen',
|
||||
Create_Channel: 'Kanal erstellen',
|
||||
Create_Direct_Messages: 'Direkt-Nachricht erstellen',
|
||||
Create_Discussion: 'Diskussion erstellen',
|
||||
Created_snippet: 'Erstellt ein Snippet',
|
||||
Create_a_new_workspace: 'Erstellen Sie einen neuen Arbeitsbereich',
|
||||
Create: 'Erstellen',
|
||||
Custom_Status: 'eigener Status',
|
||||
Dark: 'Dunkel',
|
||||
Dark_level: 'Dunkelstufe',
|
||||
Default: 'Standard',
|
||||
Default_browser: 'Standard-Browser',
|
||||
Delete_Room_Warning: 'Durch das Löschen eines Raums werden alle Nachrichten gelöscht, die im Raum gepostet wurden. Das kann nicht rückgängig gemacht werden.',
|
||||
Department: 'Abteilung',
|
||||
delete: 'löschen',
|
||||
Delete: 'Löschen',
|
||||
DELETE: 'LÖSCHEN',
|
||||
|
@ -173,17 +185,23 @@ export default {
|
|||
Direct_Messages: 'Direkte Nachrichten',
|
||||
Disable_notifications: 'Benachrichtigungen deaktiveren',
|
||||
Discussions: 'Diskussionen',
|
||||
Dont_Have_An_Account: 'Sie haben noch kein Konto?',
|
||||
Discussion_Desc: 'Hilft dir die Übersicht zu behalten! Durch das Erstellen einer Diskussion wird ein Unter-Kanal im ausgewählten Raum erzeugt und beide verknüpft.',
|
||||
Discussion_name: 'Diskussions-Name',
|
||||
Done: 'Erledigt',
|
||||
Dont_Have_An_Account: 'Du hast noch kein Konto?',
|
||||
Do_you_have_an_account: 'Du hast schon ein Konto?',
|
||||
Do_you_have_a_certificate: 'Haben Sie ein Zertifikat?',
|
||||
Do_you_really_want_to_key_this_room_question_mark: 'Möchten Sie diesen Raum wirklich {{key}}?',
|
||||
edit: 'bearbeiten',
|
||||
edited: 'bearbeitet',
|
||||
Edit: 'Bearbeiten',
|
||||
Edit_Status: 'Status ändern',
|
||||
Edit_Invite: 'Einladung bearbeiten',
|
||||
Email_or_password_field_is_empty: 'Das E-Mail- oder Passwortfeld ist leer',
|
||||
Email: 'Email',
|
||||
EMAIL: 'EMAIL',
|
||||
email: 'Email',
|
||||
Empty_title: 'leerer Titel',
|
||||
Enable_Auto_Translate: 'Automatische Übersetzung aktivieren',
|
||||
Enable_notifications: 'Benachrichtigungen aktivieren',
|
||||
Everyone_can_access_this_channel: 'Jeder kann auf diesen Kanal zugreifen',
|
||||
|
@ -200,6 +218,10 @@ export default {
|
|||
Forgot_password_If_this_email_is_registered: 'Wenn diese E-Mail registriert ist, senden wir Anweisungen zum Zurücksetzen Ihres Passworts. Wenn Sie in Kürze keine E-Mail erhalten, kommen Sie bitte zurück und versuchen Sie es erneut.',
|
||||
Forgot_password: 'Passwort vergessen',
|
||||
Forgot_Password: 'Passwort vergessen',
|
||||
Forward: 'Weiterleiten',
|
||||
Forward_Chat: 'Chat weiterleiten',
|
||||
Forward_to_department: 'Weiterleiten an Abteilung',
|
||||
Forward_to_user: 'Weiterleiten an Benutzer',
|
||||
Full_table: 'Klicken um die ganze Tabelle anzuzeigen',
|
||||
Generate_New_Link: 'Neuen Link erstellen',
|
||||
Group_by_favorites: 'Nach Favoriten gruppieren',
|
||||
|
@ -210,19 +232,20 @@ export default {
|
|||
Has_left_the_channel: 'Hat den Kanal verlassen',
|
||||
Hide_System_Messages: 'Systemnachrichten verstecken',
|
||||
Hide_type_messages: 'Verstecke "{{type}}"-Nachrichten',
|
||||
Message_HideType_uj: 'Nutzer beigetreten',
|
||||
Message_HideType_ul: 'Nutzer verlassen',
|
||||
Message_HideType_ru: 'Nutzer entfernt',
|
||||
Message_HideType_au: 'Nutzer hinzugefügt',
|
||||
Message_HideType_mute_unmute: 'Nutzer stummgeschaltet / freigegeben',
|
||||
Message_HideType_uj: 'Benutzer beigetreten',
|
||||
Message_HideType_ul: 'Benutzer verlassen',
|
||||
Message_HideType_ru: 'Benutzer entfernt',
|
||||
Message_HideType_au: 'Benutzer hinzugefügt',
|
||||
Message_HideType_mute_unmute: 'Benutzer stummgeschaltet / freigegeben',
|
||||
Message_HideType_r: 'Raumname geändert',
|
||||
Message_HideType_ut: 'Nutzer ist der Unterhaltung beigetreten',
|
||||
Message_HideType_ut: 'Benutzer ist der Unterhaltung beigetreten',
|
||||
Message_HideType_wm: 'Willkommen',
|
||||
Message_HideType_rm: 'Nachricht entfernt',
|
||||
Message_HideType_subscription_role_added: 'Rolle wurde gesetzt',
|
||||
Message_HideType_subscription_role_removed: 'Rolle nicht länger definiert',
|
||||
Message_HideType_room_archived: 'Raum archiviert',
|
||||
Message_HideType_room_unarchived: 'Raum nicht mehr archiviert',
|
||||
IP: 'IP',
|
||||
In_app: 'In-App-Browser',
|
||||
IN_APP_AND_DESKTOP: 'IN-APP UND DESKTOP',
|
||||
In_App_and_Desktop_Alert_info: 'Zeigt ein Banner oben am Bildschirm, wenn die App geöffnet ist und eine Benachrichtigung auf dem Desktop.',
|
||||
|
@ -230,12 +253,14 @@ export default {
|
|||
Invite: 'Einladen',
|
||||
is_a_valid_RocketChat_instance: 'ist eine gültige Rocket.Chat-Instanz',
|
||||
is_not_a_valid_RocketChat_instance: 'ist keine gültige Rocket.Chat-Instanz',
|
||||
is_typing: 'tippt',
|
||||
is_typing: 'schreibt',
|
||||
Invalid_or_expired_invite_token: 'Ungültiger oder abgelaufener Einladungscode',
|
||||
Invalid_server_version: 'Der Server, zu dem Sie eine Verbindung herstellen möchten, verwendet eine Version, die von der App nicht mehr unterstützt wird: {{currentVersion}}.\n\nWir benötigen Version {{MinVersion}}.',
|
||||
Invite_Link: 'Einladungs-Link',
|
||||
Invite_users: 'Benutzer einladen',
|
||||
Join: 'Beitreten',
|
||||
Join_our_open_workspace: 'Tritt unserem offenen Arbeitsbereich bei',
|
||||
Join_your_workspace: 'Tritt deinem Arbeitsbereich bei',
|
||||
Just_invited_people_can_access_this_channel: 'Nur eingeladene Personen können auf diesen Kanal zugreifen',
|
||||
Language: 'Sprache',
|
||||
last_message: 'letzte Nachricht',
|
||||
|
@ -246,12 +271,14 @@ export default {
|
|||
Light: 'Hell',
|
||||
License: 'Lizenz',
|
||||
Livechat: 'Live-Chat',
|
||||
Livechat_edit: 'Livechat bearbeiten',
|
||||
Login: 'Anmeldung',
|
||||
Login_error: 'Ihre Zugangsdaten wurden abgelehnt! Bitte versuchen Sie es erneut.',
|
||||
Login_with: 'Einloggen mit',
|
||||
Logging_out: 'Abmelden.',
|
||||
Logout: 'Abmelden',
|
||||
Max_number_of_uses: 'Maximale Anzahl der Benutzungen',
|
||||
Max_number_of_users_allowed_is_number: 'Maximale Anzahl von erlaubten Benutzern ist {{maxUsers}}',
|
||||
members: 'Mitglieder',
|
||||
Members: 'Mitglieder',
|
||||
Mentioned_Messages: 'Erwähnte Nachrichten',
|
||||
|
@ -277,6 +304,7 @@ export default {
|
|||
N_users: '{{n}} Benutzer',
|
||||
name: 'Name',
|
||||
Name: 'Name',
|
||||
Navigation_history: 'Navigations-Verlauf',
|
||||
Never: 'Niemals',
|
||||
New_Message: 'Neue Nachricht',
|
||||
New_Password: 'Neues Kennwort',
|
||||
|
@ -303,20 +331,34 @@ export default {
|
|||
Notifications: 'Benachrichtigungen',
|
||||
Notification_Duration: 'Benachrichtigungsdauer',
|
||||
Notification_Preferences: 'Benachrichtigungseinstellungen',
|
||||
No_available_agents_to_transfer: 'Keine Agenten für den Transfer verfügbar',
|
||||
Offline: 'Offline',
|
||||
Oops: 'Hoppla!',
|
||||
Onboarding_description: 'Ein Arbeitsbereich ist der Ort für die Zusammenarbeit deines Teams oder Organisation. Bitte den Admin des Arbeitsbereichs um eine Adresse, um ihm beizutreten, oder erstelle einen Arbeitsbereich für dein Team.',
|
||||
Onboarding_join_workspace: 'Tritt einem Arbeitsbereich bei',
|
||||
Onboarding_subtitle: 'Mehr als Team-Zusammenarbeit',
|
||||
Onboarding_title: 'Willkommen bei Rocket.Chat',
|
||||
Onboarding_join_open_description: 'Tritt unserem Arbeitsbereich bei um mit dem Rocket.Chat-Team oder der Gemeinschaft zu chatten.',
|
||||
Onboarding_agree_terms: 'Durch fortfahren stimmst du Rocket.Chats Bedingungen zu',
|
||||
Onboarding_less_options: 'Weniger Optionen',
|
||||
Onboarding_more_options: 'Mehr Optionen',
|
||||
Online: 'Online',
|
||||
Only_authorized_users_can_write_new_messages: 'Nur autorisierte Benutzer können neue Nachrichten schreiben',
|
||||
Open_emoji_selector: 'Öffne die Emoji-Auswahl',
|
||||
Open_Source_Communication: 'Open-Source-Kommunikation',
|
||||
Open_your_authentication_app_and_enter_the_code: 'Öffne deine Authentifizierungsanwendung und gib den Code ein.',
|
||||
OR: 'ODER',
|
||||
OS: 'OS',
|
||||
Overwrites_the_server_configuration_and_use_room_config: 'Übergeht die Servereinstellungen und nutzt Einstellung für den Raum',
|
||||
Password: 'Passwort',
|
||||
Parent_channel_or_group: 'Übergeordneter Kanal oder Gruppe',
|
||||
Permalink_copied_to_clipboard: 'Permalink in die Zwischenablage kopiert!',
|
||||
Phone: 'Telefon',
|
||||
Pin: 'Anheften',
|
||||
Pinned_Messages: 'Angeheftete Nachrichten',
|
||||
pinned: 'angeheftet',
|
||||
Pinned: 'Angeheftet',
|
||||
Please_add_a_comment: 'Bitte Kommentar hinzufügen',
|
||||
Please_enter_your_password: 'Bitte geben Sie Ihr Passwort ein',
|
||||
Please_wait: 'Bitte warten.',
|
||||
Preferences: 'Einstellungen',
|
||||
|
@ -355,6 +397,7 @@ export default {
|
|||
Reset_password: 'Passwort zurücksetzen',
|
||||
resetting_password: 'Passwort zurücksetzen',
|
||||
RESET: 'ZURÜCKSETZEN',
|
||||
Return: 'Zurück',
|
||||
Review_app_title: 'Gefällt dir diese App?',
|
||||
Review_app_desc: 'Gib uns 5 Sterne im {{store}}',
|
||||
Review_app_yes: 'Sicher!',
|
||||
|
@ -375,7 +418,8 @@ export default {
|
|||
Room_name_changed: 'Raumname geändert in {{name}} von {{userBy}}',
|
||||
SAVE: 'SPEICHERN',
|
||||
Save_Changes: 'Änderungen speichern',
|
||||
Save: 'sparen',
|
||||
Save: 'speichern',
|
||||
Saved: 'gespeichert',
|
||||
saving_preferences: 'Präferenzen speichern',
|
||||
saving_profile: 'Profil speichern',
|
||||
saving_settings: 'Einstellungen speichern',
|
||||
|
@ -388,17 +432,25 @@ export default {
|
|||
Seconds: '{{second}} Sekunden',
|
||||
Select_Avatar: 'Wählen Sie einen Avatar aus',
|
||||
Select_Server: 'Server auswählen',
|
||||
Select_Users: 'Wählen Sie einen Benutzer aus',
|
||||
Select_Users: 'Benutzer auswählen',
|
||||
Select_a_Channel: 'Kanal auswählen',
|
||||
Select_a_Department: 'Abteilung auswählen',
|
||||
Select_an_option: 'Option auswählen',
|
||||
Select_a_User: 'Benutzer auswählen',
|
||||
Send: 'Senden',
|
||||
Send_audio_message: 'Audio-Nachricht senden',
|
||||
Send_crash_report: 'Absturzbericht senden',
|
||||
Send_message: 'Nachricht senden',
|
||||
Send_me_the_code_again: 'Den Code neu versenden',
|
||||
Send_to: 'Senden an …',
|
||||
Sent_an_attachment: 'Sende einen Anhang',
|
||||
Server: 'Server',
|
||||
Servers: 'Server',
|
||||
Server_version: 'Server version: {{version}}',
|
||||
Set_username_subtitle: 'Der Benutzername wird verwendet, damit andere Personen Sie in Nachrichten erwähnen können',
|
||||
Set_custom_status: 'Individuellen Status setzen',
|
||||
Set_status: 'Status setzen',
|
||||
Status_saved_successfully: 'Status erfolgreich gesetzt!',
|
||||
Settings: 'Einstellungen',
|
||||
Settings_succesfully_changed: 'Einstellungen erfolgreich geändert!',
|
||||
Share: 'Teilen',
|
||||
|
@ -407,7 +459,7 @@ export default {
|
|||
Show_more: 'Mehr anzeigen …',
|
||||
Show_Unread_Counter: 'Zähler anzeigen',
|
||||
Show_Unread_Counter_Info: 'Anzahl der ungelesenen Nachrichten anzeigen',
|
||||
Sign_in_your_server: 'Melden Sie sich bei Ihrem Server an',
|
||||
Sign_in_your_server: 'Melde dich bei deinem Server an',
|
||||
Sign_Up: 'Anmelden',
|
||||
Some_field_is_invalid_or_empty: 'Ein Feld ist ungültig oder leer',
|
||||
Sorting_by: 'Sortierung nach {{key}}',
|
||||
|
@ -422,6 +474,7 @@ export default {
|
|||
Started_call: 'Anruf gestartet von {{userBy}}',
|
||||
Submit: 'einreichen',
|
||||
Table: 'Tabelle',
|
||||
Tags: 'Tags',
|
||||
Take_a_photo: 'Foto aufnehmen',
|
||||
Take_a_video: 'Video aufnehmen',
|
||||
tap_to_change_status: 'Tippen um den Status zu ändern',
|
||||
|
@ -441,10 +494,10 @@ export default {
|
|||
Translate: 'Übersetzen',
|
||||
Try_again: 'Versuchen Sie es nochmal',
|
||||
Two_Factor_Authentication: 'Zwei-Faktor-Authentifizierung',
|
||||
Type_the_channel_name_here: 'Geben Sie hier den Kanalnamen ein',
|
||||
Type_the_channel_name_here: 'Gib hier den Kanalnamen ein',
|
||||
unarchive: 'wiederherstellen',
|
||||
UNARCHIVE: 'WIEDERHERSTELLEN',
|
||||
Unblock_user: 'Nutzer entsperren',
|
||||
Unblock_user: 'Benutzer entsperren',
|
||||
Unfavorite: 'Nicht mehr favorisieren',
|
||||
Unfollowed_thread: 'Thread nicht mehr folgen',
|
||||
Unmute: 'Stummschaltung aufheben',
|
||||
|
@ -457,9 +510,10 @@ export default {
|
|||
Updating: 'Aktualisierung …',
|
||||
Uploading: 'Hochladen',
|
||||
Upload_file_question_mark: 'Datei hochladen?',
|
||||
User: 'Benutzer',
|
||||
Users: 'Benutzer',
|
||||
User_added_by: 'Benutzer {{userAdded}} hinzugefügt von {{userBy}}',
|
||||
User_Info: 'Nutzerinfo',
|
||||
User_Info: 'Benutzerinfo',
|
||||
User_has_been_key: 'Benutzer wurde {{key}}!',
|
||||
User_is_no_longer_role_by_: '{{user}} ist nicht länger {{role}} von {{userBy}}',
|
||||
User_muted_by: 'Benutzer {{userMuted}} von {{userBy}} stummgeschaltet',
|
||||
|
@ -471,18 +525,26 @@ export default {
|
|||
Username: 'Benutzername',
|
||||
Username_or_email: 'Benutzername oder E-Mail-Adresse',
|
||||
Uses_server_configuration: 'Nutzt Servereinstellungen',
|
||||
Usually_a_discussion_starts_with_a_question_like_How_do_I_upload_a_picture: 'Üblicherweise beginnt eine Diskussion mit einer Frage, beispielsweise: "Wie lade ich ein Bild hoch?"',
|
||||
Validating: 'Validierung',
|
||||
Registration_Succeeded: 'Registrierung erfolgreich!',
|
||||
Verify: 'Überprüfen',
|
||||
Verify_email_title: 'Registrierung erfolgreich!',
|
||||
Verify_email_desc: 'Wir haben dir eine Email geschickt um deine Anmeldung zu bestätigen. Wenn du keine Email erhältst, komme bitte wieder und versuche es noch einmal.',
|
||||
Verify_your_email_for_the_code_we_sent: 'Prüfe deine Mails für den Code, den wir dir eben geschickt haben.',
|
||||
Video_call: 'Videoanruf',
|
||||
View_Original: 'Original anzeigen',
|
||||
Voice_call: 'Sprachanruf',
|
||||
Websocket_disabled: 'Websockets sind auf diesem Server nicht aktiviert.\n{{contact}}',
|
||||
Welcome: 'Herzlich willkommen',
|
||||
What_are_you_doing_right_now: 'Was machst du gerade?',
|
||||
Whats_your_2fa: 'Wie lautet Ihr 2FA-Code?',
|
||||
Without_Servers: 'Ohne Server',
|
||||
Workspaces: 'Arbeitsbereiche',
|
||||
Would_you_like_to_return_the_inquiry: 'Willst du zur Anfrage zurück?',
|
||||
Write_External_Permission_Message: 'Rocket.Chat benötigt Zugriff auf Ihre Galerie um Bilder speichern zu können.',
|
||||
Write_External_Permission: 'Galerie-Zugriff',
|
||||
Yes: 'Ja',
|
||||
Yes_action_it: 'Ja, {{action}}!',
|
||||
Yesterday: 'Gestern',
|
||||
You_are_in_preview_mode: 'Sie befinden sich im Vorschaumodus',
|
||||
|
@ -495,11 +557,13 @@ export default {
|
|||
You: 'Sie',
|
||||
Logged_out_by_server: 'Du bist vom Server abgemeldet worden. Bitte melde dich wieder an.',
|
||||
You_need_to_access_at_least_one_RocketChat_server_to_share_something: 'Sie benötigen Zugang zu mindestens einem Rocket.Chat-Server um etwas zu teilen.',
|
||||
Your_certificate: 'Ihr Zertifikat',
|
||||
Your_certificate: 'Dein Zertifikat',
|
||||
Your_message: 'Deine Nachricht',
|
||||
Your_invite_link_will_expire_after__usesLeft__uses: 'Dein Einladungs-Link wird nach {{usesLeft}} Benutzungen ablaufen.',
|
||||
Your_invite_link_will_expire_on__date__or_after__usesLeft__uses: 'Dein Einladungs-Link wird am {{date}} oder nach {{usesLeft}} Benutzungen ablaufen.',
|
||||
Your_invite_link_will_expire_on__date__: 'Dein Einladungs-Link wird am {{date}} ablaufen.',
|
||||
Your_invite_link_will_never_expire: 'Dein Einladungs-Link wird niemals ablaufen.',
|
||||
Your_workspace: 'Dein Arbeitsbereich',
|
||||
Version_no: 'Version: {{version}}',
|
||||
You_will_not_be_able_to_recover_this_message: 'Sie können diese Nachricht nicht wiederherstellen!',
|
||||
Change_Language: 'Sprache ändern',
|
||||
|
@ -521,5 +585,30 @@ export default {
|
|||
You_will_be_logged_out_of_this_application: 'Du wirst in dieser Anwendung vom Server abgemeldet.',
|
||||
Clear: 'Löschen',
|
||||
This_will_clear_all_your_offline_data: 'Dies wird deine Offline-Daten löschen.',
|
||||
Mark_unread: 'Als ungelesen markieren'
|
||||
This_will_remove_all_data_from_this_server: 'Dies wird alle Daten von diesem Server löschen.',
|
||||
Mark_unread: 'Als ungelesen markieren',
|
||||
Wait_activation_warning: 'Bevor du dich anmelden kannst, muss dein Konto durch einen Administrator freigeschaltet werden.',
|
||||
Screen_lock: 'Zugriffs-Sperre',
|
||||
Local_authentication_biometry_title: 'Authentifizieren',
|
||||
Local_authentication_biometry_fallback: 'Sicherheitscode benutzen',
|
||||
Local_authentication_unlock_option: 'Entsperren mit Sicherheitscode',
|
||||
Local_authentication_change_passcode: 'Ändere Sicherheitscode',
|
||||
Local_authentication_info: 'Anmerkung: Wenn du den Sicherheitscode vergisst, musst du diese App löschen und neu installieren.',
|
||||
Local_authentication_facial_recognition: 'Gesichtserkennung',
|
||||
Local_authentication_fingerprint: 'Fingerabdruck',
|
||||
Local_authentication_unlock_with_label: 'Entsperren mit {{label}}',
|
||||
Local_authentication_auto_lock_60: 'Nach 1 Minute',
|
||||
Local_authentication_auto_lock_300: 'Nach 5 Minuten',
|
||||
Local_authentication_auto_lock_900: 'Nach 15 Minuten',
|
||||
Local_authentication_auto_lock_1800: 'Nach 30 Minuten',
|
||||
Local_authentication_auto_lock_3600: 'Nach 1 Stunde',
|
||||
Passcode_enter_title: 'Gib deinen Sicherheitscode ein',
|
||||
Passcode_choose_title: 'Setze deinen neuen Sicherheitscode',
|
||||
Passcode_choose_confirm_title: 'Bestätige deinen neuen Sicherheitscode',
|
||||
Passcode_choose_error: 'Sicherheitscodes stimmen nicht überein. Probiere es noch einmal.',
|
||||
Passcode_choose_force_set: 'Sicherheitscode wird vom Admin verlangt',
|
||||
Passcode_app_locked_title: 'App gesperrt',
|
||||
Passcode_app_locked_subtitle: 'Versuche es in {{timeLeft}} Sekunden noch einmal.',
|
||||
After_seconds_set_by_admin: 'Nach {{seconds}} Sekunden (durch den Admin gesetzt)',
|
||||
Dont_activate: 'Jetzt nicht aktivieren'
|
||||
};
|
||||
|
|
|
@ -18,6 +18,7 @@ export default {
|
|||
'error-email-domain-blacklisted': 'The email domain is blacklisted',
|
||||
'error-email-send-failed': 'Error trying to send email: {{message}}',
|
||||
'error-save-image': 'Error while saving image',
|
||||
'error-save-video': 'Error while saving video',
|
||||
'error-field-unavailable': '{{field}} is already in use :(',
|
||||
'error-file-too-large': 'File is too large',
|
||||
'error-importer-not-defined': 'The importer was not defined correctly, it is missing the Import class.',
|
||||
|
@ -84,6 +85,7 @@ export default {
|
|||
Add_Server: 'Add Server',
|
||||
Add_users: 'Add users',
|
||||
Admin_Panel: 'Admin Panel',
|
||||
Agent: 'Agent',
|
||||
Alert: 'Alert',
|
||||
alert: 'alert',
|
||||
alerts: 'alerts',
|
||||
|
@ -132,7 +134,9 @@ export default {
|
|||
Click_to_join: 'Click to Join!',
|
||||
Close: 'Close',
|
||||
Close_emoji_selector: 'Close emoji selector',
|
||||
Closing_chat: 'Closing chat',
|
||||
Change_language_loading: 'Changing language.',
|
||||
Chat_closed_by_agent: 'Chat closed by agent',
|
||||
Choose: 'Choose',
|
||||
Choose_from_library: 'Choose from library',
|
||||
Choose_file: 'Choose file',
|
||||
|
@ -150,6 +154,7 @@ export default {
|
|||
Continue_with: 'Continue with',
|
||||
Copied_to_clipboard: 'Copied to clipboard!',
|
||||
Copy: 'Copy',
|
||||
Conversation: 'Conversation',
|
||||
Permalink: 'Permalink',
|
||||
Certificate_password: 'Certificate Password',
|
||||
Clear_cache: 'Clear local server cache',
|
||||
|
@ -168,6 +173,7 @@ export default {
|
|||
Default: 'Default',
|
||||
Default_browser: 'Default browser',
|
||||
Delete_Room_Warning: 'Deleting a room will delete all messages posted within the room. This cannot be undone.',
|
||||
Department: 'Department',
|
||||
delete: 'delete',
|
||||
Delete: 'Delete',
|
||||
DELETE: 'DELETE',
|
||||
|
@ -195,6 +201,7 @@ export default {
|
|||
Email: 'Email',
|
||||
EMAIL: 'EMAIL',
|
||||
email: 'e-mail',
|
||||
Empty_title: 'Empty title',
|
||||
Enable_Auto_Translate: 'Enable Auto-Translate',
|
||||
Enable_notifications: 'Enable notifications',
|
||||
Everyone_can_access_this_channel: 'Everyone can access this channel',
|
||||
|
@ -211,6 +218,10 @@ export default {
|
|||
Forgot_password_If_this_email_is_registered: 'If this email is registered, we\'ll send instructions on how to reset your password. If you do not receive an email shortly, please come back and try again.',
|
||||
Forgot_password: 'Forgot your password?',
|
||||
Forgot_Password: 'Forgot Password',
|
||||
Forward: 'Forward',
|
||||
Forward_Chat: 'Forward Chat',
|
||||
Forward_to_department: 'Forward to department',
|
||||
Forward_to_user: 'Forward to user',
|
||||
Full_table: 'Click to see full table',
|
||||
Generate_New_Link: 'Generate New Link',
|
||||
Group_by_favorites: 'Group favorites',
|
||||
|
@ -234,6 +245,7 @@ export default {
|
|||
Message_HideType_subscription_role_removed: 'Role No Longer Defined',
|
||||
Message_HideType_room_archived: 'Room Archived',
|
||||
Message_HideType_room_unarchived: 'Room Unarchived',
|
||||
IP: 'IP',
|
||||
In_app: 'In-app',
|
||||
IN_APP_AND_DESKTOP: 'IN-APP AND DESKTOP',
|
||||
In_App_and_Desktop_Alert_info: 'Displays a banner at the top of the screen when app is open, and displays a notification on desktop',
|
||||
|
@ -259,6 +271,7 @@ export default {
|
|||
Light: 'Light',
|
||||
License: 'License',
|
||||
Livechat: 'Livechat',
|
||||
Livechat_edit: 'Livechat edit',
|
||||
Login: 'Login',
|
||||
Login_error: 'Your credentials were rejected! Please try again.',
|
||||
Login_with: 'Login with',
|
||||
|
@ -291,6 +304,7 @@ export default {
|
|||
N_users: '{{n}} users',
|
||||
name: 'name',
|
||||
Name: 'Name',
|
||||
Navigation_history: 'Navigation history',
|
||||
Never: 'Never',
|
||||
New_Message: 'New Message',
|
||||
New_Password: 'New Password',
|
||||
|
@ -317,6 +331,7 @@ export default {
|
|||
Notifications: 'Notifications',
|
||||
Notification_Duration: 'Notification Duration',
|
||||
Notification_Preferences: 'Notification Preferences',
|
||||
No_available_agents_to_transfer: 'No available agents to transfer',
|
||||
Offline: 'Offline',
|
||||
Oops: 'Oops!',
|
||||
Onboarding_description: 'A workspace is your team or organization’s space to collaborate. Ask the workspace admin for address to join or create one for your team.',
|
||||
|
@ -333,14 +348,17 @@ export default {
|
|||
Open_Source_Communication: 'Open Source Communication',
|
||||
Open_your_authentication_app_and_enter_the_code: 'Open your authentication app and enter the code.',
|
||||
OR: 'OR',
|
||||
OS: 'OS',
|
||||
Overwrites_the_server_configuration_and_use_room_config: 'Overwrites the server configuration and use room config',
|
||||
Password: 'Password',
|
||||
Parent_channel_or_group: 'Parent channel or group',
|
||||
Permalink_copied_to_clipboard: 'Permalink copied to clipboard!',
|
||||
Phone: 'Phone',
|
||||
Pin: 'Pin',
|
||||
Pinned_Messages: 'Pinned Messages',
|
||||
pinned: 'pinned',
|
||||
Pinned: 'Pinned',
|
||||
Please_add_a_comment: 'Please add a comment',
|
||||
Please_enter_your_password: 'Please enter your password',
|
||||
Please_wait: 'Please wait.',
|
||||
Preferences: 'Preferences',
|
||||
|
@ -379,6 +397,7 @@ export default {
|
|||
Reset_password: 'Reset password',
|
||||
resetting_password: 'resetting password',
|
||||
RESET: 'RESET',
|
||||
Return: 'Return',
|
||||
Review_app_title: 'Are you enjoying this app?',
|
||||
Review_app_desc: 'Give us 5 stars on {{store}}',
|
||||
Review_app_yes: 'Sure!',
|
||||
|
@ -400,6 +419,7 @@ export default {
|
|||
SAVE: 'SAVE',
|
||||
Save_Changes: 'Save Changes',
|
||||
Save: 'Save',
|
||||
Saved: 'Saved',
|
||||
saving_preferences: 'saving preferences',
|
||||
saving_profile: 'saving profile',
|
||||
saving_settings: 'saving settings',
|
||||
|
@ -414,7 +434,9 @@ export default {
|
|||
Select_Server: 'Select Server',
|
||||
Select_Users: 'Select Users',
|
||||
Select_a_Channel: 'Select a Channel',
|
||||
Select_a_Department: 'Select a Department',
|
||||
Select_an_option: 'Select an option',
|
||||
Select_a_User: 'Select a User',
|
||||
Send: 'Send',
|
||||
Send_audio_message: 'Send audio message',
|
||||
Send_crash_report: 'Send crash report',
|
||||
|
@ -452,6 +474,7 @@ export default {
|
|||
Started_call: 'Call started by {{userBy}}',
|
||||
Submit: 'Submit',
|
||||
Table: 'Table',
|
||||
Tags: 'Tags',
|
||||
Take_a_photo: 'Take a photo',
|
||||
Take_a_video: 'Take a video',
|
||||
tap_to_change_status: 'tap to change status',
|
||||
|
@ -487,6 +510,7 @@ export default {
|
|||
Updating: 'Updating...',
|
||||
Uploading: 'Uploading',
|
||||
Upload_file_question_mark: 'Upload file?',
|
||||
User: 'User',
|
||||
Users: 'Users',
|
||||
User_added_by: 'User {{userAdded}} added by {{userBy}}',
|
||||
User_Info: 'User Info',
|
||||
|
@ -517,8 +541,10 @@ export default {
|
|||
Whats_your_2fa: 'What\'s your 2FA code?',
|
||||
Without_Servers: 'Without Servers',
|
||||
Workspaces: 'Workspaces',
|
||||
Would_you_like_to_return_the_inquiry: 'Would you like to return the inquiry?',
|
||||
Write_External_Permission_Message: 'Rocket Chat needs access to your gallery so you can save images.',
|
||||
Write_External_Permission: 'Gallery Permission',
|
||||
Yes: 'Yes',
|
||||
Yes_action_it: 'Yes, {{action}} it!',
|
||||
Yesterday: 'Yesterday',
|
||||
You_are_in_preview_mode: 'You are in preview mode',
|
||||
|
@ -559,6 +585,30 @@ export default {
|
|||
You_will_be_logged_out_of_this_application: 'You will be logged out of this application.',
|
||||
Clear: 'Clear',
|
||||
This_will_clear_all_your_offline_data: 'This will clear all your offline data.',
|
||||
This_will_remove_all_data_from_this_server: 'This will remove all data from this server.',
|
||||
Mark_unread: 'Mark Unread',
|
||||
Wait_activation_warning: 'Before you can login, your account must be manually activated by an administrator.'
|
||||
Wait_activation_warning: 'Before you can login, your account must be manually activated by an administrator.',
|
||||
Screen_lock: 'Screen lock',
|
||||
Local_authentication_biometry_title: 'Authenticate',
|
||||
Local_authentication_biometry_fallback: 'Use passcode',
|
||||
Local_authentication_unlock_option: 'Unlock with Passcode',
|
||||
Local_authentication_change_passcode: 'Change Passcode',
|
||||
Local_authentication_info: 'Note: if you forget the Passcode, you\'ll need to delete and reinstall the app.',
|
||||
Local_authentication_facial_recognition: 'facial recognition',
|
||||
Local_authentication_fingerprint: 'fingerprint',
|
||||
Local_authentication_unlock_with_label: 'Unlock with {{label}}',
|
||||
Local_authentication_auto_lock_60: 'After 1 minute',
|
||||
Local_authentication_auto_lock_300: 'After 5 minutes',
|
||||
Local_authentication_auto_lock_900: 'After 15 minutes',
|
||||
Local_authentication_auto_lock_1800: 'After 30 minutes',
|
||||
Local_authentication_auto_lock_3600: 'After 1 hour',
|
||||
Passcode_enter_title: 'Enter your passcode',
|
||||
Passcode_choose_title: 'Choose your new passcode',
|
||||
Passcode_choose_confirm_title: 'Confirm your new passcode',
|
||||
Passcode_choose_error: 'Passcodes don\'t match. Try again.',
|
||||
Passcode_choose_force_set: 'Passcode required by admin',
|
||||
Passcode_app_locked_title: 'App locked',
|
||||
Passcode_app_locked_subtitle: 'Try again in {{timeLeft}} seconds',
|
||||
After_seconds_set_by_admin: 'After {{seconds}} seconds (set by admin)',
|
||||
Dont_activate: 'Don\'t activate now'
|
||||
};
|
||||
|
|
|
@ -89,6 +89,7 @@ export default {
|
|||
Add_Reaction: 'Reagir',
|
||||
Add_Server: 'Adicionar servidor',
|
||||
Add_users: 'Adicionar usuário',
|
||||
Agent: 'Agente',
|
||||
Alert: 'Alerta',
|
||||
alert: 'alerta',
|
||||
alerts: 'alertas',
|
||||
|
@ -135,7 +136,9 @@ export default {
|
|||
Click_to_join: 'Clique para participar!',
|
||||
Close: 'Fechar',
|
||||
Close_emoji_selector: 'Fechar seletor de emojis',
|
||||
Closing_chat: 'Fechando conversa',
|
||||
Choose: 'Escolher',
|
||||
Chat_closed_by_agent: 'Conversa fechada por agente',
|
||||
Choose_from_library: 'Escolha da biblioteca',
|
||||
Choose_file: 'Enviar arquivo',
|
||||
Choose_where_you_want_links_be_opened: 'Escolha onde deseja que os links sejam abertos',
|
||||
|
@ -145,6 +148,7 @@ export default {
|
|||
Confirm: 'Confirmar',
|
||||
Connect: 'Conectar',
|
||||
Connected: 'Conectado',
|
||||
Conversation: 'Conversação',
|
||||
connecting_server: 'conectando no servidor',
|
||||
Connecting: 'Conectando...',
|
||||
Continue_with: 'Entrar com',
|
||||
|
@ -187,6 +191,7 @@ export default {
|
|||
Email_or_password_field_is_empty: 'Email ou senha estão vazios',
|
||||
Email: 'Email',
|
||||
email: 'e-mail',
|
||||
Empty_title: 'Título vazio',
|
||||
Enable_notifications: 'Habilitar notificações',
|
||||
Everyone_can_access_this_channel: 'Todos podem acessar este canal',
|
||||
Error_uploading: 'Erro subindo',
|
||||
|
@ -201,6 +206,10 @@ export default {
|
|||
Forgot_password_If_this_email_is_registered: 'Se este e-mail estiver cadastrado, enviaremos instruções sobre como redefinir sua senha. Se você não receber um e-mail em breve, volte e tente novamente.',
|
||||
Forgot_password: 'Esqueceu sua senha?',
|
||||
Forgot_Password: 'Esqueci minha senha',
|
||||
Forward: 'Encaminhar',
|
||||
Forward_Chat: 'Encaminhar Conversa',
|
||||
Forward_to_department: 'Encaminhar para departamento',
|
||||
Forward_to_user: 'Encaminhar para usuário',
|
||||
Full_table: 'Clique para ver a tabela completa',
|
||||
Generate_New_Link: 'Gerar novo convite',
|
||||
Group_by_favorites: 'Agrupar favoritos',
|
||||
|
@ -223,6 +232,7 @@ export default {
|
|||
Message_HideType_subscription_role_removed: 'Papel removido',
|
||||
Message_HideType_room_archived: 'Sala arquivada',
|
||||
Message_HideType_room_unarchived: 'Sala desarquivada',
|
||||
IP: 'IP',
|
||||
In_app: 'No app',
|
||||
Invisible: 'Invisível',
|
||||
Invite: 'Convidar',
|
||||
|
@ -269,6 +279,7 @@ export default {
|
|||
N_users: '{{n}} usuários',
|
||||
name: 'nome',
|
||||
Name: 'Nome',
|
||||
Navigation_history: 'Histórico de navegação',
|
||||
Never: 'Nunca',
|
||||
New_in_RocketChat_question_mark: 'Novo no Rocket.Chat?',
|
||||
New_Message: 'Nova Mensagem',
|
||||
|
@ -289,6 +300,7 @@ export default {
|
|||
Notify_active_in_this_room: 'Notificar usuários ativos nesta sala',
|
||||
Notify_all_in_this_room: 'Notificar todos nesta sala',
|
||||
Not_RC_Server: 'Este não é um servidor Rocket.Chat.\n{{contact}}',
|
||||
No_available_agents_to_transfer: 'Nenhum agente disponível para transferência',
|
||||
Offline: 'Offline',
|
||||
Oops: 'Ops!',
|
||||
Onboarding_description: 'Workspace é o espaço de colaboração do seu time ou organização. Peça um convite ou o endereço ao seu administrador ou crie uma workspace para o seu time.',
|
||||
|
@ -305,6 +317,7 @@ export default {
|
|||
Open_Source_Communication: 'Comunicação Open Source',
|
||||
Open_your_authentication_app_and_enter_the_code: 'Abra seu aplicativo de autenticação e digite o código.',
|
||||
OR: 'OU',
|
||||
OS: 'SO',
|
||||
Overwrites_the_server_configuration_and_use_room_config: 'Substituir a configuração do servidor e usar a configuração da sala',
|
||||
Password: 'Senha',
|
||||
Parent_channel_or_group: 'Canal ou grupo pai',
|
||||
|
@ -315,6 +328,7 @@ export default {
|
|||
Pinned: 'Mensagens Fixadas',
|
||||
Please_wait: 'Por favor, aguarde.',
|
||||
Please_enter_your_password: 'Por favor, digite sua senha',
|
||||
Please_add_a_comment: 'Por favor, adicione um comentário',
|
||||
Preferences: 'Preferências',
|
||||
Preferences_saved: 'Preferências salvas!',
|
||||
Privacy_Policy: ' Política de Privacidade',
|
||||
|
@ -343,6 +357,7 @@ export default {
|
|||
Reset_password: 'Resetar senha',
|
||||
resetting_password: 'redefinindo senha',
|
||||
RESET: 'RESETAR',
|
||||
Return: 'Retornar',
|
||||
Review_app_title: 'Você está gostando do app?',
|
||||
Review_app_desc: 'Nos dê 5 estrelas na {{store}}',
|
||||
Review_app_yes: 'Claro!',
|
||||
|
@ -377,7 +392,9 @@ export default {
|
|||
Select_Server: 'Selecionar Servidor',
|
||||
Select_Users: 'Selecionar Usuários',
|
||||
Select_a_Channel: 'Selecione um canal',
|
||||
Select_a_Department: 'Selecione um Departamento',
|
||||
Select_an_option: 'Selecione uma opção',
|
||||
Select_a_User: 'Selecione um Usuário',
|
||||
Send: 'Enviar',
|
||||
Send_audio_message: 'Enviar mensagem de áudio',
|
||||
Send_message: 'Enviar mensagem',
|
||||
|
@ -436,6 +453,7 @@ export default {
|
|||
Updating: 'Atualizando...',
|
||||
Uploading: 'Subindo arquivo',
|
||||
Upload_file_question_mark: 'Enviar arquivo?',
|
||||
User: 'Usuário',
|
||||
Users: 'Usuários',
|
||||
User_added_by: 'Usuário {{userAdded}} adicionado por {{userBy}}',
|
||||
User_has_been_key: 'Usuário foi {{key}}!',
|
||||
|
@ -479,8 +497,10 @@ export default {
|
|||
Your_invite_link_will_never_expire: 'Seu link de convite nunca irá vencer.',
|
||||
Your_workspace: 'Sua workspace',
|
||||
You_will_not_be_able_to_recover_this_message: 'Você não será capaz de recuperar essa mensagem!',
|
||||
Would_you_like_to_return_the_inquiry: 'Deseja retornar a consulta?',
|
||||
Write_External_Permission_Message: 'Rocket Chat precisa de acesso à sua galeria para salvar imagens',
|
||||
Write_External_Permission: 'Acesso à Galeria',
|
||||
Yes: 'Sim',
|
||||
Crash_report_disclaimer: 'Nós não rastreamos o conteúdo das suas conversas. O relatório de erros apenas contém informações relevantes para identificarmos problemas e corrigí-los.',
|
||||
Type_message: 'Digitar mensagem',
|
||||
Room_search: 'Busca de sala',
|
||||
|
@ -499,6 +519,30 @@ export default {
|
|||
You_will_be_logged_out_of_this_application: 'Você sairá deste aplicativo.',
|
||||
Clear: 'Limpar',
|
||||
This_will_clear_all_your_offline_data: 'Isto limpará todos os seus dados offline.',
|
||||
This_will_remove_all_data_from_this_server: 'Isto removerá todos os dados desse servidor.',
|
||||
Mark_unread: 'Marcar como não Lida',
|
||||
Wait_activation_warning: 'Antes que você possa fazer o login, sua conta deve ser manualmente ativada por um administrador.'
|
||||
Wait_activation_warning: 'Antes que você possa fazer o login, sua conta deve ser manualmente ativada por um administrador.',
|
||||
Screen_lock: 'Bloqueio de Tela',
|
||||
Local_authentication_biometry_title: 'Autenticar',
|
||||
Local_authentication_biometry_fallback: 'Usar senha',
|
||||
Local_authentication_unlock_option: 'Desbloquear com senha',
|
||||
Local_authentication_change_passcode: 'Alterar senha',
|
||||
Local_authentication_info: 'Nota: se você esquecer sua senha, terá de apagar e reinstalar o app.',
|
||||
Local_authentication_facial_recognition: 'reconhecimento facial',
|
||||
Local_authentication_fingerprint: 'impressão digital',
|
||||
Local_authentication_unlock_with_label: 'Desbloquear com {{label}}',
|
||||
Local_authentication_auto_lock_60: 'Após 1 minuto',
|
||||
Local_authentication_auto_lock_300: 'Após 5 minutos',
|
||||
Local_authentication_auto_lock_900: 'Após 15 minutos',
|
||||
Local_authentication_auto_lock_1800: 'Após 30 minutos',
|
||||
Local_authentication_auto_lock_3600: 'Após 1 hora',
|
||||
Passcode_enter_title: 'Digite sua senha',
|
||||
Passcode_choose_title: 'Insira sua nova senha',
|
||||
Passcode_choose_confirm_title: 'Confirme sua nova senha',
|
||||
Passcode_choose_error: 'As senhas não coincidem. Tente novamente.',
|
||||
Passcode_choose_force_set: 'Senha foi exigida pelo admin',
|
||||
Passcode_app_locked_title: 'Aplicativo bloqueado',
|
||||
Passcode_app_locked_subtitle: 'Tente novamente em {{timeLeft}} segundos',
|
||||
After_seconds_set_by_admin: 'Após {{seconds}} segundos (Configurado pelo adm)',
|
||||
Dont_activate: 'Não ativar agora'
|
||||
};
|
||||
|
|
20
app/index.js
20
app/index.js
|
@ -46,6 +46,8 @@ import TwoFactor from './containers/TwoFactor';
|
|||
|
||||
import RoomsListView from './views/RoomsListView';
|
||||
import RoomView from './views/RoomView';
|
||||
import ScreenLockedView from './views/ScreenLockedView';
|
||||
import ChangePasscodeView from './views/ChangePasscodeView';
|
||||
|
||||
if (isIOS) {
|
||||
const RNScreens = require('react-native-screens');
|
||||
|
@ -166,6 +168,15 @@ const ChatsStack = createStackNavigator({
|
|||
NotificationPrefView: {
|
||||
getScreen: () => require('./views/NotificationPreferencesView').default
|
||||
},
|
||||
VisitorNavigationView: {
|
||||
getScreen: () => require('./views/VisitorNavigationView').default
|
||||
},
|
||||
ForwardLivechatView: {
|
||||
getScreen: () => require('./views/ForwardLivechatView').default
|
||||
},
|
||||
LivechatEditView: {
|
||||
getScreen: () => require('./views/LivechatEditView').default
|
||||
},
|
||||
PickerView: {
|
||||
getScreen: () => require('./views/PickerView').default
|
||||
},
|
||||
|
@ -224,6 +235,9 @@ const SettingsStack = createStackNavigator({
|
|||
},
|
||||
DefaultBrowserView: {
|
||||
getScreen: () => require('./views/DefaultBrowserView').default
|
||||
},
|
||||
ScreenLockConfigView: {
|
||||
getScreen: () => require('./views/ScreenLockConfigView').default
|
||||
}
|
||||
}, {
|
||||
defaultNavigationOptions: defaultHeader,
|
||||
|
@ -514,7 +528,7 @@ class CustomModalStack extends React.Component {
|
|||
const pageSheetViews = ['AttachmentView'];
|
||||
const pageSheet = pageSheetViews.includes(getActiveRouteName(navigation.state));
|
||||
|
||||
const androidProps = isAndroid && {
|
||||
const androidProps = isAndroid && !pageSheet && {
|
||||
style: { marginBottom: 0 }
|
||||
};
|
||||
|
||||
|
@ -524,7 +538,7 @@ class CustomModalStack extends React.Component {
|
|||
</View>
|
||||
);
|
||||
|
||||
if (isAndroid) {
|
||||
if (isAndroid && !pageSheet) {
|
||||
content = (
|
||||
<ScrollView overScrollMode='never'>
|
||||
{content}
|
||||
|
@ -729,6 +743,8 @@ export default class Root extends React.Component {
|
|||
>
|
||||
{content}
|
||||
<TwoFactor />
|
||||
<ScreenLockedView />
|
||||
<ChangePasscodeView />
|
||||
</ThemeContext.Provider>
|
||||
</Provider>
|
||||
</AppearanceProvider>
|
||||
|
|
|
@ -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;
|
||||
};
|
|
@ -1,9 +1,9 @@
|
|||
import { createStore, applyMiddleware, compose } from 'redux';
|
||||
import createSagaMiddleware from 'redux-saga';
|
||||
import applyAppStateListener from 'redux-enhancer-react-native-appstate';
|
||||
|
||||
import reducers from '../reducers';
|
||||
import sagas from '../sagas';
|
||||
import applyAppStateMiddleware from './appStateMiddleware';
|
||||
|
||||
let sagaMiddleware;
|
||||
let enhancers;
|
||||
|
@ -16,7 +16,7 @@ if (__DEV__) {
|
|||
});
|
||||
|
||||
enhancers = compose(
|
||||
applyAppStateListener(),
|
||||
applyAppStateMiddleware(),
|
||||
applyMiddleware(reduxImmutableStateInvariant),
|
||||
applyMiddleware(sagaMiddleware),
|
||||
Reactotron.createEnhancer()
|
||||
|
@ -24,7 +24,7 @@ if (__DEV__) {
|
|||
} else {
|
||||
sagaMiddleware = createSagaMiddleware();
|
||||
enhancers = compose(
|
||||
applyAppStateListener(),
|
||||
applyAppStateMiddleware(),
|
||||
applyMiddleware(sagaMiddleware)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -34,6 +34,36 @@ if (__DEV__ && isIOS) {
|
|||
console.log(appGroupPath);
|
||||
}
|
||||
|
||||
export const getDatabase = (database = '') => {
|
||||
const path = database.replace(/(^\w+:|^)\/\//, '').replace(/\//g, '.');
|
||||
const dbName = `${ appGroupPath }${ path }.db`;
|
||||
|
||||
const adapter = new SQLiteAdapter({
|
||||
dbName,
|
||||
schema: appSchema,
|
||||
migrations
|
||||
});
|
||||
|
||||
return new Database({
|
||||
adapter,
|
||||
modelClasses: [
|
||||
Subscription,
|
||||
Room,
|
||||
Message,
|
||||
Thread,
|
||||
ThreadMessage,
|
||||
CustomEmoji,
|
||||
FrequentlyUsedEmoji,
|
||||
Upload,
|
||||
Setting,
|
||||
Role,
|
||||
Permission,
|
||||
SlashCommand
|
||||
],
|
||||
actionsEnabled: true
|
||||
});
|
||||
};
|
||||
|
||||
class DB {
|
||||
databases = {
|
||||
serversDB: new Database({
|
||||
|
@ -87,34 +117,8 @@ class DB {
|
|||
});
|
||||
}
|
||||
|
||||
setActiveDB(database = '') {
|
||||
const path = database.replace(/(^\w+:|^)\/\//, '').replace(/\//g, '.');
|
||||
const dbName = `${ appGroupPath }${ path }.db`;
|
||||
|
||||
const adapter = new SQLiteAdapter({
|
||||
dbName,
|
||||
schema: appSchema,
|
||||
migrations
|
||||
});
|
||||
|
||||
this.databases.activeDB = new Database({
|
||||
adapter,
|
||||
modelClasses: [
|
||||
Subscription,
|
||||
Room,
|
||||
Message,
|
||||
Thread,
|
||||
ThreadMessage,
|
||||
CustomEmoji,
|
||||
FrequentlyUsedEmoji,
|
||||
Upload,
|
||||
Setting,
|
||||
Role,
|
||||
Permission,
|
||||
SlashCommand
|
||||
],
|
||||
actionsEnabled: true
|
||||
});
|
||||
setActiveDB(database) {
|
||||
this.databases.activeDB = getDatabase(database);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -30,6 +30,8 @@ export default class Message extends Model {
|
|||
|
||||
@field('avatar') avatar;
|
||||
|
||||
@field('emoji') emoji;
|
||||
|
||||
@json('attachments', sanitizer) attachments;
|
||||
|
||||
@json('urls', sanitizer) urls;
|
||||
|
|
|
@ -13,4 +13,14 @@ export default class Room extends Model {
|
|||
@field('encrypted') encrypted;
|
||||
|
||||
@field('ro') ro;
|
||||
|
||||
@json('v', sanitizer) v;
|
||||
|
||||
@json('served_by', sanitizer) servedBy;
|
||||
|
||||
@field('department_id') departmentId;
|
||||
|
||||
@json('livechat_data', sanitizer) livechatData;
|
||||
|
||||
@json('tags', sanitizer) tags;
|
||||
}
|
||||
|
|
|
@ -17,4 +17,12 @@ export default class Server extends Model {
|
|||
@date('rooms_updated_at') roomsUpdatedAt;
|
||||
|
||||
@field('version') version;
|
||||
|
||||
@date('last_local_authenticated_session') lastLocalAuthenticatedSession;
|
||||
|
||||
@field('auto_lock') autoLock;
|
||||
|
||||
@field('auto_lock_time') autoLockTime;
|
||||
|
||||
@field('biometry') biometry;
|
||||
}
|
||||
|
|
|
@ -50,6 +50,8 @@ export default class Subscription extends Model {
|
|||
|
||||
@field('announcement') announcement;
|
||||
|
||||
@field('banner_closed') bannerClosed;
|
||||
|
||||
@field('topic') topic;
|
||||
|
||||
@field('blocked') blocked;
|
||||
|
@ -95,4 +97,14 @@ export default class Subscription extends Model {
|
|||
@json('uids', sanitizer) uids;
|
||||
|
||||
@json('usernames', sanitizer) usernames;
|
||||
|
||||
@json('visitor', sanitizer) visitor;
|
||||
|
||||
@field('department_id') departmentId;
|
||||
|
||||
@json('served_by', sanitizer) servedBy;
|
||||
|
||||
@json('livechat_data', sanitizer) livechatData;
|
||||
|
||||
@json('tags', sanitizer) tags;
|
||||
}
|
||||
|
|
|
@ -30,6 +30,8 @@ export default class Thread extends Model {
|
|||
|
||||
@field('avatar') avatar;
|
||||
|
||||
@field('emoji') emoji;
|
||||
|
||||
@json('attachments', sanitizer) attachments;
|
||||
|
||||
@json('urls', sanitizer) urls;
|
||||
|
|
|
@ -32,6 +32,8 @@ export default class ThreadMessage extends Model {
|
|||
|
||||
@field('avatar') avatar;
|
||||
|
||||
@field('emoji') emoji;
|
||||
|
||||
@json('attachments', sanitizer) attachments;
|
||||
|
||||
@json('urls', sanitizer) urls;
|
||||
|
|
|
@ -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 }
|
||||
]
|
||||
})
|
||||
]
|
||||
}
|
||||
]
|
||||
});
|
||||
|
|
|
@ -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 }
|
||||
]
|
||||
})
|
||||
]
|
||||
}
|
||||
]
|
||||
});
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { appSchema, tableSchema } from '@nozbe/watermelondb';
|
||||
|
||||
export default appSchema({
|
||||
version: 7,
|
||||
version: 8,
|
||||
tables: [
|
||||
tableSchema({
|
||||
name: 'subscriptions',
|
||||
|
@ -25,6 +25,7 @@ export default appSchema({
|
|||
{ name: 'last_message', type: 'string', isOptional: true },
|
||||
{ name: 'description', type: 'string', isOptional: true },
|
||||
{ name: 'announcement', type: 'string', isOptional: true },
|
||||
{ name: 'banner_closed', type: 'boolean', isOptional: true },
|
||||
{ name: 'topic', type: 'string', isOptional: true },
|
||||
{ name: 'blocked', type: 'boolean', isOptional: true },
|
||||
{ name: 'blocker', type: 'boolean', isOptional: true },
|
||||
|
@ -42,7 +43,12 @@ export default appSchema({
|
|||
{ name: 'hide_unread_status', type: 'boolean', isOptional: true },
|
||||
{ name: 'sys_mes', type: 'string', isOptional: true },
|
||||
{ name: 'uids', type: 'string', isOptional: true },
|
||||
{ name: 'usernames', type: 'string', isOptional: true }
|
||||
{ name: 'usernames', type: 'string', isOptional: true },
|
||||
{ name: 'visitor', type: 'string', isOptional: true },
|
||||
{ name: 'department_id', type: 'string', isOptional: true },
|
||||
{ name: 'served_by', type: 'string', isOptional: true },
|
||||
{ name: 'livechat_data', type: 'string', isOptional: true },
|
||||
{ name: 'tags', type: 'string', isOptional: true }
|
||||
]
|
||||
}),
|
||||
tableSchema({
|
||||
|
@ -51,7 +57,12 @@ export default appSchema({
|
|||
{ name: 'custom_fields', type: 'string' },
|
||||
{ name: 'broadcast', type: 'boolean' },
|
||||
{ name: 'encrypted', type: 'boolean' },
|
||||
{ name: 'ro', type: 'boolean' }
|
||||
{ name: 'ro', type: 'boolean' },
|
||||
{ name: 'v', type: 'string', isOptional: true },
|
||||
{ name: 'department_id', type: 'string', isOptional: true },
|
||||
{ name: 'served_by', type: 'string', isOptional: true },
|
||||
{ name: 'livechat_data', type: 'string', isOptional: true },
|
||||
{ name: 'tags', type: 'string', isOptional: true }
|
||||
]
|
||||
}),
|
||||
tableSchema({
|
||||
|
@ -66,6 +77,7 @@ export default appSchema({
|
|||
{ name: 'parse_urls', type: 'string' },
|
||||
{ name: 'groupable', type: 'boolean', isOptional: true },
|
||||
{ name: 'avatar', type: 'string', isOptional: true },
|
||||
{ name: 'emoji', type: 'string', isOptional: true },
|
||||
{ name: 'attachments', type: 'string', isOptional: true },
|
||||
{ name: 'urls', type: 'string', isOptional: true },
|
||||
{ name: '_updated_at', type: 'number' },
|
||||
|
@ -104,6 +116,7 @@ export default appSchema({
|
|||
{ name: 'parse_urls', type: 'string', isOptional: true },
|
||||
{ name: 'groupable', type: 'boolean', isOptional: true },
|
||||
{ name: 'avatar', type: 'string', isOptional: true },
|
||||
{ name: 'emoji', type: 'string', isOptional: true },
|
||||
{ name: 'attachments', type: 'string', isOptional: true },
|
||||
{ name: 'urls', type: 'string', isOptional: true },
|
||||
{ name: 'status', type: 'number', isOptional: true },
|
||||
|
@ -140,6 +153,7 @@ export default appSchema({
|
|||
{ name: 'parse_urls', type: 'string', isOptional: true },
|
||||
{ name: 'groupable', type: 'boolean', isOptional: true },
|
||||
{ name: 'avatar', type: 'string', isOptional: true },
|
||||
{ name: 'emoji', type: 'string', isOptional: true },
|
||||
{ name: 'attachments', type: 'string', isOptional: true },
|
||||
{ name: 'urls', type: 'string', isOptional: true },
|
||||
{ name: 'status', type: 'number', isOptional: true },
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { appSchema, tableSchema } from '@nozbe/watermelondb';
|
||||
|
||||
export default appSchema({
|
||||
version: 3,
|
||||
version: 4,
|
||||
tables: [
|
||||
tableSchema({
|
||||
name: 'users',
|
||||
|
@ -24,7 +24,11 @@ export default appSchema({
|
|||
{ name: 'file_upload_media_type_white_list', type: 'string', isOptional: true },
|
||||
{ name: 'file_upload_max_file_size', type: 'number', isOptional: true },
|
||||
{ name: 'rooms_updated_at', type: 'number', isOptional: true },
|
||||
{ name: 'version', type: 'string', isOptional: true }
|
||||
{ name: 'version', type: 'string', isOptional: true },
|
||||
{ name: 'last_local_authenticated_session', type: 'number', isOptional: true },
|
||||
{ name: 'auto_lock', type: 'boolean', isOptional: true },
|
||||
{ name: 'auto_lock_time', type: 'number', isOptional: true },
|
||||
{ name: 'biometry', type: 'boolean', isOptional: true }
|
||||
]
|
||||
})
|
||||
]
|
||||
|
|
|
@ -10,8 +10,9 @@ import log from '../../utils/log';
|
|||
import database from '../database';
|
||||
import protectedFunction from './helpers/protectedFunction';
|
||||
import fetch from '../../utils/fetch';
|
||||
import { DEFAULT_AUTO_LOCK } from '../../constants/localAuthentication';
|
||||
|
||||
const serverInfoKeys = ['Site_Name', 'UI_Use_Real_Name', 'FileUpload_MediaTypeWhiteList', 'FileUpload_MaxFileSize'];
|
||||
const serverInfoKeys = ['Site_Name', 'UI_Use_Real_Name', 'FileUpload_MediaTypeWhiteList', 'FileUpload_MaxFileSize', 'Force_Screen_Lock', 'Force_Screen_Lock_After'];
|
||||
|
||||
// these settings are used only on onboarding process
|
||||
const loginSettings = [
|
||||
|
@ -32,6 +33,8 @@ const loginSettings = [
|
|||
const serverInfoUpdate = async(serverInfo, iconSetting) => {
|
||||
const serversDB = database.servers;
|
||||
const serverId = reduxStore.getState().server.server;
|
||||
const serversCollection = serversDB.collections.get('servers');
|
||||
const server = await serversCollection.find(serverId);
|
||||
|
||||
let info = serverInfo.reduce((allSettings, setting) => {
|
||||
if (setting._id === 'Site_Name') {
|
||||
|
@ -46,6 +49,23 @@ const serverInfoUpdate = async(serverInfo, iconSetting) => {
|
|||
if (setting._id === 'FileUpload_MaxFileSize') {
|
||||
return { ...allSettings, FileUpload_MaxFileSize: setting.valueAsNumber };
|
||||
}
|
||||
if (setting._id === 'Force_Screen_Lock') {
|
||||
// if this was disabled on server side we must keep this enabled on app
|
||||
const autoLock = server.autoLock || setting.valueAsBoolean;
|
||||
return { ...allSettings, autoLock };
|
||||
}
|
||||
if (setting._id === 'Force_Screen_Lock_After') {
|
||||
const forceScreenLock = serverInfo.find(s => s._id === 'Force_Screen_Lock')?.valueAsBoolean;
|
||||
|
||||
// if Force_Screen_Lock_After === 0 and autoLockTime is null, set app's default value
|
||||
if (setting.valueAsNumber === 0 && !server.autoLockTime) {
|
||||
return { ...allSettings, autoLockTime: DEFAULT_AUTO_LOCK };
|
||||
}
|
||||
// if Force_Screen_Lock_After > 0 and forceScreenLock is enabled, use it
|
||||
if (setting.valueAsNumber > 0 && forceScreenLock) {
|
||||
return { ...allSettings, autoLockTime: setting.valueAsNumber };
|
||||
}
|
||||
}
|
||||
return allSettings;
|
||||
}, {});
|
||||
|
||||
|
@ -56,9 +76,6 @@ const serverInfoUpdate = async(serverInfo, iconSetting) => {
|
|||
|
||||
await serversDB.action(async() => {
|
||||
try {
|
||||
const serversCollection = serversDB.collections.get('servers');
|
||||
const server = await serversCollection.find(serverId);
|
||||
|
||||
await server.update((record) => {
|
||||
Object.assign(record, info);
|
||||
});
|
||||
|
|
|
@ -27,6 +27,7 @@ export default async(subscriptions = [], rooms = []) => {
|
|||
lastOpen: s.lastOpen,
|
||||
description: s.description,
|
||||
announcement: s.announcement,
|
||||
bannerClosed: s.bannerClosed,
|
||||
topic: s.topic,
|
||||
blocked: s.blocked,
|
||||
blocker: s.blocker,
|
||||
|
@ -43,7 +44,12 @@ export default async(subscriptions = [], rooms = []) => {
|
|||
autoTranslateLanguage: s.autoTranslateLanguage,
|
||||
lastMessage: s.lastMessage,
|
||||
usernames: s.usernames,
|
||||
uids: s.uids
|
||||
uids: s.uids,
|
||||
visitor: s.visitor,
|
||||
departmentId: s.departmentId,
|
||||
servedBy: s.servedBy,
|
||||
livechatData: s.livechatData,
|
||||
tags: s.tags
|
||||
}));
|
||||
subscriptions = subscriptions.concat(existingSubs);
|
||||
|
||||
|
@ -64,7 +70,12 @@ export default async(subscriptions = [], rooms = []) => {
|
|||
ro: r.ro,
|
||||
broadcast: r.broadcast,
|
||||
muted: r.muted,
|
||||
sysMes: r.sysMes
|
||||
sysMes: r.sysMes,
|
||||
v: r.v,
|
||||
departmentId: r.departmentId,
|
||||
servedBy: r.servedBy,
|
||||
livechatData: r.livechatData,
|
||||
tags: r.tags
|
||||
}));
|
||||
rooms = rooms.concat(existingRooms);
|
||||
} catch {
|
||||
|
|
|
@ -35,6 +35,21 @@ export const merge = (subscription, room) => {
|
|||
} else {
|
||||
subscription.muted = [];
|
||||
}
|
||||
if (room.v) {
|
||||
subscription.visitor = room.v;
|
||||
}
|
||||
if (room.departmentId) {
|
||||
subscription.departmentId = room.departmentId;
|
||||
}
|
||||
if (room.servedBy) {
|
||||
subscription.servedBy = room.servedBy;
|
||||
}
|
||||
if (room.livechatData) {
|
||||
subscription.livechatData = room.livechatData;
|
||||
}
|
||||
if (room.tags) {
|
||||
subscription.tags = room.tags;
|
||||
}
|
||||
subscription.sysMes = room.sysMes;
|
||||
}
|
||||
|
||||
|
|
|
@ -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 });
|
||||
}
|
|
@ -62,7 +62,7 @@ export function sendFileMessage(rid, fileInfo, tmid, server, user) {
|
|||
formData.append('file', {
|
||||
uri: fileInfo.path,
|
||||
type: fileInfo.type,
|
||||
name: fileInfo.name || 'fileMessage'
|
||||
name: encodeURI(fileInfo.name) || 'fileMessage'
|
||||
});
|
||||
|
||||
if (fileInfo.description) {
|
||||
|
|
|
@ -10,6 +10,7 @@ import reduxStore from '../../createStore';
|
|||
import { addUserTyping, removeUserTyping, clearUserTyping } from '../../../actions/usersTyping';
|
||||
import debounce from '../../../utils/debounce';
|
||||
import RocketChat from '../../rocketchat';
|
||||
import { subscribeRoom, unsubscribeRoom } from '../../../actions/room';
|
||||
|
||||
const WINDOW_TIME = 1000;
|
||||
|
||||
|
@ -38,6 +39,8 @@ export default class RoomSubscription {
|
|||
if (!this.isAlive) {
|
||||
this.unsubscribe();
|
||||
}
|
||||
|
||||
reduxStore.dispatch(subscribeRoom(this.rid));
|
||||
}
|
||||
|
||||
unsubscribe = async() => {
|
||||
|
@ -59,6 +62,8 @@ export default class RoomSubscription {
|
|||
if (this.timer) {
|
||||
clearTimeout(this.timer);
|
||||
}
|
||||
|
||||
reduxStore.dispatch(unsubscribeRoom(this.rid));
|
||||
}
|
||||
|
||||
removeListener = async(promise) => {
|
||||
|
@ -155,22 +160,17 @@ export default class RoomSubscription {
|
|||
const msgCollection = db.collections.get('messages');
|
||||
const threadsCollection = db.collections.get('threads');
|
||||
const threadMessagesCollection = db.collections.get('thread_messages');
|
||||
let messageRecord;
|
||||
let threadRecord;
|
||||
let threadMessageRecord;
|
||||
|
||||
// Create or update message
|
||||
try {
|
||||
messageRecord = await msgCollection.find(message._id);
|
||||
} catch (error) {
|
||||
// Do nothing
|
||||
}
|
||||
if (messageRecord) {
|
||||
const update = messageRecord.prepareUpdate((m) => {
|
||||
Object.assign(m, message);
|
||||
});
|
||||
this._messagesBatch[message._id] = update;
|
||||
} else {
|
||||
const messageRecord = await msgCollection.find(message._id);
|
||||
if (!messageRecord._hasPendingUpdate) {
|
||||
const update = messageRecord.prepareUpdate(protectedFunction((m) => {
|
||||
Object.assign(m, message);
|
||||
}));
|
||||
this._messagesBatch[message._id] = update;
|
||||
}
|
||||
} catch {
|
||||
const create = msgCollection.prepareCreate(protectedFunction((m) => {
|
||||
m._raw = sanitizedRaw({ id: message._id }, msgCollection.schema);
|
||||
m.subscription.id = this.rid;
|
||||
|
@ -182,17 +182,14 @@ export default class RoomSubscription {
|
|||
// Create or update thread
|
||||
if (message.tlm) {
|
||||
try {
|
||||
threadRecord = await threadsCollection.find(message._id);
|
||||
} catch (error) {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
if (threadRecord) {
|
||||
const updateThread = threadRecord.prepareUpdate(protectedFunction((t) => {
|
||||
Object.assign(t, message);
|
||||
}));
|
||||
this._threadsBatch[message._id] = updateThread;
|
||||
} else {
|
||||
const threadRecord = await threadsCollection.find(message._id);
|
||||
if (!threadRecord._hasPendingUpdate) {
|
||||
const updateThread = threadRecord.prepareUpdate(protectedFunction((t) => {
|
||||
Object.assign(t, message);
|
||||
}));
|
||||
this._threadsBatch[message._id] = updateThread;
|
||||
}
|
||||
} catch {
|
||||
const createThread = threadsCollection.prepareCreate(protectedFunction((t) => {
|
||||
t._raw = sanitizedRaw({ id: message._id }, threadsCollection.schema);
|
||||
t.subscription.id = this.rid;
|
||||
|
@ -205,19 +202,16 @@ export default class RoomSubscription {
|
|||
// Create or update thread message
|
||||
if (message.tmid) {
|
||||
try {
|
||||
threadMessageRecord = await threadMessagesCollection.find(message._id);
|
||||
} catch (error) {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
if (threadMessageRecord) {
|
||||
const updateThreadMessage = threadMessageRecord.prepareUpdate(protectedFunction((tm) => {
|
||||
Object.assign(tm, message);
|
||||
tm.rid = message.tmid;
|
||||
delete tm.tmid;
|
||||
}));
|
||||
this._threadMessagesBatch[message._id] = updateThreadMessage;
|
||||
} else {
|
||||
const threadMessageRecord = await threadMessagesCollection.find(message._id);
|
||||
if (!threadMessageRecord._hasPendingUpdate) {
|
||||
const updateThreadMessage = threadMessageRecord.prepareUpdate(protectedFunction((tm) => {
|
||||
Object.assign(tm, message);
|
||||
tm.rid = message.tmid;
|
||||
delete tm.tmid;
|
||||
}));
|
||||
this._threadMessagesBatch[message._id] = updateThreadMessage;
|
||||
}
|
||||
} catch {
|
||||
const createThreadMessage = threadMessagesCollection.prepareCreate(protectedFunction((tm) => {
|
||||
tm._raw = sanitizedRaw({ id: message._id }, threadMessagesCollection.schema);
|
||||
Object.assign(tm, message);
|
||||
|
|
|
@ -57,6 +57,7 @@ const createOrUpdateSubscription = async(subscription, room) => {
|
|||
lastOpen: s.lastOpen,
|
||||
description: s.description,
|
||||
announcement: s.announcement,
|
||||
bannerClosed: s.bannerClosed,
|
||||
topic: s.topic,
|
||||
blocked: s.blocked,
|
||||
blocker: s.blocker,
|
||||
|
@ -74,7 +75,12 @@ const createOrUpdateSubscription = async(subscription, room) => {
|
|||
lastMessage: s.lastMessage,
|
||||
roles: s.roles,
|
||||
usernames: s.usernames,
|
||||
uids: s.uids
|
||||
uids: s.uids,
|
||||
visitor: s.visitor,
|
||||
departmentId: s.departmentId,
|
||||
servedBy: s.servedBy,
|
||||
livechatData: s.livechatData,
|
||||
tags: s.tags
|
||||
};
|
||||
} catch (error) {
|
||||
try {
|
||||
|
@ -97,10 +103,15 @@ const createOrUpdateSubscription = async(subscription, room) => {
|
|||
// We have to create a plain obj so we can manipulate it on `merge`
|
||||
// Can we do it in a better way?
|
||||
room = {
|
||||
customFields: r.customFields,
|
||||
broadcast: r.broadcast,
|
||||
v: r.v,
|
||||
ro: r.ro,
|
||||
tags: r.tags,
|
||||
servedBy: r.servedBy,
|
||||
encrypted: r.encrypted,
|
||||
ro: r.ro
|
||||
broadcast: r.broadcast,
|
||||
customFields: r.customFields,
|
||||
departmentId: r.departmentId,
|
||||
livechatData: r.livechatData
|
||||
};
|
||||
} catch (error) {
|
||||
// Do nothing
|
||||
|
@ -121,6 +132,11 @@ const createOrUpdateSubscription = async(subscription, room) => {
|
|||
try {
|
||||
const update = sub.prepareUpdate((s) => {
|
||||
Object.assign(s, tmp);
|
||||
if (subscription.announcement) {
|
||||
if (subscription.announcement !== sub.announcement) {
|
||||
s.bannerClosed = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
batch.push(update);
|
||||
} catch (e) {
|
||||
|
@ -141,7 +157,8 @@ const createOrUpdateSubscription = async(subscription, room) => {
|
|||
}
|
||||
}
|
||||
|
||||
if (tmp.lastMessage) {
|
||||
const { rooms } = store.getState().room;
|
||||
if (tmp.lastMessage && !rooms.includes(tmp.rid)) {
|
||||
const lastMessage = buildMessage(tmp.lastMessage);
|
||||
const messagesCollection = db.collections.get('messages');
|
||||
let messageRecord;
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { AsyncStorage, InteractionManager } from 'react-native';
|
||||
import { InteractionManager } from 'react-native';
|
||||
import semver from 'semver';
|
||||
import { Rocketchat as RocketchatClient } from '@rocket.chat/sdk';
|
||||
import RNUserDefaults from 'rn-user-defaults';
|
||||
import { Q } from '@nozbe/watermelondb';
|
||||
import * as FileSystem from 'expo-file-system';
|
||||
import AsyncStorage from '@react-native-community/async-storage';
|
||||
|
||||
import reduxStore from './createStore';
|
||||
import defaultSettings from '../constants/settings';
|
||||
|
@ -11,8 +11,7 @@ import messagesStatus from '../constants/messagesStatus';
|
|||
import database from './database';
|
||||
import log from '../utils/log';
|
||||
import { isIOS, getBundleId } from '../utils/deviceInfo';
|
||||
import { extractHostname } from '../utils/server';
|
||||
import fetch, { BASIC_AUTH_KEY } from '../utils/fetch';
|
||||
import fetch from '../utils/fetch';
|
||||
|
||||
import { setUser, setLoginServices, loginRequest } from '../actions/login';
|
||||
import { disconnect, connectSuccess, connectRequest } from '../actions/connect';
|
||||
|
@ -43,12 +42,14 @@ import sendMessage, { sendMessageCall } from './methods/sendMessage';
|
|||
import { sendFileMessage, cancelUpload, isUploadActive } from './methods/sendFileMessage';
|
||||
|
||||
import callJitsi from './methods/callJitsi';
|
||||
import logout, { removeServer } from './methods/logout';
|
||||
|
||||
import { getDeviceToken } from '../notifications/push';
|
||||
import { SERVERS, SERVER_URL } from '../constants/userDefaults';
|
||||
import { setActiveUsers } from '../actions/activeUsers';
|
||||
import I18n from '../i18n';
|
||||
import { twoFactor } from '../utils/twoFactor';
|
||||
import { selectServerFailure } from '../actions/server';
|
||||
import { useSsl } from '../utils/url';
|
||||
|
||||
const TOKEN_KEY = 'reactnativemeteor_usertoken';
|
||||
const SORT_PREFS_KEY = 'RC_SORT_PREFS_KEY';
|
||||
|
@ -86,10 +87,7 @@ const RocketChat = {
|
|||
}
|
||||
},
|
||||
async getWebsocketInfo({ server }) {
|
||||
// Use useSsl: false only if server url starts with http://
|
||||
const useSsl = !/http:\/\//.test(server);
|
||||
|
||||
const sdk = new RocketchatClient({ host: server, protocol: 'ddp', useSsl });
|
||||
const sdk = new RocketchatClient({ host: server, protocol: 'ddp', useSsl: useSsl(server) });
|
||||
|
||||
try {
|
||||
await sdk.connect();
|
||||
|
@ -146,6 +144,10 @@ const RocketChat = {
|
|||
}
|
||||
return result;
|
||||
} catch (e) {
|
||||
if (e.message === 'Aborted') {
|
||||
reduxStore.dispatch(selectServerFailure());
|
||||
throw e;
|
||||
}
|
||||
log(e);
|
||||
}
|
||||
return {
|
||||
|
@ -159,6 +161,16 @@ const RocketChat = {
|
|||
stopListener(listener) {
|
||||
return listener && listener.stop();
|
||||
},
|
||||
// Abort all requests and create a new AbortController
|
||||
abort() {
|
||||
if (this.controller) {
|
||||
this.controller.abort();
|
||||
if (this.sdk) {
|
||||
this.sdk.abort();
|
||||
}
|
||||
}
|
||||
this.controller = new AbortController();
|
||||
},
|
||||
connect({ server, user, logoutOnError = false }) {
|
||||
return new Promise((resolve) => {
|
||||
if (!this.sdk || this.sdk.client.host !== server) {
|
||||
|
@ -200,15 +212,13 @@ const RocketChat = {
|
|||
this.code = null;
|
||||
}
|
||||
|
||||
// Use useSsl: false only if server url starts with http://
|
||||
const useSsl = !/http:\/\//.test(server);
|
||||
|
||||
this.sdk = new RocketchatClient({ host: server, protocol: 'ddp', useSsl });
|
||||
this.sdk = new RocketchatClient({ host: server, protocol: 'ddp', useSsl: useSsl(server) });
|
||||
this.getSettings();
|
||||
|
||||
const sdkConnect = () => this.sdk.connect()
|
||||
.then(() => {
|
||||
if (user && user.token) {
|
||||
const { server: currentServer } = reduxStore.getState().server;
|
||||
if (user && user.token && server === currentServer) {
|
||||
reduxStore.dispatch(loginRequest({ resume: user.token }, logoutOnError));
|
||||
}
|
||||
})
|
||||
|
@ -217,7 +227,9 @@ const RocketChat = {
|
|||
|
||||
// when `connect` raises an error, we try again in 10 seconds
|
||||
this.connectTimeout = setTimeout(() => {
|
||||
sdkConnect();
|
||||
if (this.sdk?.client?.host === server) {
|
||||
sdkConnect();
|
||||
}
|
||||
}, 10000);
|
||||
});
|
||||
|
||||
|
@ -270,10 +282,7 @@ const RocketChat = {
|
|||
this.shareSDK = null;
|
||||
}
|
||||
|
||||
// Use useSsl: false only if server url starts with http://
|
||||
const useSsl = !/http:\/\//.test(server);
|
||||
|
||||
this.shareSDK = new RocketchatClient({ host: server, protocol: 'ddp', useSsl });
|
||||
this.shareSDK = new RocketchatClient({ host: server, protocol: 'ddp', useSsl: useSsl(server) });
|
||||
|
||||
// set Server
|
||||
const serversDB = database.servers;
|
||||
|
@ -306,7 +315,7 @@ const RocketChat = {
|
|||
}
|
||||
database.share = null;
|
||||
|
||||
reduxStore.dispatch(shareSetUser(null));
|
||||
reduxStore.dispatch(shareSetUser({}));
|
||||
},
|
||||
|
||||
updateJitsiTimeout(rid) {
|
||||
|
@ -369,112 +378,35 @@ const RocketChat = {
|
|||
};
|
||||
}
|
||||
|
||||
try {
|
||||
return this.loginTOTP(params);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
return this.loginTOTP(params);
|
||||
},
|
||||
|
||||
async loginOAuthOrSso(params) {
|
||||
try {
|
||||
const result = await this.login(params);
|
||||
reduxStore.dispatch(loginRequest({ resume: result.token }));
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
const result = await this.login(params);
|
||||
reduxStore.dispatch(loginRequest({ resume: result.token }));
|
||||
},
|
||||
|
||||
async login(params) {
|
||||
try {
|
||||
const sdk = this.shareSDK || this.sdk;
|
||||
// RC 0.64.0
|
||||
await sdk.login(params);
|
||||
const { result } = sdk.currentLogin;
|
||||
const user = {
|
||||
id: result.userId,
|
||||
token: result.authToken,
|
||||
username: result.me.username,
|
||||
name: result.me.name,
|
||||
language: result.me.language,
|
||||
status: result.me.status,
|
||||
statusText: result.me.statusText,
|
||||
customFields: result.me.customFields,
|
||||
emails: result.me.emails,
|
||||
roles: result.me.roles
|
||||
};
|
||||
return user;
|
||||
} catch (e) {
|
||||
throw e;
|
||||
}
|
||||
},
|
||||
async logout({ server }) {
|
||||
if (this.roomsSub) {
|
||||
this.roomsSub.stop();
|
||||
this.roomsSub = null;
|
||||
}
|
||||
|
||||
if (this.activeUsersSubTimeout) {
|
||||
clearTimeout(this.activeUsersSubTimeout);
|
||||
this.activeUsersSubTimeout = false;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.removePushToken();
|
||||
} catch (error) {
|
||||
console.log('logout -> removePushToken -> catch -> error', error);
|
||||
}
|
||||
try {
|
||||
// RC 0.60.0
|
||||
await this.sdk.logout();
|
||||
} catch (error) {
|
||||
console.log('logout -> api logout -> catch -> error', error);
|
||||
}
|
||||
this.sdk = null;
|
||||
|
||||
try {
|
||||
const servers = await RNUserDefaults.objectForKey(SERVERS);
|
||||
await RNUserDefaults.setObjectForKey(SERVERS, servers && servers.filter(srv => srv[SERVER_URL] !== server));
|
||||
// clear certificate for server - SSL Pinning
|
||||
const certificate = await RNUserDefaults.objectForKey(extractHostname(server));
|
||||
if (certificate && certificate.path) {
|
||||
await RNUserDefaults.clear(extractHostname(server));
|
||||
await FileSystem.deleteAsync(certificate.path);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('logout_rn_user_defaults', error);
|
||||
}
|
||||
|
||||
const userId = await RNUserDefaults.get(`${ TOKEN_KEY }-${ server }`);
|
||||
|
||||
try {
|
||||
const serversDB = database.servers;
|
||||
await serversDB.action(async() => {
|
||||
const usersCollection = serversDB.collections.get('users');
|
||||
const userRecord = await usersCollection.find(userId);
|
||||
const serverCollection = serversDB.collections.get('servers');
|
||||
const serverRecord = await serverCollection.find(server);
|
||||
await serversDB.batch(
|
||||
userRecord.prepareDestroyPermanently(),
|
||||
serverRecord.prepareDestroyPermanently()
|
||||
);
|
||||
});
|
||||
} catch (error) {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
await RNUserDefaults.clear('currentServer');
|
||||
await RNUserDefaults.clear(TOKEN_KEY);
|
||||
await RNUserDefaults.clear(`${ TOKEN_KEY }-${ server }`);
|
||||
await RNUserDefaults.clear(`${ BASIC_AUTH_KEY }-${ server }`);
|
||||
|
||||
try {
|
||||
const db = database.active;
|
||||
await db.action(() => db.unsafeResetDatabase());
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
const sdk = this.shareSDK || this.sdk;
|
||||
// RC 0.64.0
|
||||
await sdk.login(params);
|
||||
const { result } = sdk.currentLogin;
|
||||
const user = {
|
||||
id: result.userId,
|
||||
token: result.authToken,
|
||||
username: result.me.username,
|
||||
name: result.me.name,
|
||||
language: result.me.language,
|
||||
status: result.me.status,
|
||||
statusText: result.me.statusText,
|
||||
customFields: result.me.customFields,
|
||||
emails: result.me.emails,
|
||||
roles: result.me.roles
|
||||
};
|
||||
return user;
|
||||
},
|
||||
logout,
|
||||
removeServer,
|
||||
async clearCache({ server }) {
|
||||
try {
|
||||
const serversDB = database.servers;
|
||||
|
@ -573,9 +505,9 @@ const RocketChat = {
|
|||
).fetch();
|
||||
|
||||
if (filterUsers && !filterRooms) {
|
||||
data = data.filter(item => item.t === 'd');
|
||||
data = data.filter(item => item.t === 'd' && !RocketChat.isGroupChat(item));
|
||||
} else if (!filterUsers && filterRooms) {
|
||||
data = data.filter(item => item.t !== 'd');
|
||||
data = data.filter(item => item.t !== 'd' || RocketChat.isGroupChat(item));
|
||||
}
|
||||
data = data.slice(0, 7);
|
||||
|
||||
|
@ -824,6 +756,59 @@ const RocketChat = {
|
|||
return this.sdk.get('rooms.info', { roomId });
|
||||
},
|
||||
|
||||
getVisitorInfo(visitorId) {
|
||||
// RC 2.3.0
|
||||
return this.sdk.get('livechat/visitors.info', { visitorId });
|
||||
},
|
||||
closeLivechat(rid, comment) {
|
||||
// RC 0.29.0
|
||||
return this.methodCall('livechat:closeRoom', rid, comment, { clientAction: true });
|
||||
},
|
||||
editLivechat(userData, roomData) {
|
||||
// RC 0.55.0
|
||||
return this.methodCall('livechat:saveInfo', userData, roomData);
|
||||
},
|
||||
returnLivechat(rid) {
|
||||
// RC 0.72.0
|
||||
return this.methodCall('livechat:returnAsInquiry', rid);
|
||||
},
|
||||
forwardLivechat(transferData) {
|
||||
// RC 0.36.0
|
||||
return this.methodCall('livechat:transfer', transferData);
|
||||
},
|
||||
getPagesLivechat(rid, offset) {
|
||||
// RC 2.3.0
|
||||
return this.sdk.get(`livechat/visitors.pagesVisited/${ rid }?count=50&offset=${ offset }`);
|
||||
},
|
||||
getDepartmentInfo(departmentId) {
|
||||
// RC 2.2.0
|
||||
return this.sdk.get(`livechat/department/${ departmentId }?includeAgents=false`);
|
||||
},
|
||||
getDepartments() {
|
||||
// RC 2.2.0
|
||||
return this.sdk.get('livechat/department');
|
||||
},
|
||||
usersAutoComplete(selector) {
|
||||
// RC 2.4.0
|
||||
return this.sdk.get('users.autocomplete', { selector });
|
||||
},
|
||||
getRoutingConfig() {
|
||||
// RC 2.0.0
|
||||
return this.methodCall('livechat:getRoutingConfig');
|
||||
},
|
||||
getTagsList() {
|
||||
// RC 2.0.0
|
||||
return this.methodCall('livechat:getTagsList');
|
||||
},
|
||||
getAgentDepartments(uid) {
|
||||
// RC 2.4.0
|
||||
return this.sdk.get(`livechat/agents/${ uid }/departments`);
|
||||
},
|
||||
getCustomFields() {
|
||||
// RC 2.2.0
|
||||
return this.sdk.get('livechat/custom-fields');
|
||||
},
|
||||
|
||||
getUidDirectMessage(room) {
|
||||
const { id: userId } = reduxStore.getState().login.user;
|
||||
|
||||
|
@ -909,7 +894,7 @@ const RocketChat = {
|
|||
methodCall(...args) {
|
||||
return new Promise(async(resolve, reject) => {
|
||||
try {
|
||||
const result = await this.sdk.methodCall(...args, this.code);
|
||||
const result = await this.sdk.methodCall(...args, this.code || '');
|
||||
return resolve(result);
|
||||
} catch (e) {
|
||||
if (e.error && (e.error === 'totp-required' || e.error === 'totp-invalid')) {
|
||||
|
@ -977,7 +962,7 @@ const RocketChat = {
|
|||
const shareUser = reduxStore.getState().share.user;
|
||||
const loginUser = reduxStore.getState().login.user;
|
||||
// get user roles on the server from redux
|
||||
const userRoles = (shareUser.roles || loginUser.roles) || [];
|
||||
const userRoles = (shareUser?.roles || loginUser?.roles) || [];
|
||||
// merge both roles
|
||||
const mergedRoles = [...new Set([...roomRoles, ...userRoles])];
|
||||
|
||||
|
@ -1226,16 +1211,19 @@ const RocketChat = {
|
|||
return this.methodCall('autoTranslate.translateMessage', message, targetLanguage);
|
||||
},
|
||||
getRoomTitle(room) {
|
||||
const { UI_Use_Real_Name: useRealName } = reduxStore.getState().settings;
|
||||
const { UI_Use_Real_Name: useRealName, UI_Allow_room_names_with_special_chars: allowSpecialChars } = reduxStore.getState().settings;
|
||||
const { username } = reduxStore.getState().login.user;
|
||||
if (RocketChat.isGroupChat(room) && !(room.name && room.name.length)) {
|
||||
return room.usernames.filter(u => u !== username).sort((u1, u2) => u1.localeCompare(u2)).join(', ');
|
||||
}
|
||||
if (allowSpecialChars && room.t !== 'd') {
|
||||
return room.fname || room.name;
|
||||
}
|
||||
return ((room.prid || useRealName) && room.fname) || room.name;
|
||||
},
|
||||
getRoomAvatar(room) {
|
||||
if (RocketChat.isGroupChat(room)) {
|
||||
return room.uids.length + room.usernames.join();
|
||||
return room.uids?.length + room.usernames?.join();
|
||||
}
|
||||
return room.prid ? room.fname : room.name;
|
||||
},
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue