Merge branch 'master' into mark-as-unread

This commit is contained in:
Harrison Healey 2019-10-29 10:00:22 -04:00
commit bf68a33bab
180 changed files with 9307 additions and 15863 deletions

View file

@ -159,39 +159,30 @@ commands:
no_output_timeout: 30m
command: bundle exec fastlane ios build || exit 1
deploy-android:
description: "Deploy apk to Google Play"
deploy-to-store:
description: "Deploy build to store"
parameters:
apk_path:
task:
type: string
target:
type: string
file:
type: string
steps:
- attach_workspace:
at: ~/mattermost-mobile
- deploy:
name: "Deploy apk to Google Play"
at: /
- run:
name: <<parameters.task>>
working_directory: fastlane
command: bundle exec fastlane android deploy apk:../<<parameters.apk_path>>
deploy-ios:
description: "Deploy ipa to TestFlight"
parameters:
ipa_path:
type: string
steps:
- attach_workspace:
at: ~/mattermost-mobile
- deploy:
name: "Deploy ipa to TestFlight"
working_directory: fastlane
command: bundle exec fastlane ios deploy ipa:../<<parameters.ipa_path>>
command: bundle exec fastlane <<parameters.target>> deploy file:$HOME/mattermost-mobile/<<parameters.file>>
persist:
description: "Persist mattermost-mobile directory"
steps:
- persist_to_workspace:
root: ./
root: /
paths:
- ./
- mattermost-mobile*
save:
description: "Save binaries artifacts"
@ -244,7 +235,6 @@ jobs:
BRANCH_TO_BUILD: ${CIRCLE_BRANCH}
steps:
- build-android
- persist
- save:
filename: "Mattermost_Beta.apk"
@ -270,7 +260,6 @@ jobs:
BRANCH_TO_BUILD: ${CIRCLE_BRANCH}
steps:
- build-ios
- persist
- save:
filename: "Mattermost_Beta.ipa"
@ -279,28 +268,36 @@ jobs:
name: android
resource_class: medium
steps:
- deploy-android:
apk_path: Mattermost.apk
- deploy-to-store:
task: "Deploy to Google Play"
target: android
file: Mattermost.apk
deploy-android-beta:
executor:
name: android
resource_class: medium
steps:
- deploy-android:
apk_path: Mattermost_Beta.apk
- deploy-to-store:
task: "Deploy to Google Play"
target: android
file: Mattermost_Beta.apk
deploy-ios-release:
executor: ios
steps:
- deploy-ios:
ipa_path: Mattermost.ipa
- deploy-to-store:
task: "Deploy to TestFlight"
target: ios
file: Mattermost.ipa
deploy-ios-beta:
executor: ios
steps:
- deploy-ios:
ipa_path: Mattermost_Beta.ipa
- deploy-to-store:
task: "Deploy to TestFlight"
target: ios
file: Mattermost_Beta.ipa
workflows:
version: 2

View file

@ -5,26 +5,24 @@
; Ignore "BUCK" generated dirs
<PROJECT_ROOT>/\.buckd/
; Ignore unexpected extra "@providesModule"
.*/node_modules/.*/node_modules/fbjs/.*
; Ignore duplicate module providers
; For RN Apps installed via npm, "Libraries" folder is inside
; "node_modules/react-native" but in the source repo it is in the root
.*/Libraries/react-native/React.js
; Ignore polyfills
.*/Libraries/polyfills/.*
node_modules/react-native/Libraries/polyfills/.*
; Ignore metro
.*/node_modules/metro/.*
; These should not be required directly
; require from fbjs/lib instead: require('fbjs/lib/warning')
node_modules/warning/.*
; Flow doesn't support platforms
.*/Libraries/Utilities/LoadingView.js
[untyped]
.*/node_modules/@react-native-community/cli/.*/.*
[include]
[libs]
node_modules/react-native/Libraries/react-native/react-native-interface.js
node_modules/react-native/flow/
node_modules/react-native/flow-github/
[options]
emoji=true
@ -32,39 +30,46 @@ emoji=true
esproposal.optional_chaining=enable
esproposal.nullish_coalescing=enable
module.system=haste
module.system.haste.use_name_reducers=true
# get basename
module.system.haste.name_reducers='^.*/\([a-zA-Z0-9$_.-]+\.js\(\.flow\)?\)$' -> '\1'
# strip .js or .js.flow suffix
module.system.haste.name_reducers='^\(.*\)\.js\(\.flow\)?$' -> '\1'
# strip .ios suffix
module.system.haste.name_reducers='^\(.*\)\.ios$' -> '\1'
module.system.haste.name_reducers='^\(.*\)\.android$' -> '\1'
module.system.haste.name_reducers='^\(.*\)\.native$' -> '\1'
module.system.haste.paths.blacklist=.*/__tests__/.*
module.system.haste.paths.blacklist=.*/__mocks__/.*
module.system.haste.paths.blacklist=<PROJECT_ROOT>/node_modules/react-native/Libraries/Animated/src/polyfills/.*
module.system.haste.paths.whitelist=<PROJECT_ROOT>/node_modules/react-native/Libraries/.*
module.file_ext=.js
module.file_ext=.json
module.file_ext=.ios.js
munge_underscores=true
module.name_mapper='^[./a-zA-Z0-9$_-]+\.\(bmp\|gif\|jpg\|jpeg\|png\|psd\|svg\|webp\|m4v\|mov\|mp4\|mpeg\|mpg\|webm\|aac\|aiff\|caf\|m4a\|mp3\|wav\|html\|pdf\)$' -> 'RelativeImageStub'
module.file_ext=.js
module.file_ext=.jsx
module.file_ext=.json
module.file_ext=.native.js
module.name_mapper='^react-native$' -> '<PROJECT_ROOT>/node_modules/react-native/Libraries/react-native/react-native-implementation'
module.name_mapper='^react-native/\(.*\)$' -> '<PROJECT_ROOT>/node_modules/react-native/\1'
module.name_mapper='^[./a-zA-Z0-9$_-]+\.\(bmp\|gif\|jpg\|jpeg\|png\|psd\|svg\|webp\|m4v\|mov\|mp4\|mpeg\|mpg\|webm\|aac\|aiff\|caf\|m4a\|mp3\|wav\|html\|pdf\)$' -> '<PROJECT_ROOT>/node_modules/react-native/Libraries/Image/RelativeImageStub'
suppress_type=$FlowIssue
suppress_type=$FlowFixMe
suppress_type=$FlowFixMeProps
suppress_type=$FlowFixMeState
suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(<VERSION>\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)
suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(<VERSION>\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)?:? #[0-9]+
suppress_comment=\\(.\\|\n\\)*\\$FlowFixedInNextDeploy
suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(<VERSION>\\)? *\\(site=[a-z,_]*react_native\\(_ios\\)?_\\(oss\\|fb\\)[a-z,_]*\\)?)\\)
suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(<VERSION>\\)? *\\(site=[a-z,_]*react_native\\(_ios\\)?_\\(oss\\|fb\\)[a-z,_]*\\)?)\\)?:? #[0-9]+
suppress_comment=\\(.\\|\n\\)*\\$FlowExpectedError
[lints]
sketchy-null-number=warn
sketchy-null-mixed=warn
sketchy-number=warn
untyped-type-import=warn
nonstrict-import=warn
deprecated-type=warn
unsafe-getters-setters=warn
inexact-spread=warn
unnecessary-invariant=warn
signature-verification-failure=warn
deprecated-utility=error
[strict]
deprecated-type
nonstrict-import
sketchy-null
unclear-type
unsafe-getters-setters
untyped-import
untyped-type-import
[version]
^0.92.0
^0.105.0

View file

@ -74,6 +74,8 @@ clean: ## Cleans dependencies, previous builds and temp files
@echo Cleanup finished
post-install:
@./node_modules/.bin/jetify
@rm -f node_modules/intl/.babelrc
@# Hack to get react-intl and its dependencies to work with react-native
@# Based off of https://github.com/este/este/blob/master/gulp/native-fix.js

View file

@ -2812,38 +2812,3 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
---
## react-native-syntax-highlighter
This product contains 'react-native-syntax-highlighter' by Conor Hastings.
A syntax highlighter for react native using https://github.com/conorhastings/react-syntax-highlighter under the hood
* HOMEPAGE:
* https://github.com/conorhastings/react-native-syntax-highlighter#readme
* LICENSE: MIT
The MIT License (MIT)
Copyright (c) 2019 Conor Hastings
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -74,8 +74,8 @@ import com.android.build.OutputFile
project.ext.react = [
entryFile: "index.js",
bundleCommand: "ram-bundle",
bundleConfig: "metro.config.js"
bundleConfig: "metro.config.js",
enableHermes: false,
]
apply from: "../../node_modules/react-native/react.gradle"
@ -87,7 +87,7 @@ if (System.getenv("SENTRY_ENABLED") == "true") {
flavorAware: false
]
apply from: "../../node_modules/react-native-sentry/sentry.gradle"
apply from: "../../node_modules/@sentry/react-native/sentry.gradle"
}
/**
@ -105,6 +105,28 @@ def enableSeparateBuildPerCPUArchitecture = false
*/
def enableProguardInReleaseBuilds = false
/**
* The preferred build flavor of JavaScriptCore.
*
* For example, to use the international variant, you can use:
* `def jscFlavor = 'org.webkit:android-jsc-intl:+'`
*
* The international variant includes ICU i18n library and necessary data
* allowing to use e.g. `Date.toLocaleString` and `String.localeCompare` that
* give correct results when using with locales other than en-US. Note that
* this variant is about 6MiB larger per architecture than default.
*/
def jscFlavor = 'org.webkit:android-jsc-intl:r241213'
/**
* Whether to enable the Hermes VM.
*
* This should be set on project.ext.react and mirrored here. If it is not set
* on project.ext.react, JavaScript will not be compiled to Hermes Bytecode
* and the benefits of using Hermes will therefore be sharply reduced.
*/
def enableHermes = project.ext.react.get("enableHermes", false);
android {
compileSdkVersion rootProject.ext.compileSdkVersion
@ -113,17 +135,12 @@ android {
targetCompatibility JavaVersion.VERSION_1_8
}
packagingOptions {
pickFirst '**/libjsc.so'
pickFirst '**/libc++_shared.so'
}
defaultConfig {
applicationId "com.mattermost.rnbeta"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
missingDimensionStrategy "RNN.reactNativeVersion", "reactNative57_5"
versionCode 240
missingDimensionStrategy "RNN.reactNativeVersion", "reactNative60"
versionCode 241
versionName "1.25.0"
multiDexEnabled = true
ndk {
@ -211,15 +228,21 @@ configurations.all {
}
dependencies {
// Make sure to put android-jsc at the top
implementation "org.webkit:android-jsc-intl:r241213"
if (enableHermes) {
def hermesPath = "../../node_modules/hermes-engine/android/";
debugImplementation files(hermesPath + "hermes-debug.aar")
releaseImplementation files(hermesPath + "hermes-release.aar")
} else {
implementation jscFlavor
}
implementation fileTree(dir: "libs", include: ["*.jar"])
implementation "com.android.support:appcompat-v7:${rootProject.ext.supportLibVersion}"
implementation 'com.android.support:design:28.0.0'
implementation 'com.android.support:percent:28.0.0'
implementation "com.google.firebase:firebase-messaging:17.3.0"
implementation "com.facebook.react:react-native:+" // From node_modules
implementation 'androidx.appcompat:appcompat:1.0.0'
implementation 'com.google.android.material:material:1.0.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation "com.google.firebase:firebase-messaging:17.3.0"
implementation project(':react-native-document-picker')
implementation project(':react-native-keychain')
implementation project(':react-native-doc-viewer')
@ -235,22 +258,22 @@ dependencies {
implementation project(':react-native-local-auth')
implementation project(':jail-monkey')
implementation project(':react-native-youtube')
implementation project(':react-native-sentry')
implementation project(':react-native-exception-handler')
implementation project(':rn-fetch-blob')
implementation project(':react-native-webview')
implementation project(':react-native-gesture-handler')
implementation project(':@react-native-community_async-storage')
implementation project(':@react-native-community_netinfo')
implementation project(':@sentry_react-native')
implementation project(':react-native-android-open-settings')
implementation project(':react-native-haptic-feedback')
// For animated GIF support
implementation 'com.facebook.fresco:fresco:1.10.0'
implementation 'com.facebook.fresco:animated-gif:1.10.0'
implementation 'com.facebook.fresco:fresco:2.0.0'
implementation 'com.facebook.fresco:animated-gif:2.0.0'
// For WebP support, including animated WebP
implementation 'com.facebook.fresco:animated-webp:1.10.0'
implementation 'com.facebook.fresco:webpsupport:1.10.0'
implementation 'com.facebook.fresco:animated-webp:2.0.0'
implementation 'com.facebook.fresco:webpsupport:2.0.0'
}
// Run this once to be able to run the application with BUCK

View file

@ -8,10 +8,3 @@
# http://developer.android.com/guide/developing/tools/proguard.html
# Add any project specific keep options here:
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}

View file

@ -29,7 +29,8 @@
android:name=".MainActivity"
android:label="@string/app_name"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
android:windowSoftInputMode="adjustResize">
android:windowSoftInputMode="adjustResize"
android:launchMode="singleInstance">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
@ -52,7 +53,8 @@
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
android:label="@string/app_name"
android:screenOrientation="portrait"
android:theme="@style/AppTheme">
android:theme="@style/AppTheme"
android:taskAffinity="com.mattermost.share">
<intent-filter>
<action android:name="android.intent.action.SEND" />
<action android:name="android.intent.action.SEND_MULTIPLE" />

View file

@ -1,7 +1,7 @@
package com.mattermost.rnbeta;
import android.os.Bundle;
import android.support.annotation.Nullable;
import androidx.annotation.Nullable;
import com.reactnativenavigation.NavigationActivity;
@ -10,19 +10,5 @@ public class MainActivity extends NavigationActivity {
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.launch_screen);
/**
* Reference: https://stackoverflow.com/questions/7944338/resume-last-activity-when-launcher-icon-is-clicked
* 1. Open app from launcher/appDrawer
* 2. Go home
* 3. Send notification and open
* 4. It creates a new Activity and Destroys the old
* 5. Causing an unnecessary app restart
* 6. This solution short-circuits the restart
*/
if (!isTaskRoot()) {
finish();
return;
}
}
}

View file

@ -1,7 +1,7 @@
package com.mattermost.rnbeta;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import android.content.Context;
import android.content.RestrictionsManager;
import android.os.Bundle;
@ -22,15 +22,15 @@ import com.wix.reactnativenotifications.RNNotificationsModule;
import io.tradle.react.LocalAuthModule;
import com.gantix.JailMonkey.JailMonkeyModule;
import com.RNFetchBlob.RNFetchBlob;
import io.sentry.RNSentryModule;
import io.sentry.RNSentryEventEmitter;
import com.masteratul.exceptionhandler.ReactNativeExceptionHandlerModule;
import com.inprogress.reactnativeyoutube.YouTubeStandaloneModule;
import com.reactlibrary.RNReactNativeDocViewerModule;
import com.reactnativedocumentpicker.DocumentPicker;
import com.philipphecht.RNDocViewerModule;
import io.github.elyx0.reactnativedocumentpicker.DocumentPickerModule;
import com.oblador.keychain.KeychainModule;
import com.reactnativecommunity.asyncstorage.AsyncStorageModule;
import com.reactnativecommunity.netinfo.NetInfoModule;
import com.reactnativecommunity.webview.RNCWebViewPackage;
import io.sentry.RNSentryModule;
import com.levelasquez.androidopensettings.AndroidOpenSettings;
import com.mkuczera.RNReactNativeHapticFeedbackModule;
@ -124,7 +124,7 @@ public class MainApplication extends NavigationApplication implements INotificat
case "RNLocalAuth":
return new LocalAuthModule(reactContext);
case "JailMonkey":
return new JailMonkeyModule(reactContext);
return new JailMonkeyModule(reactContext, false);
case "RNFetchBlob":
return new RNFetchBlob(reactContext);
case "MattermostManaged":
@ -133,20 +133,18 @@ public class MainApplication extends NavigationApplication implements INotificat
return NotificationPreferencesModule.getInstance(instance, reactContext);
case "RNTextInputReset":
return new RNTextInputResetModule(reactContext);
case "RNSentry":
return new RNSentryModule(reactContext);
case "RNSentryEventEmitter":
return new RNSentryEventEmitter(reactContext);
case "ReactNativeExceptionHandler":
return new ReactNativeExceptionHandlerModule(reactContext);
case "YouTubeStandaloneModule":
return new YouTubeStandaloneModule(reactContext);
case "RNReactNativeDocViewer":
return new RNReactNativeDocViewerModule(reactContext);
case "RNDocViewer":
return new RNDocViewerModule(reactContext);
case "RNDocumentPicker":
return new DocumentPicker(reactContext);
return new DocumentPickerModule(reactContext);
case "RNKeychainManager":
return new KeychainModule(reactContext);
case "RNSentry":
return new RNSentryModule(reactContext);
case AsyncStorageModule.NAME:
return new AsyncStorageModule(reactContext);
case NetInfoModule.NAME:
@ -179,13 +177,12 @@ public class MainApplication extends NavigationApplication implements INotificat
map.put("RNLocalAuth", new ReactModuleInfo("RNLocalAuth", "io.tradle.react.LocalAuthModule", false, false, false, false, false));
map.put("JailMonkey", new ReactModuleInfo("JailMonkey", "com.gantix.JailMonkey.JailMonkeyModule", false, false, true, false, false));
map.put("RNFetchBlob", new ReactModuleInfo("RNFetchBlob", "com.RNFetchBlob.RNFetchBlob", false, false, true, false, false));
map.put("RNSentry", new ReactModuleInfo("RNSentry", "com.sentry.RNSentryModule", false, false, true, false, false));
map.put("RNSentryEventEmitter", new ReactModuleInfo("RNSentryEventEmitter", "com.sentry.RNSentryEventEmitter", false, false, true, false, false));
map.put("ReactNativeExceptionHandler", new ReactModuleInfo("ReactNativeExceptionHandler", "com.masteratul.exceptionhandler.ReactNativeExceptionHandlerModule", false, false, false, false, false));
map.put("YouTubeStandaloneModule", new ReactModuleInfo("YouTubeStandaloneModule", "com.inprogress.reactnativeyoutube.YouTubeStandaloneModule", false, false, false, false, false));
map.put("RNReactNativeDocViewer", new ReactModuleInfo("RNReactNativeDocViewer", "com.reactlibrary.RNReactNativeDocViewerModule", false, false, false, false, false));
map.put("RNDocumentPicker", new ReactModuleInfo("RNDocumentPicker", "com.reactnativedocumentpicker.DocumentPicker", false, false, false, false, false));
map.put("RNDocViewer", new ReactModuleInfo("RNDocViewer", "com.philipphecht.RNDocViewerModule", false, false, false, false, false));
map.put("RNDocumentPicker", new ReactModuleInfo("RNDocumentPicker", "io.github.elyx0.reactnativedocumentpicker.DocumentPickerModule", false, false, false, false, false));
map.put("RNKeychainManager", new ReactModuleInfo("RNKeychainManager", "com.oblador.keychain.KeychainModule", false, false, true, false, false));
map.put("RNSentry", new ReactModuleInfo("RNSentry", "com.sentry.RNSentryModule", false, false, true, false, false));
map.put(AsyncStorageModule.NAME, new ReactModuleInfo(AsyncStorageModule.NAME, "com.reactnativecommunity.asyncstorage.AsyncStorageModule", false, false, false, false, false));
map.put(NetInfoModule.NAME, new ReactModuleInfo(NetInfoModule.NAME, "com.reactnativecommunity.netinfo.NetInfoModule", false, false, false, false, false));
map.put("RNAndroidOpenSettings", new ReactModuleInfo("RNAndroidOpenSettings", "com.levelasquez.androidopensettings.AndroidOpenSettings", false, false, false, false, false));

View file

@ -10,7 +10,7 @@ import android.content.Intent;
import android.content.res.Resources;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.Nullable;
import androidx.annotation.Nullable;
import android.util.Log;
import java.io.IOException;

View file

@ -1,8 +1,8 @@
package com.mattermost.rnbeta;
import android.support.v13.view.inputmethod.EditorInfoCompat;
import android.support.v13.view.inputmethod.InputConnectionCompat;
import android.support.v4.os.BuildCompat;
import androidx.core.view.inputmethod.EditorInfoCompat;
import androidx.core.view.inputmethod.InputConnectionCompat;
import androidx.core.os.BuildCompat;
import android.text.InputType;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;

View file

@ -1,7 +1,7 @@
package com.mattermost.rnbeta;
import android.content.Context;
import android.support.annotation.Nullable;
import androidx.annotation.Nullable;
import android.util.Log;
import java.lang.System;

View file

@ -77,7 +77,7 @@ public class ShareModule extends ReactContextBaseJavaModule {
this.clear();
Activity currentActivity = getCurrentActivity();
if (currentActivity != null) {
currentActivity.finish();
currentActivity.finishAndRemoveTask();
}
if (data != null && data.hasKey("url")) {

View file

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.percent.PercentRelativeLayout
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
@ -16,4 +16,4 @@
android:adjustViewBounds="true"
android:src="@drawable/splash" />
</android.support.percent.PercentRelativeLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -15,7 +15,7 @@ buildscript {
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.3.1'
classpath 'com.android.tools.build:gradle:3.4.2'
classpath 'com.google.gms:google-services:4.2.0'
// NOTE: Do not place your application dependencies here; they belong

View file

@ -20,3 +20,6 @@ org.gradle.jvmargs=-Xmx2048M
#android.enableAapt2=false
#android.useDeprecatedNdk=true
android.useAndroidX=true
android.enableJetifier=true

View file

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

View file

@ -1,8 +0,0 @@
keystore(
name = "debug",
properties = "debug.keystore.properties",
store = "debug.keystore",
visibility = [
"PUBLIC",
],
)

View file

@ -1,4 +0,0 @@
key.store=debug.keystore
key.alias=androiddebugkey
key.store.password=android
key.alias.password=android

View file

@ -1,4 +1,6 @@
rootProject.name = 'Mattermost'
include ':@sentry_react-native'
project(':@sentry_react-native').projectDir = new File(rootProject.projectDir, '../node_modules/@sentry/react-native/android')
include ':react-native-android-open-settings'
project(':react-native-android-open-settings').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-android-open-settings/android')
include ':react-native-haptic-feedback'
@ -15,8 +17,6 @@ include ':react-native-video'
project(':react-native-video').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-video/android')
include ':react-native-youtube'
project(':react-native-youtube').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-youtube/android')
include ':react-native-sentry'
project(':react-native-sentry').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-sentry/android')
include ':react-native-exception-handler'
project(':react-native-exception-handler').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-exception-handler/android')
include ':rn-fetch-blob'

View file

@ -15,7 +15,7 @@ import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
import {ViewTypes} from 'app/constants';
import {setAppCredentials} from 'app/init/credentials';
import PushNotifications from 'app/push_notifications';
import {getDeviceTimezone} from 'app/utils/timezone';
import {getDeviceTimezoneAsync} from 'app/utils/timezone';
import {setCSRFFromCookie} from 'app/utils/security';
export function handleLoginIdChanged(loginId) {
@ -51,7 +51,8 @@ export function handleSuccessfulLogin() {
const enableTimezone = isTimezoneEnabled(state);
if (enableTimezone) {
dispatch(autoUpdateTimezone(getDeviceTimezone()));
const timezone = await getDeviceTimezoneAsync();
dispatch(autoUpdateTimezone(timezone));
}
dispatch({

View file

@ -27,6 +27,7 @@ exports[`profile_picture_button should match snapshot 1`] = `
"dndIndicator": "#f74343",
"errorTextColor": "#fd5960",
"linkColor": "#2389d7",
"mentionBg": "#ffffff",
"mentionBj": "#ffffff",
"mentionColor": "#145dbf",
"mentionHighlightBg": "#ffe577",

View file

@ -14,7 +14,7 @@ import DeviceInfo from 'react-native-device-info';
import AndroidOpenSettings from 'react-native-android-open-settings';
import Icon from 'react-native-vector-icons/Ionicons';
import {DocumentPicker} from 'react-native-document-picker';
import DocumentPicker from 'react-native-document-picker';
import ImagePicker from 'react-native-image-picker';
import Permissions from 'react-native-permissions';
@ -257,13 +257,8 @@ export default class AttachmentButton extends PureComponent {
const hasPermission = await this.hasStoragePermission();
if (hasPermission) {
DocumentPicker.show({
filetype: [browseFileTypes],
}, async (error, res) => {
if (error) {
return;
}
try {
const res = await DocumentPicker.pick({type: [browseFileTypes]});
if (Platform.OS === 'android') {
// For android we need to retrieve the realPath in case the file being imported is from the cloud
const newUri = await ShareExtension.getFilePath(res.uri);
@ -278,7 +273,9 @@ export default class AttachmentButton extends PureComponent {
res.uri = decodeURIComponent(res.uri);
this.uploadFiles([res]);
});
} catch (error) {
// Do nothing
}
}
};

View file

@ -15,12 +15,6 @@ import AttachmentButton from './index';
jest.mock('react-intl');
jest.mock('Platform', () => {
const Platform = require.requireActual('Platform');
Platform.OS = 'ios';
return Platform;
});
describe('AttachmentButton', () => {
const formatMessage = jest.fn();
const baseProps = {

View file

@ -59,19 +59,19 @@ export default class Badge extends PureComponent {
this.mounted = false;
}
setBadgeRef = (ref) => {
this.badgeRef = ref;
};
handlePress = () => {
if (this.props.onPress) {
this.props.onPress();
}
};
setBadgeRef = (ref) => {
this.badgeContainerRef = ref;
};
setNativeProps = (props) => {
if (this.mounted && this.badgeContainerRef) {
this.badgeContainerRef.setNativeProps(props);
if (this.mounted && this.badgeRef) {
this.badgeRef.setNativeProps(props);
}
};

View file

@ -74,6 +74,7 @@ exports[`UserListRow should match snapshot 1`] = `
"dndIndicator": "#f74343",
"errorTextColor": "#fd5960",
"linkColor": "#2389d7",
"mentionBg": "#ffffff",
"mentionBj": "#ffffff",
"mentionColor": "#145dbf",
"mentionHighlightBg": "#ffe577",
@ -105,6 +106,7 @@ exports[`UserListRow should match snapshot 1`] = `
"dndIndicator": "#f74343",
"errorTextColor": "#fd5960",
"linkColor": "#2389d7",
"mentionBg": "#ffffff",
"mentionBj": "#ffffff",
"mentionColor": "#145dbf",
"mentionHighlightBg": "#ffe577",
@ -202,6 +204,7 @@ exports[`UserListRow should match snapshot for currentUser with (you) populated
"dndIndicator": "#f74343",
"errorTextColor": "#fd5960",
"linkColor": "#2389d7",
"mentionBg": "#ffffff",
"mentionBj": "#ffffff",
"mentionColor": "#145dbf",
"mentionHighlightBg": "#ffe577",
@ -233,6 +236,7 @@ exports[`UserListRow should match snapshot for currentUser with (you) populated
"dndIndicator": "#f74343",
"errorTextColor": "#fd5960",
"linkColor": "#2389d7",
"mentionBg": "#ffffff",
"mentionBj": "#ffffff",
"mentionColor": "#145dbf",
"mentionHighlightBg": "#ffe577",
@ -332,6 +336,7 @@ exports[`UserListRow should match snapshot for deactivated user 1`] = `
"dndIndicator": "#f74343",
"errorTextColor": "#fd5960",
"linkColor": "#2389d7",
"mentionBg": "#ffffff",
"mentionBj": "#ffffff",
"mentionColor": "#145dbf",
"mentionHighlightBg": "#ffe577",
@ -363,6 +368,7 @@ exports[`UserListRow should match snapshot for deactivated user 1`] = `
"dndIndicator": "#f74343",
"errorTextColor": "#fd5960",
"linkColor": "#2389d7",
"mentionBg": "#ffffff",
"mentionBj": "#ffffff",
"mentionColor": "#145dbf",
"mentionHighlightBg": "#ffe577",
@ -473,6 +479,7 @@ exports[`UserListRow should match snapshot for guest user 1`] = `
"dndIndicator": "#f74343",
"errorTextColor": "#fd5960",
"linkColor": "#2389d7",
"mentionBg": "#ffffff",
"mentionBj": "#ffffff",
"mentionColor": "#145dbf",
"mentionHighlightBg": "#ffe577",
@ -504,6 +511,7 @@ exports[`UserListRow should match snapshot for guest user 1`] = `
"dndIndicator": "#f74343",
"errorTextColor": "#fd5960",
"linkColor": "#2389d7",
"mentionBg": "#ffffff",
"mentionBj": "#ffffff",
"mentionColor": "#145dbf",
"mentionHighlightBg": "#ffe577",

View file

@ -27,7 +27,7 @@ export default class EmojiPicker extends EmojiPickerBase {
<React.Fragment>
<View style={styles.searchBar}>
<SearchBar
ref={this.searchBarRef}
ref={this.setSearchBarRef}
placeholder={formatMessage({id: 'search_bar.search', defaultMessage: 'Search'})}
cancelTitle={formatMessage({id: 'mobile.post.cancel', defaultMessage: 'Cancel'})}
backgroundColor='transparent'

View file

@ -49,7 +49,7 @@ export default class EmojiPicker extends EmojiPickerBase {
>
<View style={[styles.searchBar, padding(isLandscape)]}>
<SearchBar
ref={this.searchBarRef}
ref={this.setSearchBarRef}
placeholder={formatMessage({id: 'search_bar.search', defaultMessage: 'Search'})}
cancelTitle={formatMessage({id: 'mobile.post.cancel', defaultMessage: 'Cancel'})}
backgroundColor='transparent'
@ -71,7 +71,6 @@ export default class EmojiPicker extends EmojiPickerBase {
{this.renderListComponent(shorten)}
{!searchTerm &&
<KeyboardTrackingView
ref={this.keyboardTracker}
scrollViewNativeID={SCROLLVIEW_NATIVE_ID}
normalList={true}
>

View file

@ -80,7 +80,6 @@ export default class EmojiPicker extends PureComponent {
const emojis = this.renderableEmojis(props.emojisBySection, props.deviceWidth);
const emojiSectionIndexByOffset = this.measureEmojiSections(emojis);
this.searchBarRef = React.createRef();
this.scrollToSectionTries = 0;
this.state = {
emojis,
@ -97,8 +96,8 @@ export default class EmojiPicker extends PureComponent {
if (this.props.deviceWidth !== nextProps.deviceWidth) {
this.rebuildEmojis = true;
if (this.searchBarRef?.current) {
this.searchBarRef.current.blur();
if (this.searchBarRef) {
this.searchBarRef.blur();
}
}
@ -108,6 +107,14 @@ export default class EmojiPicker extends PureComponent {
}
}
setSearchBarRef = (ref) => {
this.searchBarRef = ref;
}
setSectionListRef = (ref) => {
this.sectionListRef = ref;
};
setRebuiltEmojis = (searchBarAnimationComplete = true) => {
if (this.rebuildEmojis && searchBarAnimationComplete) {
this.rebuildEmojis = false;
@ -253,6 +260,7 @@ export default class EmojiPicker extends PureComponent {
} else {
listComponent = (
<SectionList
ref={this.setSectionListRef}
getItemLayout={this.sectionListGetItemLayout}
initialNumToRender={50}
keyboardShouldPersistTaps='always'
@ -265,7 +273,6 @@ export default class EmojiPicker extends PureComponent {
onScroll={this.onScroll}
onScrollToIndexFailed={this.handleScrollToSectionFailed}
pageSize={50}
ref={this.attachSectionList}
removeClippedSubviews={false}
renderItem={this.renderItem}
renderSectionHeader={this.renderSectionHeader}
@ -356,7 +363,7 @@ export default class EmojiPicker extends PureComponent {
jumpToSection: true,
currentSectionIndex: index,
}, () => {
this.sectionList.scrollToLocation({
this.sectionListRef.scrollToLocation({
sectionIndex: index,
itemIndex: 0,
viewOffset: 25,
@ -423,10 +430,6 @@ export default class EmojiPicker extends PureComponent {
});
};
attachSectionList = (c) => {
this.sectionList = c;
};
renderFooter = () => {
if (!this.state.missingPages) {
return null;

View file

@ -42,6 +42,7 @@ exports[`FileAttachment should match snapshot 1`] = `
"dndIndicator": "#f74343",
"errorTextColor": "#fd5960",
"linkColor": "#2389d7",
"mentionBg": "#ffffff",
"mentionBj": "#ffffff",
"mentionColor": "#145dbf",
"mentionHighlightBg": "#ffe577",

View file

@ -48,6 +48,7 @@ exports[`PostAttachmentOpenGraph should match snapshot with a single image file
"dndIndicator": "#f74343",
"errorTextColor": "#fd5960",
"linkColor": "#2389d7",
"mentionBg": "#ffffff",
"mentionBj": "#ffffff",
"mentionColor": "#145dbf",
"mentionHighlightBg": "#ffe577",

View file

@ -69,7 +69,7 @@ export default class FileAttachmentDocument extends PureComponent {
componentDidMount() {
this.mounted = true;
this.eventEmitter = new NativeEventEmitter(NativeModules.RNReactNativeDocViewer);
this.eventEmitter = new NativeEventEmitter(NativeModules.RNDocViewer);
this.eventEmitter.addListener('DoneButtonEvent', this.onDonePreviewingFile);
}

View file

@ -5,22 +5,19 @@ import {PropTypes} from 'prop-types';
import React from 'react';
import {intlShape} from 'react-intl';
import {
Platform,
Clipboard,
StyleSheet,
Text,
View,
} from 'react-native';
import SyntaxHighlighter from 'react-native-syntax-highlighter';
import CustomPropTypes from 'app/constants/custom_prop_types';
import FormattedText from 'app/components/formatted_text';
import TouchableWithFeedback from 'app/components/touchable_with_feedback';
import BottomSheet from 'app/utils/bottom_sheet';
import {getDisplayNameForLanguage, getCodeFont} from 'app/utils/markdown';
import {getDisplayNameForLanguage} from 'app/utils/markdown';
import {preventDoubleTap} from 'app/utils/tap';
import {changeOpacity, makeStyleSheetFromTheme, getHighlightStyleFromTheme} from 'app/utils/theme';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
import mattermostManaged from 'app/mattermost_managed';
import {goToScreen} from 'app/actions/navigation';
@ -43,13 +40,11 @@ export default class MarkdownCodeBlock extends React.PureComponent {
};
handlePress = preventDoubleTap(() => {
const {language, content, textStyle} = this.props;
const {language, content} = this.props;
const {intl} = this.context;
const screen = 'Code';
const passProps = {
content,
language,
textStyle,
};
const languageDisplayName = getDisplayNameForLanguage(language);
@ -155,26 +150,6 @@ export default class MarkdownCodeBlock extends React.PureComponent {
);
}
let textComponent = null;
if (Platform.OS === 'ios') {
textComponent = (
<SyntaxHighlighter
language={this.props.language}
style={getHighlightStyleFromTheme(this.props.theme)}
highlighter={'hljs'}
customStyle={{...style.codeText, ...this.props.textStyle}}
>
{content}
</SyntaxHighlighter>
);
} else {
textComponent = (
<Text style={[style.codeText, this.props.textStyle]}>
{content}
</Text>
);
}
return (
<TouchableWithFeedback
onPress={this.handlePress}
@ -189,7 +164,9 @@ export default class MarkdownCodeBlock extends React.PureComponent {
</View>
<View style={style.rightColumn}>
<View style={style.code}>
{textComponent}
<Text style={[style.codeText, this.props.textStyle]}>
{content}
</Text>
</View>
{plusMoreLines}
</View>
@ -227,15 +204,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
flexDirection: 'column',
flex: 1,
paddingHorizontal: 6,
...Platform.select({
ios: {
paddingVertical: 0,
backgroundColor: getHighlightStyleFromTheme(theme).hljs.background,
},
android: {
paddingVertical: 4,
},
}),
paddingVertical: 4,
},
code: {
flexDirection: 'row',
@ -245,12 +214,6 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
color: changeOpacity(theme.centerChannelColor, 0.65),
fontSize: 12,
lineHeight: 18,
...Platform.select({
ios: {
fontFamily: getCodeFont(),
paddingVertical: 6,
},
}),
},
plusMoreLinesText: {
color: changeOpacity(theme.centerChannelColor, 0.4),

View file

@ -92,6 +92,14 @@ export default class MarkdownImage extends React.Component {
this.mounted = false;
}
setImageRef = (ref) => {
this.imageRef = ref;
}
setItemRef = (ref) => {
this.itemRef = ref;
}
getSource = () => {
let source = this.props.source;
@ -195,7 +203,7 @@ export default class MarkdownImage extends React.Component {
},
}];
previewImageAtIndex([this.refs.item], 0, files);
previewImageAtIndex([this.itemRef], 0, files);
};
loadImageSize = (source) => {
@ -252,7 +260,7 @@ export default class MarkdownImage extends React.Component {
style={{width, height}}
>
<ProgressiveImage
ref='image'
ref={this.setImageRef}
defaultSource={source}
resizeMode='contain'
style={{width, height}}
@ -282,7 +290,7 @@ export default class MarkdownImage extends React.Component {
return (
<View
ref='item'
ref={this.setItemRef}
style={style.container}
>
{image}

View file

@ -52,6 +52,14 @@ export default class AttachmentImage extends PureComponent {
}
}
setImageRef = (ref) => {
this.imageRef = ref;
}
setItemRef = (ref) => {
this.itemRef = ref;
}
handlePreviewImage = () => {
const {imageUrl} = this.props;
const {
@ -79,7 +87,7 @@ export default class AttachmentImage extends PureComponent {
localPath: uri,
},
}];
previewImageAtIndex([this.refs.item], 0, files);
previewImageAtIndex([this.itemRef], 0, files);
};
setImageDimensions = (imageUri, dimensions, originalWidth, originalHeight) => {
@ -132,7 +140,7 @@ export default class AttachmentImage extends PureComponent {
if (imageUri) {
progressiveImage = (
<ProgressiveImage
ref='image'
ref={this.setImageRef}
style={{height, width}}
imageUri={imageUri}
resizeMode='contain'
@ -149,7 +157,7 @@ export default class AttachmentImage extends PureComponent {
type={'none'}
>
<View
ref='item'
ref={this.setItemRef}
style={[style.imageContainer, {width, height}]}
>
{progressiveImage}

View file

@ -4,10 +4,14 @@
/* eslint-disable no-underscore-dangle */
import React from 'react';
import {TextInput, Text, TouchableWithoutFeedback} from 'react-native';
import UIManager from 'UIManager';
import {
TextInput,
Text,
TouchableWithoutFeedback,
UIManager,
requireNativeComponent,
} from 'react-native';
import invariant from 'invariant';
import requireNativeComponent from 'requireNativeComponent';
const AndroidTextInput = requireNativeComponent('PasteableTextInputAndroid');

View file

@ -64,6 +64,10 @@ export default class PostAttachmentOpenGraph extends PureComponent {
this.mounted = false;
}
setItemRef = (ref) => {
this.itemRef = ref;
}
fetchData = (url, openGraphData) => {
if (!openGraphData) {
this.props.actions.getOpenGraphMetadata(url);
@ -205,7 +209,7 @@ export default class PostAttachmentOpenGraph extends PureComponent {
},
}];
previewImageAtIndex([this.refs.item], 0, files);
previewImageAtIndex([this.itemRef], 0, files);
};
renderDescription = () => {
@ -247,7 +251,7 @@ export default class PostAttachmentOpenGraph extends PureComponent {
return (
<View
ref='item'
ref={this.setItemRef}
style={[style.imageContainer, {width, height}]}
>
<TouchableWithFeedback

View file

@ -0,0 +1,37 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renderSystemMessage uses renderer for Channel Display Name update 1`] = `
<Connect(Markdown)
baseTextStyle={Object {}}
onPostPress={[MockFunction]}
textStyles={Object {}}
value="{username} updated the channel display name from: {oldDisplayName} to: {newDisplayName}"
/>
`;
exports[`renderSystemMessage uses renderer for Channel Header update 1`] = `
<Connect(Markdown)
baseTextStyle={Object {}}
onPostPress={[MockFunction]}
textStyles={Object {}}
value="{username} updated the channel header from: {oldHeader} to: {newHeader}"
/>
`;
exports[`renderSystemMessage uses renderer for Channel Purpose update 1`] = `
<Connect(Markdown)
baseTextStyle={Object {}}
onPostPress={[MockFunction]}
textStyles={Object {}}
value="{username} updated the channel purpose from: {oldPurpose} to: {newPurpose}"
/>
`;
exports[`renderSystemMessage uses renderer for archived channel 1`] = `
<Connect(Markdown)
baseTextStyle={Object {}}
onPostPress={[MockFunction]}
textStyles={Object {}}
value="{username} archived the channel"
/>
`;

View file

@ -13,6 +13,7 @@ import Icon from 'react-native-vector-icons/Ionicons';
import {Posts} from 'mattermost-redux/constants';
import CombinedSystemMessage from 'app/components/combined_system_message';
import {renderSystemMessage} from './system_message_helpers';
import FormattedText from 'app/components/formatted_text';
import Markdown from 'app/components/markdown';
import MarkdownEmoji from 'app/components/markdown/markdown_emoji';
@ -348,6 +349,13 @@ export default class PostBody extends PureComponent {
const messageStyle = isSystemMessage ? [style.message, style.systemMessage] : style.message;
const isPendingOrFailedPost = isPending || isFailed;
const messageStyles = {messageStyle, textStyles};
const intl = this.context.intl;
const systemMessage = renderSystemMessage(this.props, messageStyles, intl);
if (systemMessage) {
return systemMessage;
}
let body;
let messageComponent;
if (hasBeenDeleted) {

View file

@ -9,6 +9,9 @@ import PostBodyAdditionalContent from 'app/components/post_body_additional_conte
import {shallowWithIntl} from 'test/intl-test-helper';
import PostBody from './post_body.js';
import * as SystemMessageHelpers from './system_message_helpers';
jest.mock('./system_message_helpers');
describe('PostBody', () => {
const baseProps = {
@ -122,4 +125,9 @@ describe('PostBody', () => {
instance.measurePost(event);
expect(wrapper.state('isLongPost')).toEqual(false);
});
test('should return system message as post body', () => {
shallowWithIntl(<PostBody {...baseProps}/>);
expect(SystemMessageHelpers.renderSystemMessage.mock.calls.length).toBe(1);
});
});

View file

@ -0,0 +1,160 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {Posts} from 'mattermost-redux/constants';
import Markdown from 'app/components/markdown';
import {t} from 'app/utils/i18n';
const renderUsername = (value) => {
return (value[0] === '@') ? value : `@${value}`;
};
const renderMessage = (postBodyProps, styles, intl, localeHolder, values) => {
const {onPress} = postBodyProps;
const {messageStyle, textStyles} = styles;
return (
<Markdown
baseTextStyle={messageStyle}
onPostPress={onPress}
textStyles={textStyles}
value={intl.formatMessage(localeHolder, values)}
/>
);
};
const renderHeaderChangeMessage = (postBodyProps, styles, intl) => {
const {postProps} = postBodyProps;
let values;
if (!postProps.username) {
return null;
}
const username = renderUsername(postProps.username);
const oldHeader = postProps.old_header;
const newHeader = postProps.new_header;
let localeHolder;
if (postProps.new_header) {
if (postProps.old_header) {
localeHolder = {
id: t('mobile.system_message.update_channel_header_message_and_forget.updated_from'),
defaultMessage: '{username} updated the channel header from: {oldHeader} to: {newHeader}',
};
values = {username, oldHeader, newHeader};
return renderMessage(postBodyProps, styles, intl, localeHolder, values);
}
localeHolder = {
id: t('mobile.system_message.update_channel_header_message_and_forget.updated_to'),
defaultMessage: '{username} updated the channel header to: {newHeader}',
};
values = {username, oldHeader, newHeader};
return renderMessage(postBodyProps, styles, intl, localeHolder, values);
} else if (postProps.old_header) {
localeHolder = {
id: t('mobile.system_message.update_channel_header_message_and_forget.removed'),
defaultMessage: '{username} removed the channel header (was: {oldHeader})',
};
values = {username, oldHeader, newHeader};
return renderMessage(postBodyProps, styles, intl, localeHolder, values);
}
return null;
};
const renderPurposeChangeMessage = (postBodyProps, styles, intl) => {
const {postProps} = postBodyProps;
let values;
if (!postProps.username) {
return null;
}
const username = renderUsername(postProps.username);
const oldPurpose = postProps.old_purpose;
const newPurpose = postProps.new_purpose;
let localeHolder;
if (postProps.new_purpose) {
if (postProps.old_purpose) {
localeHolder = {
id: t('mobile.system_message.update_channel_purpose_message.updated_from'),
defaultMessage: '{username} updated the channel purpose from: {oldPurpose} to: {newPurpose}',
};
values = {username, oldPurpose, newPurpose};
return renderMessage(postBodyProps, styles, intl, localeHolder, values);
}
localeHolder = {
id: t('mobile.system_message.update_channel_purpose_message.updated_to'),
defaultMessage: '{username} updated the channel purpose to: {newPurpose}',
};
values = {username, oldPurpose, newPurpose};
return renderMessage(postBodyProps, styles, intl, localeHolder, values);
} else if (postProps.old_purpose) {
localeHolder = {
id: t('mobile.system_message.update_channel_purpose_message.removed'),
defaultMessage: '{username} removed the channel purpose (was: {oldPurpose})',
};
values = {username, oldPurpose, newPurpose};
return renderMessage(postBodyProps, styles, intl, localeHolder, values);
}
return null;
};
const renderDisplayNameChangeMessage = (postBodyProps, styles, intl) => {
const {postProps} = postBodyProps;
const oldDisplayName = postProps.old_displayname;
const newDisplayName = postProps.new_displayname;
if (!(postProps.username && postProps.old_displayname && postProps.new_displayname)) {
return null;
}
const username = renderUsername(postProps.username);
const localeHolder = {
id: t('mobile.system_message.update_channel_displayname_message_and_forget.updated_from'),
defaultMessage: '{username} updated the channel display name from: {oldDisplayName} to: {newDisplayName}',
};
const values = {username, oldDisplayName, newDisplayName};
return renderMessage(postBodyProps, styles, intl, localeHolder, values);
};
const renderArchivedMessage = (postBodyProps, styles, intl) => {
const {postProps} = postBodyProps;
const username = renderUsername(postProps.username);
const localeHolder = {
id: t('mobile.system_message.channel_archived_message'),
defaultMessage: '{username} archived the channel',
};
const values = {username};
return renderMessage(postBodyProps, styles, intl, localeHolder, values);
};
const systemMessageRenderers = {
[Posts.POST_TYPES.HEADER_CHANGE]: renderHeaderChangeMessage,
[Posts.POST_TYPES.DISPLAYNAME_CHANGE]: renderDisplayNameChangeMessage,
[Posts.POST_TYPES.PURPOSE_CHANGE]: renderPurposeChangeMessage,
[Posts.POST_TYPES.CHANNEL_DELETED]: renderArchivedMessage,
};
export const renderSystemMessage = (postBodyProps, styles, intl) => {
const renderer = systemMessageRenderers[postBodyProps.postType];
if (!renderer) {
return null;
}
return renderer(postBodyProps, styles, intl);
};

View file

@ -0,0 +1,90 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import * as SystemMessageHelpers from './system_message_helpers';
import {Posts} from 'mattermost-redux/constants';
const basePostBodyProps = {
postProps: {
username: 'username',
},
onPress: jest.fn(),
};
const mockStyles = {
messageStyle: {},
textStyles: {},
};
const mockIntl = {
formatMessage: ({defaultMessage}) => defaultMessage,
};
describe('renderSystemMessage', () => {
test('uses renderer for Channel Header update', () => {
const postBodyProps = {
...basePostBodyProps,
postProps: {
...basePostBodyProps.postProps,
old_header: 'old header',
new_header: 'new header',
},
postType: Posts.POST_TYPES.HEADER_CHANGE,
};
const renderedMessage = SystemMessageHelpers.renderSystemMessage(postBodyProps, mockStyles, mockIntl);
expect(renderedMessage).toMatchSnapshot();
});
test('uses renderer for Channel Display Name update', () => {
const postBodyProps = {
...basePostBodyProps,
postProps: {
...basePostBodyProps.postProps,
old_displayname: 'old displayname',
new_displayname: 'new displayname',
},
postType: Posts.POST_TYPES.DISPLAYNAME_CHANGE,
};
const renderedMessage = SystemMessageHelpers.renderSystemMessage(postBodyProps, mockStyles, mockIntl);
expect(renderedMessage).toMatchSnapshot();
});
test('uses renderer for Channel Purpose update', () => {
const postBodyProps = {
...basePostBodyProps,
postProps: {
...basePostBodyProps.postProps,
old_purpose: 'old purpose',
new_purpose: 'new purpose',
},
postType: Posts.POST_TYPES.PURPOSE_CHANGE,
};
const renderedMessage = SystemMessageHelpers.renderSystemMessage(postBodyProps, mockStyles, mockIntl);
expect(renderedMessage).toMatchSnapshot();
});
test('uses renderer for archived channel', () => {
const postBodyProps = {
...basePostBodyProps,
postProps: {
...basePostBodyProps.postProps,
},
postType: Posts.POST_TYPES.CHANNEL_DELETED,
};
const renderedMessage = SystemMessageHelpers.renderSystemMessage(postBodyProps, mockStyles, mockIntl);
expect(renderedMessage).toMatchSnapshot();
});
test('is null for non-qualifying system messages', () => {
const postBodyProps = {
...basePostBodyProps,
postType: 'not_relevant',
};
const renderedMessage = SystemMessageHelpers.renderSystemMessage(postBodyProps, mockStyles, mockIntl);
expect(renderedMessage).toBeNull();
});
});

View file

@ -0,0 +1,509 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`PostHeader should match snapshot when just a base post 1`] = `
<React.Fragment>
<View
style={
Array [
Object {
"flex": 1,
"marginTop": 10,
},
false,
]
}
>
<View
style={
Object {
"flex": 1,
"flexDirection": "row",
}
}
>
<TouchableWithFeedbackIOS
onPress={[Function]}
style={
Array [
Object {
"marginBottom": 3,
"marginRight": 5,
"maxWidth": "60%",
},
null,
]
}
type="opacity"
>
<Text
ellipsizeMode="tail"
numberOfLines={1}
style={
Object {
"color": "#3d3c40",
"flexGrow": 1,
"fontSize": 15,
"fontWeight": "600",
"paddingVertical": 2,
}
}
>
John Smith
</Text>
</TouchableWithFeedbackIOS>
<FormattedTime
hour12={true}
style={
Object {
"color": "#3d3c40",
"flex": 1,
"fontSize": 12,
"marginTop": 5,
"opacity": 0.5,
}
}
timeZone=""
value={0}
/>
</View>
</View>
</React.Fragment>
`;
exports[`PostHeader should match snapshot when post is autoresponder 1`] = `
<React.Fragment>
<View
style={
Array [
Object {
"flex": 1,
"marginTop": 10,
},
false,
]
}
>
<View
style={
Object {
"flex": 1,
"flexDirection": "row",
}
}
>
<View
style={
Array [
Object {
"marginBottom": 3,
"marginRight": 5,
"maxWidth": "60%",
},
null,
]
}
>
<Text
ellipsizeMode="tail"
numberOfLines={1}
style={
Object {
"color": "#3d3c40",
"flexGrow": 1,
"fontSize": 15,
"fontWeight": "600",
"paddingVertical": 2,
}
}
>
John Smith
</Text>
</View>
<Tag
defaultMessage="AUTOMATIC REPLY"
id="post_info.auto_responder"
show={true}
style={
Object {
"marginBottom": 5,
"marginLeft": 0,
"marginRight": 5,
}
}
theme={
Object {
"awayIndicator": "#ffbc42",
"buttonBg": "#166de0",
"buttonColor": "#ffffff",
"centerChannelBg": "#ffffff",
"centerChannelColor": "#3d3c40",
"codeTheme": "github",
"dndIndicator": "#f74343",
"errorTextColor": "#fd5960",
"linkColor": "#2389d7",
"mentionBg": "#ffffff",
"mentionBj": "#ffffff",
"mentionColor": "#145dbf",
"mentionHighlightBg": "#ffe577",
"mentionHighlightLink": "#166de0",
"newMessageSeparator": "#ff8800",
"onlineIndicator": "#06d6a0",
"sidebarBg": "#145dbf",
"sidebarHeaderBg": "#1153ab",
"sidebarHeaderTextColor": "#ffffff",
"sidebarText": "#ffffff",
"sidebarTextActiveBorder": "#579eff",
"sidebarTextActiveColor": "#ffffff",
"sidebarTextHoverBg": "#4578bf",
"sidebarUnreadText": "#ffffff",
"type": "Mattermost",
}
}
/>
<FormattedTime
hour12={true}
style={
Object {
"color": "#3d3c40",
"flex": 1,
"fontSize": 12,
"marginTop": 5,
"opacity": 0.5,
}
}
timeZone=""
value={0}
/>
</View>
</View>
</React.Fragment>
`;
exports[`PostHeader should match snapshot when post is from system message 1`] = `
<React.Fragment>
<View
style={
Array [
Object {
"flex": 1,
"marginTop": 10,
},
false,
]
}
>
<View
style={
Object {
"flex": 1,
"flexDirection": "row",
}
}
>
<View
style={
Object {
"marginBottom": 3,
"marginRight": 5,
"maxWidth": "60%",
}
}
>
<FormattedText
defaultMessage="System"
id="post_info.system"
style={
Array [
Object {
"color": "#3d3c40",
"flexGrow": 1,
"fontSize": 15,
"fontWeight": "600",
"paddingVertical": 2,
},
]
}
/>
</View>
<FormattedTime
hour12={true}
style={
Object {
"color": "#3d3c40",
"flex": 1,
"fontSize": 12,
"marginTop": 5,
"opacity": 0.5,
}
}
timeZone=""
value={0}
/>
</View>
</View>
</React.Fragment>
`;
exports[`PostHeader should match snapshot when post isBot and shouldRenderReplyButton 1`] = `
<React.Fragment>
<View
style={
Array [
Object {
"flex": 1,
"marginTop": 10,
},
false,
]
}
>
<View
style={
Object {
"flex": 1,
"flexDirection": "row",
}
}
>
<TouchableWithFeedbackIOS
onPress={[Function]}
style={
Array [
Object {
"marginBottom": 3,
"marginRight": 5,
"maxWidth": "60%",
},
Object {
"maxWidth": "50%",
},
]
}
type="opacity"
>
<Text
ellipsizeMode="tail"
numberOfLines={1}
style={
Object {
"color": "#3d3c40",
"flexGrow": 1,
"fontSize": 15,
"fontWeight": "600",
"paddingVertical": 2,
}
}
>
John Smith
</Text>
</TouchableWithFeedbackIOS>
<BotTag
style={
Object {
"marginBottom": 5,
"marginLeft": 0,
"marginRight": 5,
}
}
theme={
Object {
"awayIndicator": "#ffbc42",
"buttonBg": "#166de0",
"buttonColor": "#ffffff",
"centerChannelBg": "#ffffff",
"centerChannelColor": "#3d3c40",
"codeTheme": "github",
"dndIndicator": "#f74343",
"errorTextColor": "#fd5960",
"linkColor": "#2389d7",
"mentionBg": "#ffffff",
"mentionBj": "#ffffff",
"mentionColor": "#145dbf",
"mentionHighlightBg": "#ffe577",
"mentionHighlightLink": "#166de0",
"newMessageSeparator": "#ff8800",
"onlineIndicator": "#06d6a0",
"sidebarBg": "#145dbf",
"sidebarHeaderBg": "#1153ab",
"sidebarHeaderTextColor": "#ffffff",
"sidebarText": "#ffffff",
"sidebarTextActiveBorder": "#579eff",
"sidebarTextActiveColor": "#ffffff",
"sidebarTextHoverBg": "#4578bf",
"sidebarUnreadText": "#ffffff",
"type": "Mattermost",
}
}
/>
<FormattedTime
hour12={true}
style={
Object {
"color": "#3d3c40",
"flex": 1,
"fontSize": 12,
"marginTop": 5,
"opacity": 0.5,
}
}
timeZone=""
value={0}
/>
<View
style={
Object {
"flex": 1,
"justifyContent": "flex-end",
}
}
>
<TouchableWithFeedbackIOS
onPress={[MockFunction]}
style={
Object {
"alignItems": "flex-start",
"flex": 1,
"flexDirection": "row",
"justifyContent": "flex-end",
"minWidth": 40,
"paddingBottom": 10,
"paddingTop": 2,
}
}
type="opacity"
>
<ReplyIcon
color="#2389d7"
height={16}
width={16}
/>
<Text
style={
Object {
"color": "#2389d7",
"fontSize": 12,
"marginLeft": 2,
"marginTop": 2,
}
}
>
0
</Text>
</TouchableWithFeedbackIOS>
</View>
</View>
</View>
</React.Fragment>
`;
exports[`PostHeader should match snapshot when post should display reply button 1`] = `
<React.Fragment>
<View
style={
Array [
Object {
"flex": 1,
"marginTop": 10,
},
false,
]
}
>
<View
style={
Object {
"flex": 1,
"flexDirection": "row",
}
}
>
<TouchableWithFeedbackIOS
onPress={[Function]}
style={
Array [
Object {
"marginBottom": 3,
"marginRight": 5,
"maxWidth": "60%",
},
null,
]
}
type="opacity"
>
<Text
ellipsizeMode="tail"
numberOfLines={1}
style={
Object {
"color": "#3d3c40",
"flexGrow": 1,
"fontSize": 15,
"fontWeight": "600",
"paddingVertical": 2,
}
}
>
John Smith
</Text>
</TouchableWithFeedbackIOS>
<FormattedTime
hour12={true}
style={
Object {
"color": "#3d3c40",
"flex": 1,
"fontSize": 12,
"marginTop": 5,
"opacity": 0.5,
}
}
timeZone=""
value={0}
/>
<View
style={
Object {
"flex": 1,
"justifyContent": "flex-end",
}
}
>
<TouchableWithFeedbackIOS
onPress={[MockFunction]}
style={
Object {
"alignItems": "flex-start",
"flex": 1,
"flexDirection": "row",
"justifyContent": "flex-end",
"minWidth": 40,
"paddingBottom": 10,
"paddingTop": 2,
}
}
type="opacity"
>
<ReplyIcon
color="#2389d7"
height={16}
width={16}
/>
<Text
style={
Object {
"color": "#2389d7",
"fontSize": 12,
"marginLeft": 2,
"marginTop": 2,
}
}
>
0
</Text>
</TouchableWithFeedbackIOS>
</View>
</View>
</View>
</React.Fragment>
`;

View file

@ -117,9 +117,16 @@ export default class PostHeader extends PureComponent {
fromAutoResponder,
overrideUsername,
theme,
renderReplies,
shouldRenderReplyButton,
commentedOnDisplayName,
commentCount,
isBot,
} = this.props;
const style = getStyleSheet(theme);
const showReply = shouldRenderReplyButton || (!commentedOnDisplayName && commentCount > 0 && renderReplies);
const displayNameStyle = [style.displayNameContainer, showReply && (isBot || fromAutoResponder || fromWebHook) ? style.displayNameContainerBotReplyWidth : null];
if (fromAutoResponder || fromWebHook) {
let name = displayName;
@ -128,7 +135,7 @@ export default class PostHeader extends PureComponent {
}
return (
<View style={[style.displayNameContainer, {maxWidth: fromAutoResponder ? '30%' : '60%'}]}>
<View style={displayNameStyle}>
<Text
style={style.displayName}
ellipsizeMode={'tail'}
@ -152,7 +159,7 @@ export default class PostHeader extends PureComponent {
return (
<TouchableWithFeedback
onPress={this.handleUsernamePress}
style={style.displayNameContainer}
style={displayNameStyle}
type={'opacity'}
>
<Text
@ -357,5 +364,8 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
marginBottom: 3,
lineHeight: 21,
},
displayNameContainerBotReplyWidth: {
maxWidth: '50%',
},
};
});

View file

@ -0,0 +1,98 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {shallow} from 'enzyme';
import Preferences from 'mattermost-redux/constants/preferences';
import PostHeader from './post_header';
describe('PostHeader', () => {
const baseProps = {
commentCount: 0,
commentedOnDisplayName: '',
createAt: 0,
displayName: 'John Smith',
enablePostUsernameOverride: false,
fromWebHook: false,
isPendingOrFailedPost: false,
isSearchResult: false,
isSystemMessage: false,
fromAutoResponder: false,
militaryTime: false,
overrideUsername: '',
renderReplies: false,
shouldRenderReplyButton: false,
showFullDate: false,
theme: Preferences.THEMES.default,
username: 'JohnSmith',
isBot: false,
isGuest: false,
userTimezone: '',
enableTimezone: false,
previousPostExists: false,
post: {id: 'post'},
beforePrevPostUserId: '0',
onPress: jest.fn(),
};
test('should match snapshot when just a base post', () => {
const wrapper = shallow(
<PostHeader {...baseProps}/>
);
expect(wrapper.getElement()).toMatchSnapshot();
expect(wrapper.find('#ReplyIcon').exists()).toEqual(false);
});
test('should match snapshot when post isBot and shouldRenderReplyButton', () => {
const props = {
...baseProps,
shouldRenderReplyButton: true,
isBot: true,
};
const wrapper = shallow(
<PostHeader {...props}/>
);
expect(wrapper.getElement()).toMatchSnapshot();
});
test('should match snapshot when post should display reply button', () => {
const props = {
...baseProps,
shouldRenderReplyButton: true,
};
const wrapper = shallow(
<PostHeader {...props}/>
);
expect(wrapper.getElement()).toMatchSnapshot();
});
test('should match snapshot when post is autoresponder', () => {
const props = {
...baseProps,
fromAutoResponder: true,
};
const wrapper = shallow(
<PostHeader {...props}/>
);
expect(wrapper.getElement()).toMatchSnapshot();
expect(wrapper.find('#ReplyIcon').exists()).toEqual(false);
});
test('should match snapshot when post is from system message', () => {
const props = {
...baseProps,
isSystemMessage: true,
};
const wrapper = shallow(
<PostHeader {...props}/>
);
expect(wrapper.getElement()).toMatchSnapshot();
expect(wrapper.find('#ReplyIcon').exists()).toEqual(false);
});
});

View file

@ -44,6 +44,7 @@ exports[`PostTextBox should match, full snapshot 1`] = `
"dndIndicator": "#f74343",
"errorTextColor": "#fd5960",
"linkColor": "#2389d7",
"mentionBg": "#ffffff",
"mentionBj": "#ffffff",
"mentionColor": "#145dbf",
"mentionHighlightBg": "#ffe577",
@ -122,6 +123,7 @@ exports[`PostTextBox should match, full snapshot 1`] = `
"dndIndicator": "#f74343",
"errorTextColor": "#fd5960",
"linkColor": "#2389d7",
"mentionBg": "#ffffff",
"mentionBj": "#ffffff",
"mentionColor": "#145dbf",
"mentionHighlightBg": "#ffe577",

View file

@ -16,14 +16,6 @@ import EphemeralStore from 'app/store/ephemeral_store';
import PostTextbox from './post_textbox.ios';
jest.mock('NativeEventEmitter');
jest.mock('Alert', () => {
return {
alert: jest.fn(),
};
});
describe('PostTextBox', () => {
const baseProps = {
actions: {

View file

@ -409,8 +409,8 @@ export default class PostTextBoxBase extends PureComponent {
}
};
handleUploadFiles = (images) => {
this.props.actions.initUploadFiles(images, this.props.rootId);
handleUploadFiles = (files) => {
this.props.actions.initUploadFiles(files, this.props.rootId);
};
isFileLoading = () => {
@ -685,21 +685,21 @@ export default class PostTextBoxBase extends PureComponent {
});
};
showPasteImageErrorDialog = () => {
showPasteFilesErrorDialog = () => {
const {formatMessage} = this.context.intl;
Alert.alert(
formatMessage({
id: 'mobile.image_paste.error_title',
defaultMessage: 'Paste Image failed',
id: 'mobile.files_paste.error_title',
defaultMessage: 'Paste failed',
}),
formatMessage({
id: 'mobile.image_paste.error_description',
defaultMessage: 'An error occurred while pasting the image. Please try again.',
id: 'mobile.files_paste.error_description',
defaultMessage: 'An error occurred while pasting the file(s). Please try again.',
}),
[
{
text: formatMessage({
id: 'mobile.image_paste.error_dismiss',
id: 'mobile.files_paste.error_dismiss',
defaultMessage: 'Dismiss',
}),
},
@ -707,27 +707,27 @@ export default class PostTextBoxBase extends PureComponent {
);
};
handlePasteImages = (error, images) => {
handlePasteFiles = (error, files) => {
if (this.props.screenId === EphemeralStore.getNavigationTopComponentId()) {
if (error) {
this.showPasteImageErrorDialog();
this.showPasteFilesErrorDialog();
return;
}
const {maxFileSize, files} = this.props;
const availableCount = MAX_FILE_COUNT - files.length;
if (images.length > availableCount) {
const {maxFileSize} = this.props;
const availableCount = MAX_FILE_COUNT - this.props.files.length;
if (files.length > availableCount) {
this.onShowFileMaxWarning();
return;
}
const largeImage = images.find((image) => image.fileSize > maxFileSize);
if (largeImage) {
this.onShowFileSizeWarning(largeImage.fileName);
const largeFile = files.find((image) => image.fileSize > maxFileSize);
if (largeFile) {
this.onShowFileSizeWarning(largeFile.fileName);
return;
}
this.handleUploadFiles(images);
this.handleUploadFiles(files);
}
};
@ -780,7 +780,7 @@ export default class PostTextBoxBase extends PureComponent {
onEndEditing={this.handleEndEditing}
disableFullscreenUI={true}
editable={!channelIsReadOnly}
onPaste={this.handlePasteImages}
onPaste={this.handlePasteFiles}
keyboardAppearance={getKeyboardAppearanceFromTheme(theme)}
/>
<Fade visible={this.isSendButtonVisible()}>

View file

@ -70,7 +70,6 @@ export default class RadioButtonGroup extends PureComponent {
return (
<RadioButton
{...other}
ref={value}
name={name}
key={`${name}-${value}`}
value={value}

View file

@ -44,8 +44,12 @@ export default class Root extends PureComponent {
}
}
setProviderRef = (ref) => {
this.providerRef = ref;
}
handleNoTeams = () => {
if (!this.refs.provider) {
if (!this.providerRef) {
setTimeout(this.handleNoTeams, 200);
return;
}
@ -53,7 +57,7 @@ export default class Root extends PureComponent {
};
errorTeamsList = () => {
if (!this.refs.provider) {
if (!this.providerRef) {
setTimeout(this.errorTeamsList, 200);
return;
}
@ -62,7 +66,7 @@ export default class Root extends PureComponent {
navigateToTeamsPage = (screen) => {
const {currentUrl, theme} = this.props;
const {intl} = this.refs.provider.getChildContext();
const {intl} = this.providerRef.getChildContext();
let passProps = {theme};
const options = {topBar: {}};
@ -99,7 +103,7 @@ export default class Root extends PureComponent {
return (
<IntlProvider
ref='provider'
ref={this.setProviderRef}
locale={locale}
messages={getTranslations(locale)}
>

View file

@ -3,15 +3,17 @@
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {Keyboard, NativeModules, View} from 'react-native';
import {Dimensions, Keyboard, NativeModules, View} from 'react-native';
import SafeArea from 'react-native-safe-area';
import EventEmitter from 'mattermost-redux/utils/event_emitter';
import {DeviceTypes} from 'app/constants';
import {DeviceTypes, ViewTypes} from 'app/constants';
import mattermostManaged from 'app/mattermost_managed';
import EphemeralStore from 'app/store/ephemeral_store';
const {StatusBarManager} = NativeModules;
const {PORTRAIT, LANDSCAPE} = ViewTypes;
export default class SafeAreaIos extends PureComponent {
static propTypes = {
@ -57,8 +59,12 @@ export default class SafeAreaIos extends PureComponent {
componentDidMount() {
this.mounted = true;
SafeArea.addEventListener('safeAreaInsetsForRootViewDidChange', this.onSafeAreaInsetsForRootViewChange);
Dimensions.addEventListener('change', this.getSafeAreaInsets);
EventEmitter.on('update_safe_area_view', this.getSafeAreaInsets);
if (EphemeralStore.safeAreaInsets[PORTRAIT] === null || EphemeralStore.safeAreaInsets[LANDSCAPE] === null) {
SafeArea.addEventListener('safeAreaInsetsForRootViewDidChange', this.onSafeAreaInsetsForRootViewChange);
}
this.keyboardDidShowListener = Keyboard.addListener('keyboardWillShow', this.keyboardWillShow);
this.keyboardDidHideListener = Keyboard.addListener('keyboardWillHide', this.keyboardWillHide);
@ -66,14 +72,40 @@ export default class SafeAreaIos extends PureComponent {
}
componentWillUnmount() {
SafeArea.removeEventListener('safeAreaInsetsForRootViewDidChange', this.onSafeAreaInsetsForRootViewChange);
Dimensions.removeEventListener('change', this.getSafeAreaInsets);
EventEmitter.off('update_safe_area_view', this.getSafeAreaInsets);
SafeArea.removeEventListener('safeAreaInsetsForRootViewDidChange', this.onSafeAreaInsetsForRootViewChange);
this.keyboardDidShowListener.remove();
this.keyboardDidHideListener.remove();
this.mounted = false;
}
getSafeAreaInsets = async (dimensions) => {
this.getStatusBarHeight();
const safeAreaInsetsStored = EphemeralStore.safeAreaInsets[PORTRAIT] !== null && EphemeralStore.safeAreaInsets[LANDSCAPE] !== null;
if ((DeviceTypes.IS_IPHONE_WITH_INSETS || mattermostManaged.hasSafeAreaInsets) && !safeAreaInsetsStored) {
const window = dimensions?.window || Dimensions.get('window');
const landscape = window.width > window.length;
const {safeAreaInsets} = await SafeArea.getSafeAreaInsetsForRootView();
this.setSafeAreaInsets(safeAreaInsets, landscape);
}
}
setSafeAreaInsets = (safeAreaInsets, landscape) => {
const orientation = landscape ? LANDSCAPE : PORTRAIT;
if (EphemeralStore.safeAreaInsets[orientation] === null) {
EphemeralStore.safeAreaInsets[orientation] = safeAreaInsets;
}
if (this.mounted) {
this.setState({
safeAreaInsets: EphemeralStore.safeAreaInsets[orientation],
});
}
}
getStatusBarHeight = () => {
try {
StatusBarManager.getHeight(
@ -88,26 +120,18 @@ export default class SafeAreaIos extends PureComponent {
}
};
getSafeAreaInsets = () => {
this.getStatusBarHeight();
onSafeAreaInsetsForRootViewChange = ({safeAreaInsets}) => {
if (EphemeralStore.safeAreaInsets[PORTRAIT] !== null && EphemeralStore.safeAreaInsets[LANDSCAPE] !== null) {
SafeArea.removeEventListener('safeAreaInsetsForRootViewDidChange', this.onSafeAreaInsetsForRootViewChange);
return;
}
if (DeviceTypes.IS_IPHONE_WITH_INSETS || mattermostManaged.hasSafeAreaInsets) {
SafeArea.getSafeAreaInsetsForRootView().then((result) => {
const {safeAreaInsets} = result;
if (this.mounted) {
this.setState({safeAreaInsets});
}
});
}
};
onSafeAreaInsetsForRootViewChange = (result) => {
const {safeAreaInsets} = result;
if (this.mounted && (DeviceTypes.IS_IPHONE_WITH_INSETS || mattermostManaged.hasSafeAreaInsets)) {
this.getStatusBarHeight();
this.setState({safeAreaInsets});
const {width, height} = Dimensions.get('window');
const landscape = width > height;
this.setSafeAreaInsets(safeAreaInsets, landscape);
}
}

View file

@ -6,11 +6,14 @@ import {shallow} from 'enzyme';
import Preferences from 'mattermost-redux/constants/preferences';
import {DeviceTypes} from 'app/constants';
import {DeviceTypes, ViewTypes} from 'app/constants';
import mattermostManaged from 'app/mattermost_managed';
import EphemeralStore from 'app/store/ephemeral_store';
import SafeAreaIos from './safe_area_view.ios';
const {PORTRAIT, LANDSCAPE} = ViewTypes;
describe('SafeAreaIos', () => {
const baseProps = {
children: [],
@ -37,10 +40,44 @@ describe('SafeAreaIos', () => {
},
};
const PORTRAIT_INSETS = {
safeAreaInsets: {
top: 111,
left: 111,
bottom: 111,
right: 111,
},
};
const LANDSCAPE_INSETS = {
safeAreaInsets: {
top: 222,
left: 222,
bottom: 222,
right: 222,
},
};
const IGNORED_INSETS = {
safeAreaInsets: {
top: 333,
left: 333,
bottom: 333,
right: 333,
},
};
SafeArea.getSafeAreaInsetsForRootView = jest.fn().mockImplementation(() => {
return Promise.resolve(TEST_INSETS_1);
});
beforeEach(() => {
EphemeralStore.safeAreaInsets = {
[PORTRAIT]: null,
[LANDSCAPE]: null,
};
});
test('should match snapshot', () => {
const wrapper = shallow(
<SafeAreaIos {...baseProps}/>
@ -148,4 +185,144 @@ describe('SafeAreaIos', () => {
instance.onSafeAreaInsetsForRootViewChange(TEST_INSETS_2);
expect(wrapper.state().safeAreaInsets).not.toEqual(TEST_INSETS_2.safeAreaInsets);
});
test('should set portrait safe area insets', () => {
const wrapper = shallow(
<SafeAreaIos {...baseProps}/>
);
expect(wrapper.state().safeAreaInsets).not.toEqual(PORTRAIT_INSETS.safeAreaInsets);
expect(EphemeralStore.safeAreaInsets[PORTRAIT]).toEqual(null);
expect(EphemeralStore.safeAreaInsets[LANDSCAPE]).toEqual(null);
const landscape = false;
const instance = wrapper.instance();
instance.setSafeAreaInsets(PORTRAIT_INSETS.safeAreaInsets, landscape);
expect(wrapper.state().safeAreaInsets).toEqual(PORTRAIT_INSETS.safeAreaInsets);
expect(EphemeralStore.safeAreaInsets[PORTRAIT]).toEqual(PORTRAIT_INSETS.safeAreaInsets);
expect(EphemeralStore.safeAreaInsets[LANDSCAPE]).toEqual(null);
});
test('should set portrait safe area insets from EphemeralStore', () => {
const wrapper = shallow(
<SafeAreaIos {...baseProps}/>
);
EphemeralStore.safeAreaInsets[PORTRAIT] = PORTRAIT_INSETS.safeAreaInsets;
expect(wrapper.state().safeAreaInsets).not.toEqual(PORTRAIT_INSETS.safeAreaInsets);
const landscape = false;
const instance = wrapper.instance();
instance.setSafeAreaInsets(IGNORED_INSETS.safeAreaInsets, landscape);
expect(wrapper.state().safeAreaInsets).toEqual(PORTRAIT_INSETS.safeAreaInsets);
expect(EphemeralStore.safeAreaInsets[PORTRAIT]).toEqual(PORTRAIT_INSETS.safeAreaInsets);
expect(EphemeralStore.safeAreaInsets[LANDSCAPE]).toEqual(null);
});
test('should set landscape safe area insets', () => {
const wrapper = shallow(
<SafeAreaIos {...baseProps}/>
);
expect(wrapper.state().safeAreaInsets).not.toEqual(LANDSCAPE_INSETS.safeAreaInsets);
expect(EphemeralStore.safeAreaInsets[PORTRAIT]).toEqual(null);
expect(EphemeralStore.safeAreaInsets[LANDSCAPE]).toEqual(null);
const landscape = true;
const instance = wrapper.instance();
instance.setSafeAreaInsets(LANDSCAPE_INSETS.safeAreaInsets, landscape);
expect(wrapper.state().safeAreaInsets).toEqual(LANDSCAPE_INSETS.safeAreaInsets);
expect(EphemeralStore.safeAreaInsets[LANDSCAPE]).toEqual(LANDSCAPE_INSETS.safeAreaInsets);
expect(EphemeralStore.safeAreaInsets[PORTRAIT]).toEqual(null);
});
test('should set landscape safe area insets from EphemeralStore', () => {
const wrapper = shallow(
<SafeAreaIos {...baseProps}/>
);
EphemeralStore.safeAreaInsets[LANDSCAPE] = LANDSCAPE_INSETS.safeAreaInsets;
expect(wrapper.state().safeAreaInsets).not.toEqual(LANDSCAPE_INSETS.safeAreaInsets);
const landscape = true;
const instance = wrapper.instance();
instance.setSafeAreaInsets(IGNORED_INSETS.safeAreaInsets, landscape);
expect(wrapper.state().safeAreaInsets).toEqual(LANDSCAPE_INSETS.safeAreaInsets);
expect(EphemeralStore.safeAreaInsets[LANDSCAPE]).toEqual(LANDSCAPE_INSETS.safeAreaInsets);
expect(EphemeralStore.safeAreaInsets[PORTRAIT]).toEqual(null);
});
test('should add safeAreaInsetsForRootViewDidChange listener when EphemeralStore values are not set', () => {
const addEventListener = jest.spyOn(SafeArea, 'addEventListener');
expect(EphemeralStore.safeAreaInsets[PORTRAIT]).toEqual(null);
expect(EphemeralStore.safeAreaInsets[LANDSCAPE]).toEqual(null);
let wrapper = shallow(
<SafeAreaIos {...baseProps}/>
);
let instance = wrapper.instance();
expect(addEventListener).toHaveBeenCalledWith('safeAreaInsetsForRootViewDidChange', instance.onSafeAreaInsetsForRootViewChange);
addEventListener.mockClear();
EphemeralStore.safeAreaInsets[PORTRAIT] = TEST_INSETS_1.safeAreaInsets;
wrapper = shallow(
<SafeAreaIos {...baseProps}/>
);
instance = wrapper.instance();
expect(addEventListener).toHaveBeenCalledWith('safeAreaInsetsForRootViewDidChange', instance.onSafeAreaInsetsForRootViewChange);
addEventListener.mockClear();
EphemeralStore.safeAreaInsets[PORTRAIT] = TEST_INSETS_1.safeAreaInsets;
EphemeralStore.safeAreaInsets[LANDSCAPE] = TEST_INSETS_1.safeAreaInsets;
wrapper = shallow(
<SafeAreaIos {...baseProps}/>
);
instance = wrapper.instance();
expect(addEventListener).not.toHaveBeenCalled();
});
test('should remove safeAreaInsetsForRootViewDidChange listener when EphemeralStore values are set', () => {
const removeEventListener = jest.spyOn(SafeArea, 'removeEventListener');
const wrapper = shallow(
<SafeAreaIos {...baseProps}/>
);
const instance = wrapper.instance();
expect(EphemeralStore.safeAreaInsets[PORTRAIT]).toEqual(null);
expect(EphemeralStore.safeAreaInsets[LANDSCAPE]).toEqual(null);
instance.onSafeAreaInsetsForRootViewChange(TEST_INSETS_1);
expect(removeEventListener).not.toHaveBeenCalled();
EphemeralStore.safeAreaInsets[PORTRAIT] = TEST_INSETS_1.safeAreaInsets;
instance.onSafeAreaInsetsForRootViewChange(TEST_INSETS_1);
expect(removeEventListener).not.toHaveBeenCalled();
EphemeralStore.safeAreaInsets[LANDSCAPE] = TEST_INSETS_1.safeAreaInsets;
instance.onSafeAreaInsetsForRootViewChange(TEST_INSETS_1);
expect(removeEventListener).toHaveBeenCalledWith('safeAreaInsetsForRootViewDidChange', instance.onSafeAreaInsetsForRootViewChange);
});
test('getSafeAreaInsets should set safe area insets when not already in ephemeral store', async () => {
const wrapper = shallow(
<SafeAreaIos {...baseProps}/>
);
const instance = wrapper.instance();
const setSafeAreaInsets = jest.spyOn(instance, 'setSafeAreaInsets');
expect(EphemeralStore.safeAreaInsets[PORTRAIT]).toEqual(null);
expect(EphemeralStore.safeAreaInsets[LANDSCAPE]).toEqual(null);
await instance.getSafeAreaInsets();
expect(setSafeAreaInsets).toHaveBeenCalled();
setSafeAreaInsets.mockClear();
EphemeralStore.safeAreaInsets[PORTRAIT] = TEST_INSETS_1.safeAreaInsets; /* eslint-disable-line require-atomic-updates */
await instance.getSafeAreaInsets();
expect(setSafeAreaInsets).toHaveBeenCalled();
setSafeAreaInsets.mockClear();
EphemeralStore.safeAreaInsets[LANDSCAPE] = TEST_INSETS_1.safeAreaInsets; /* eslint-disable-line require-atomic-updates */
await instance.getSafeAreaInsets();
expect(setSafeAreaInsets).not.toHaveBeenCalled();
});
});

View file

@ -76,6 +76,10 @@ export default class SearchBarAndroid extends PureComponent {
};
}
setInputRef = (ref) => {
this.inputRef = ref;
}
cancel = () => {
this.onCancelButtonPress();
};
@ -123,11 +127,11 @@ export default class SearchBarAndroid extends PureComponent {
};
blur = () => {
this.refs.input.blur();
this.inputRef.blur();
};
focus = () => {
this.refs.input.focus();
this.inputRef.focus();
};
render() {
@ -207,7 +211,7 @@ export default class SearchBarAndroid extends PureComponent {
/>
}
<TextInput
ref='input'
ref={this.setInputRef}
blurOnSubmit={blurOnSubmit}
refocusInput={this.state.refocusInput}
value={this.props.value}

View file

@ -56,8 +56,12 @@ export default class SearchBarIos extends PureComponent {
searchIconExpandedMargin: 10,
};
setSearchRef = (ref) => {
this.searchRef = ref;
}
cancel = () => {
this.refs.search.onCancel();
this.searchRef.onCancel();
};
onBlur = () => {
@ -94,18 +98,18 @@ export default class SearchBarIos extends PureComponent {
};
blur = () => {
this.refs.search.blur();
this.searchRef.blur();
};
focus = () => {
this.refs.search.focus();
this.searchRef.focus();
};
render() {
return (
<Search
{...this.props}
ref='search'
ref={this.setSearchRef}
placeholderCollapsedMargin={33}
placeholderExpandedMargin={33}
shadowVisible={false}

View file

@ -142,15 +142,23 @@ export default class Search extends Component {
}
}
setSearchContainerRef = (ref) => {
this.searchContainerRef = ref;
}
setInputKeywordRef = (ref) => {
this.inputKeywordRef = ref;
}
blur = () => {
this.refs.input_keyword.getNode().blur();
this.inputKeywordRef.getNode().blur();
this.setState({expanded: false});
this.collapseAnimation();
};
focus = () => {
InteractionManager.runAfterInteractions(() => {
const input = this.refs.input_keyword.getNode();
const input = this.inputKeywordRef.getNode();
if (!input.isFocused()) {
input.focus();
}
@ -384,7 +392,7 @@ export default class Search extends Component {
return (
<Animated.View
ref='searchContainer'
ref={this.setSearchContainerRef}
style={[
styles.container,
this.props.backgroundColor && {backgroundColor: this.props.backgroundColor},
@ -403,7 +411,7 @@ export default class Search extends Component {
)}
<Animated.View style={{backgroundColor, right: this.inputFocusAnimated, borderRadius: 2}}>
<AnimatedTextInput
ref='input_keyword'
ref={this.setInputKeywordRef}
style={[
styles.input,
this.props.placeholderTextColor && {color: this.props.placeholderTextColor},

View file

@ -56,6 +56,7 @@ exports[`ChannelItem should match snapshot 1`] = `
"dndIndicator": "#f74343",
"errorTextColor": "#fd5960",
"linkColor": "#2389d7",
"mentionBg": "#ffffff",
"mentionBj": "#ffffff",
"mentionColor": "#145dbf",
"mentionHighlightBg": "#ffe577",
@ -172,6 +173,7 @@ exports[`ChannelItem should match snapshot for current user i.e currentUser (you
"dndIndicator": "#f74343",
"errorTextColor": "#fd5960",
"linkColor": "#2389d7",
"mentionBg": "#ffffff",
"mentionBj": "#ffffff",
"mentionColor": "#145dbf",
"mentionHighlightBg": "#ffe577",
@ -287,6 +289,7 @@ exports[`ChannelItem should match snapshot for current user i.e currentUser (you
"dndIndicator": "#f74343",
"errorTextColor": "#fd5960",
"linkColor": "#2389d7",
"mentionBg": "#ffffff",
"mentionBj": "#ffffff",
"mentionColor": "#145dbf",
"mentionHighlightBg": "#ffe577",
@ -402,6 +405,7 @@ exports[`ChannelItem should match snapshot for deactivated user and is currentCh
"dndIndicator": "#f74343",
"errorTextColor": "#fd5960",
"linkColor": "#2389d7",
"mentionBg": "#ffffff",
"mentionBj": "#ffffff",
"mentionColor": "#145dbf",
"mentionHighlightBg": "#ffe577",
@ -506,6 +510,7 @@ exports[`ChannelItem should match snapshot for deactivated user and is searchRes
"dndIndicator": "#f74343",
"errorTextColor": "#fd5960",
"linkColor": "#2389d7",
"mentionBg": "#ffffff",
"mentionBj": "#ffffff",
"mentionColor": "#145dbf",
"mentionHighlightBg": "#ffe577",
@ -611,6 +616,7 @@ exports[`ChannelItem should match snapshot for deactivated user and not searchRe
"dndIndicator": "#f74343",
"errorTextColor": "#fd5960",
"linkColor": "#2389d7",
"mentionBg": "#ffffff",
"mentionBj": "#ffffff",
"mentionColor": "#145dbf",
"mentionHighlightBg": "#ffe577",
@ -720,6 +726,7 @@ exports[`ChannelItem should match snapshot with draft 1`] = `
"dndIndicator": "#f74343",
"errorTextColor": "#fd5960",
"linkColor": "#2389d7",
"mentionBg": "#ffffff",
"mentionBj": "#ffffff",
"mentionColor": "#145dbf",
"mentionHighlightBg": "#ffe577",
@ -827,6 +834,7 @@ exports[`ChannelItem should match snapshot with mentions and muted 1`] = `
"dndIndicator": "#f74343",
"errorTextColor": "#fd5960",
"linkColor": "#2389d7",
"mentionBg": "#ffffff",
"mentionBj": "#ffffff",
"mentionColor": "#145dbf",
"mentionHighlightBg": "#ffe577",
@ -894,7 +902,7 @@ exports[`ChannelItem should match snapshot with mentions and muted 1`] = `
onPress={[Function]}
style={
Object {
"backgroundColor": undefined,
"backgroundColor": "#ffffff",
"height": 21,
"padding": 3,
"position": "relative",

View file

@ -55,9 +55,13 @@ export default class ChannelsList extends PureComponent {
});
}
setSearchBarRef = (ref) => {
this.searchBarRef = ref;
}
cancelSearch = () => {
if (this.refs.search_bar) {
this.refs.search_bar.cancel();
if (this.searchBarRef) {
this.searchBarRef.cancel();
}
};
@ -135,7 +139,7 @@ export default class ChannelsList extends PureComponent {
const title = (
<View style={[styles.searchContainer, padding(isLandscape)]}>
<SearchBar
ref='search_bar'
ref={this.setSearchBarRef}
placeholder={intl.formatMessage({id: 'mobile.channel_drawer.search', defaultMessage: 'Jump to...'})}
cancelTitle={intl.formatMessage({id: 'mobile.post.cancel', defaultMessage: 'Cancel'})}
inputCollapsedMargin={0}

View file

@ -98,14 +98,18 @@ export default class List extends PureComponent {
}
componentDidUpdate(prevProps, prevState) {
if (prevState.sections !== this.state.sections && this.refs.list._wrapperListRef.getListRef()._viewabilityHelper) { //eslint-disable-line
this.refs.list.recordInteraction();
if (prevState.sections !== this.state.sections && this.listRef?._wrapperListRef?.getListRef()._viewabilityHelper) { //eslint-disable-line
this.listRef.recordInteraction();
this.updateUnreadIndicators({
viewableItems: Array.from(this.refs.list._wrapperListRef.getListRef()._viewabilityHelper._viewableItems.values()) //eslint-disable-line
viewableItems: Array.from(this.listRef._wrapperListRef.getListRef()._viewabilityHelper._viewableItems.values()) //eslint-disable-line
});
}
}
setListRef = (ref) => {
this.listRef = ref;
}
getSectionConfigByType = (props, sectionType) => {
const {canCreatePrivateChannels, canJoinPublicChannels} = props;
@ -345,8 +349,8 @@ export default class List extends PureComponent {
};
scrollToTop = () => {
if (this.refs.list) {
this.refs.list._wrapperListRef.getListRef().scrollToOffset({ //eslint-disable-line no-underscore-dangle
if (this.listRef?._wrapperListRef) {
this.listRef._wrapperListRef.getListRef().scrollToOffset({ //eslint-disable-line no-underscore-dangle
x: 0,
y: 0,
animated: true,
@ -402,7 +406,7 @@ export default class List extends PureComponent {
onLayout={this.onLayout}
>
<SectionList
ref='list'
ref={this.setListRef}
sections={sections}
contentContainerStyle={{paddingBottom}}
renderItem={this.renderItem}

View file

@ -35,8 +35,8 @@ export default class DrawerSwiper extends Component {
}
runOnLayout = (shouldRun = true) => {
if (this.refs.swiper) {
this.refs.swiper.runOnLayout = shouldRun;
if (this.swiperRef) {
this.swiperRef.runOnLayout = shouldRun;
}
};

View file

@ -37,6 +37,7 @@ export default class ChannelSidebar extends Component {
makeDirectChannel: PropTypes.func.isRequired,
setChannelDisplayName: PropTypes.func.isRequired,
setChannelLoading: PropTypes.func.isRequired,
joinChannel: PropTypes.func.isRequired,
}).isRequired,
blurPostTextBox: PropTypes.func.isRequired,
children: PropTypes.node,
@ -230,10 +231,11 @@ export default class ChannelSidebar extends Component {
const {
joinChannel,
makeDirectChannel,
setChannelLoading,
} = actions;
this.closeChannelDrawer();
actions.setChannelLoading(channel.id !== currentChannelId);
setChannelLoading(channel.id !== currentChannelId);
setTimeout(async () => {
const displayValue = {displayName: channel.display_name};
@ -263,7 +265,7 @@ export default class ChannelSidebar extends Component {
}
if (result.error || (!result.data && !result.data.channel)) {
actions.setChannelLoading(false);
setChannelLoading(false);
return;
}

View file

@ -20,6 +20,7 @@ describe('MainSidebar', () => {
makeDirectChannel: jest.fn(),
setChannelDisplayName: jest.fn(),
setChannelLoading: jest.fn(),
joinChannel: jest.fn(),
},
blurPostTextBox: jest.fn(),
currentTeamId: 'current-team-id',

View file

@ -81,20 +81,32 @@ export default class SettingsDrawer extends PureComponent {
Dimensions.removeEventListener('change', this.handleDimensions);
}
setDrawerRef = (ref) => {
this.drawerRef = ref;
}
confirmReset = (status) => {
const {intl} = this.context;
confirmOutOfOfficeDisabled(intl, status, this.updateStatus);
};
closeSettingsSidebar = () => {
if (this.refs.drawer && this.drawerOpened) {
this.refs.drawer.closeDrawer();
if (this.drawerRef && this.drawerOpened) {
this.drawerRef.closeDrawer();
}
};
openSettingsSidebar = () => {
this.props.blurPostTextBox();
if (this.drawerRef && !this.drawerOpened) {
this.drawerRef.openDrawer();
}
};
handleAndroidBack = () => {
if (this.refs.drawer && this.drawerOpened) {
this.refs.drawer.closeDrawer();
if (this.drawerRef && this.drawerOpened) {
this.drawerRef.closeDrawer();
return true;
}
@ -227,12 +239,25 @@ export default class SettingsDrawer extends PureComponent {
});
};
openSettingsSidebar = () => {
this.props.blurPostTextBox();
updateStatus = (status) => {
const {currentUser: {id: currentUserId}} = this.props;
this.props.actions.setStatus({
user_id: currentUserId,
status,
});
};
if (this.refs.drawer && !this.drawerOpened) {
this.refs.drawer.openDrawer();
setStatus = (status) => {
const {status: currentUserStatus} = this.props;
if (currentUserStatus === General.OUT_OF_OFFICE) {
dismissModal();
this.closeSettingsSidebar();
this.confirmReset(status);
return;
}
this.updateStatus(status);
EventEmitter.emit(NavigationTypes.NAVIGATION_CLOSE_MODAL);
};
renderUserStatusIcon = (userId) => {
@ -341,27 +366,6 @@ export default class SettingsDrawer extends PureComponent {
);
};
updateStatus = (status) => {
const {currentUser: {id: currentUserId}} = this.props;
this.props.actions.setStatus({
user_id: currentUserId,
status,
});
};
setStatus = (status) => {
const {status: currentUserStatus} = this.props;
if (currentUserStatus === General.OUT_OF_OFFICE) {
dismissModal();
this.closeSettingsSidebar();
this.confirmReset(status);
return;
}
this.updateStatus(status);
EventEmitter.emit(NavigationTypes.NAVIGATION_CLOSE_MODAL);
};
render() {
const {children} = this.props;
const {deviceWidth, openDrawerOffset} = this.state;
@ -369,7 +373,7 @@ export default class SettingsDrawer extends PureComponent {
return (
<DrawerLayout
ref='drawer'
ref={this.setDrawerRef}
renderNavigationView={this.renderNavigationView}
onDrawerClose={this.handleDrawerClose}
onDrawerOpen={this.handleDrawerOpen}

View file

@ -22,12 +22,16 @@ export default class TextInputWithLocalizedPlaceholder extends PureComponent {
intl: intlShape.isRequired,
};
setInputRef = (ref) => {
this.inputRef = ref;
}
blur = () => {
this.refs.input.blur();
this.inputRef.blur();
};
focus = () => {
this.refs.input.focus();
this.inputRef.focus();
};
render() {
@ -40,7 +44,7 @@ export default class TextInputWithLocalizedPlaceholder extends PureComponent {
return (
<TextInput
ref='input'
ref={this.setInputRef}
{...otherProps}
placeholder={placeholderString}
disableFullscreenUI={true}

View file

@ -1,64 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import RNToolTip from 'react-native-tooltip';
import {setToolTipVisible, getToolTipVisible} from 'app/utils/tooltip';
export default class ToolTip extends PureComponent {
static propTypes = {
onHide: PropTypes.func,
onShow: PropTypes.func,
actions: PropTypes.array.isRequired,
};
static defaultProps = {
actions: [],
}
handleHide = () => {
if (this.props.onHide) {
this.props.onHide();
}
setToolTipVisible(false);
};
handleShow = () => {
if (this.props.onShow) {
this.props.onShow();
}
setToolTipVisible();
};
hideMenu = () => {
if (this.refs.toolTip) {
this.refs.toolTip.hideMenu();
}
};
showMenu = () => {
if (this.refs.toolTip) {
this.refs.toolTip.showMenu();
}
};
componentDidUpdate(prevProps) {
if (prevProps.actions.length !== this.props.actions.length && getToolTipVisible()) {
this.refs.toolTip.hideMenu();
setTimeout(() => this.refs.toolTip.showMenu(), 1);
}
}
render() {
return (
<RNToolTip
{...this.props}
onHide={this.handleHide}
onShow={this.handleShow}
ref='toolTip'
/>
);
}
}

View file

@ -95,6 +95,9 @@ const ViewTypes = keyMirror({
SELECTED_ACTION_MENU: null,
SUBMIT_ATTACHMENT_MENU_ACTION: null,
SELECT_CHANNEL_WITH_MEMBER: null,
PORTRAIT: null,
LANDSCAPE: null,
});
export default {

View file

@ -134,7 +134,7 @@ class EMMProvider {
};
showNotSecuredAlert = (translations) => {
return new Promise(async (resolve) => {
return new Promise(async (resolve) => { /* eslint-disable-line no-async-promise-executor */
const options = [];
if (Platform.OS === 'android') {

View file

@ -102,7 +102,7 @@ Client4.doFetchWithResponse = async (url, options) => {
if (headers[HEADER_X_CLUSTER_ID] || headers[HEADER_X_CLUSTER_ID.toLowerCase()]) {
const clusterId = headers[HEADER_X_CLUSTER_ID] || headers[HEADER_X_CLUSTER_ID.toLowerCase()];
if (clusterId && Client4.clusterId !== clusterId) {
Client4.clusterId = clusterId;
Client4.clusterId = clusterId; /* eslint-disable-line require-atomic-updates */
}
}

View file

@ -7,11 +7,14 @@ import semver from 'semver';
import {setAppState, setServerVersion} from 'mattermost-redux/actions/general';
import {loadMe, logout} from 'mattermost-redux/actions/users';
import {autoUpdateTimezone} from 'mattermost-redux/actions/timezone';
import {close as closeWebSocket} from 'mattermost-redux/actions/websocket';
import {Client4} from 'mattermost-redux/client';
import {General} from 'mattermost-redux/constants';
import EventEmitter from 'mattermost-redux/utils/event_emitter';
import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels';
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
import {isTimezoneEnabled} from 'mattermost-redux/selectors/entities/timezone';
import {setDeviceDimensions, setDeviceOrientation, setDeviceAsTablet, setStatusBarHeight} from 'app/actions/device';
import {selectDefaultChannel} from 'app/actions/views/channel';
@ -24,6 +27,7 @@ import PushNotifications from 'app/push_notifications';
import {getCurrentLocale} from 'app/selectors/i18n';
import {t} from 'app/utils/i18n';
import {deleteFileCache} from 'app/utils/file';
import {getDeviceTimezoneAsync} from 'app/utils/timezone';
import LocalConfig from 'assets/config';
@ -47,6 +51,7 @@ class GlobalEventHandler {
appActive = async () => {
this.turnOnInAppNotificationHandling();
this.setUserTimezone();
// if the app is being controlled by an EMM provider
if (emmProvider.enabled && emmProvider.inAppPinCode) {
@ -57,7 +62,7 @@ class GlobalEventHandler {
await emmProvider.handleAuthentication(this.store, prompt);
}
emmProvider.inBackgroundSince = null;
emmProvider.inBackgroundSince = null; /* eslint-disable-line require-atomic-updates */
};
appInactive = () => {
@ -261,6 +266,18 @@ class GlobalEventHandler {
showOverlay(screen, passProps);
}
};
setUserTimezone = async () => {
const {dispatch, getState} = this.store;
const state = getState();
const currentUserId = getCurrentUserId(state);
const enableTimezone = isTimezoneEnabled(state);
if (enableTimezone && currentUserId) {
const timezone = await getDeviceTimezoneAsync();
dispatch(autoUpdateTimezone(timezone));
}
};
}
export default new GlobalEventHandler();

View file

@ -30,6 +30,10 @@ jest.mock('react-native-notifications', () => ({
NotificationCategory: jest.fn(),
}));
jest.mock('react-native-status-bar-size', () => ({
addEventListener: jest.fn(),
}));
const mockStore = configureMockStore([thunk]);
const store = mockStore(intitialState);
GlobalEventHandler.store = store;
@ -86,4 +90,14 @@ describe('GlobalEventHandler', () => {
expect(appActive).not.toHaveBeenCalled();
expect(appInactive).not.toHaveBeenCalled();
});
it('should set the user TimeZone when the app becomes active', () => {
const onAppStateChange = jest.spyOn(GlobalEventHandler, 'onAppStateChange');
const setUserTimezone = jest.spyOn(GlobalEventHandler, 'setUserTimezone');
GlobalEventHandler.configure({store});
expect(GlobalEventHandler.store).not.toBeNull();
expect(onAppStateChange).toHaveBeenCalledWith('active');
expect(setUserTimezone).toHaveBeenCalledTimes(1);
});
});

View file

@ -38,6 +38,7 @@ exports[`ChannelNavBar should match, full snapshot 1`] = `
"dndIndicator": "#f74343",
"errorTextColor": "#fd5960",
"linkColor": "#2389d7",
"mentionBg": "#ffffff",
"mentionBj": "#ffffff",
"mentionColor": "#145dbf",
"mentionHighlightBg": "#ffe577",

View file

@ -98,7 +98,7 @@ exports[`ChannelDrawerButton should match, full snapshot 2`] = `
onPress={[Function]}
style={
Object {
"backgroundColor": undefined,
"backgroundColor": "#ffffff",
"height": 19,
"padding": 3,
}

View file

@ -33,6 +33,7 @@ export default class ChannelPostList extends PureComponent {
selectPost: PropTypes.func.isRequired,
recordLoadTime: PropTypes.func.isRequired,
refreshChannelWithRetry: PropTypes.func.isRequired,
setChannelRefreshing: PropTypes.func,
}).isRequired,
channelId: PropTypes.string.isRequired,
channelRefreshingFailed: PropTypes.bool,

View file

@ -116,6 +116,10 @@ export default class ChannelAddMembers extends PureComponent {
}
}
setSearchBarRef = (ref) => {
this.searchBarRef = ref;
}
clearSearch = () => {
this.setState({term: '', searchResults: []});
};
@ -343,7 +347,7 @@ export default class ChannelAddMembers extends PureComponent {
<StatusBar/>
<View style={[style.searchBar, padding(isLandscape)]}>
<SearchBar
ref='search_bar'
ref={this.setSearchBarRef}
placeholder={formatMessage({id: 'search_bar.search', defaultMessage: 'Search'})}
cancelTitle={formatMessage({id: 'mobile.post.cancel', defaultMessage: 'Cancel'})}
backgroundColor='transparent'

View file

@ -44,6 +44,7 @@ exports[`channel_info should match snapshot 1`] = `
"dndIndicator": "#f74343",
"errorTextColor": "#fd5960",
"linkColor": "#2389d7",
"mentionBg": "#ffffff",
"mentionBj": "#ffffff",
"mentionColor": "#145dbf",
"mentionHighlightBg": "#ffe577",
@ -95,6 +96,7 @@ exports[`channel_info should match snapshot 1`] = `
"dndIndicator": "#f74343",
"errorTextColor": "#fd5960",
"linkColor": "#2389d7",
"mentionBg": "#ffffff",
"mentionBj": "#ffffff",
"mentionColor": "#145dbf",
"mentionHighlightBg": "#ffe577",
@ -143,6 +145,7 @@ exports[`channel_info should match snapshot 1`] = `
"dndIndicator": "#f74343",
"errorTextColor": "#fd5960",
"linkColor": "#2389d7",
"mentionBg": "#ffffff",
"mentionBj": "#ffffff",
"mentionColor": "#145dbf",
"mentionHighlightBg": "#ffe577",
@ -191,6 +194,7 @@ exports[`channel_info should match snapshot 1`] = `
"dndIndicator": "#f74343",
"errorTextColor": "#fd5960",
"linkColor": "#2389d7",
"mentionBg": "#ffffff",
"mentionBj": "#ffffff",
"mentionColor": "#145dbf",
"mentionHighlightBg": "#ffe577",
@ -238,6 +242,7 @@ exports[`channel_info should match snapshot 1`] = `
"dndIndicator": "#f74343",
"errorTextColor": "#fd5960",
"linkColor": "#2389d7",
"mentionBg": "#ffffff",
"mentionBj": "#ffffff",
"mentionColor": "#145dbf",
"mentionHighlightBg": "#ffe577",
@ -287,6 +292,7 @@ exports[`channel_info should match snapshot 1`] = `
"dndIndicator": "#f74343",
"errorTextColor": "#fd5960",
"linkColor": "#2389d7",
"mentionBg": "#ffffff",
"mentionBj": "#ffffff",
"mentionColor": "#145dbf",
"mentionHighlightBg": "#ffe577",
@ -336,6 +342,7 @@ exports[`channel_info should match snapshot 1`] = `
"dndIndicator": "#f74343",
"errorTextColor": "#fd5960",
"linkColor": "#2389d7",
"mentionBg": "#ffffff",
"mentionBj": "#ffffff",
"mentionColor": "#145dbf",
"mentionHighlightBg": "#ffe577",
@ -385,6 +392,7 @@ exports[`channel_info should match snapshot 1`] = `
"dndIndicator": "#f74343",
"errorTextColor": "#fd5960",
"linkColor": "#2389d7",
"mentionBg": "#ffffff",
"mentionBj": "#ffffff",
"mentionColor": "#145dbf",
"mentionHighlightBg": "#ffe577",
@ -434,6 +442,7 @@ exports[`channel_info should match snapshot 1`] = `
"dndIndicator": "#f74343",
"errorTextColor": "#fd5960",
"linkColor": "#2389d7",
"mentionBg": "#ffffff",
"mentionBj": "#ffffff",
"mentionColor": "#145dbf",
"mentionHighlightBg": "#ffe577",
@ -484,6 +493,7 @@ exports[`channel_info should match snapshot 1`] = `
"dndIndicator": "#f74343",
"errorTextColor": "#fd5960",
"linkColor": "#2389d7",
"mentionBg": "#ffffff",
"mentionBj": "#ffffff",
"mentionColor": "#145dbf",
"mentionHighlightBg": "#ffe577",
@ -546,6 +556,7 @@ exports[`channel_info should match snapshot 1`] = `
"dndIndicator": "#f74343",
"errorTextColor": "#fd5960",
"linkColor": "#2389d7",
"mentionBg": "#ffffff",
"mentionBj": "#ffffff",
"mentionColor": "#145dbf",
"mentionHighlightBg": "#ffe577",

View file

@ -47,6 +47,7 @@ exports[`channel_info_header should match snapshot 1`] = `
"dndIndicator": "#f74343",
"errorTextColor": "#fd5960",
"linkColor": "#2389d7",
"mentionBg": "#ffffff",
"mentionBj": "#ffffff",
"mentionColor": "#145dbf",
"mentionHighlightBg": "#ffe577",
@ -463,6 +464,7 @@ exports[`channel_info_header should match snapshot when DM and hasGuests and is
"dndIndicator": "#f74343",
"errorTextColor": "#fd5960",
"linkColor": "#2389d7",
"mentionBg": "#ffffff",
"mentionBj": "#ffffff",
"mentionColor": "#145dbf",
"mentionHighlightBg": "#ffe577",
@ -907,6 +909,7 @@ exports[`channel_info_header should match snapshot when DM and hasGuests but its
"dndIndicator": "#f74343",
"errorTextColor": "#fd5960",
"linkColor": "#2389d7",
"mentionBg": "#ffffff",
"mentionBj": "#ffffff",
"mentionColor": "#145dbf",
"mentionHighlightBg": "#ffe577",
@ -1323,6 +1326,7 @@ exports[`channel_info_header should match snapshot when GM and hasGuests 1`] = `
"dndIndicator": "#f74343",
"errorTextColor": "#fd5960",
"linkColor": "#2389d7",
"mentionBg": "#ffffff",
"mentionBj": "#ffffff",
"mentionColor": "#145dbf",
"mentionHighlightBg": "#ffe577",
@ -1767,6 +1771,7 @@ exports[`channel_info_header should match snapshot when is group constrained 1`]
"dndIndicator": "#f74343",
"errorTextColor": "#fd5960",
"linkColor": "#2389d7",
"mentionBg": "#ffffff",
"mentionBj": "#ffffff",
"mentionColor": "#145dbf",
"mentionHighlightBg": "#ffe577",
@ -2205,6 +2210,7 @@ exports[`channel_info_header should match snapshot when public channel and hasGu
"dndIndicator": "#f74343",
"errorTextColor": "#fd5960",
"linkColor": "#2389d7",
"mentionBg": "#ffffff",
"mentionBj": "#ffffff",
"mentionColor": "#145dbf",
"mentionHighlightBg": "#ffe577",

View file

@ -52,6 +52,7 @@ export default class ChannelInfo extends PureComponent {
currentChannelCreatorName: PropTypes.string,
currentChannelMemberCount: PropTypes.number,
currentChannelGuestCount: PropTypes.number,
currentChannelPinnedPostCount: PropTypes.number,
currentUserId: PropTypes.string,
currentUserIsGuest: PropTypes.bool,
status: PropTypes.string,
@ -414,6 +415,7 @@ export default class ChannelInfo extends PureComponent {
actionsRows = (style, channelIsArchived) => {
const {
currentChannelMemberCount,
currentChannelPinnedPostCount,
canManageUsers,
canEditChannel,
theme,
@ -475,6 +477,7 @@ export default class ChannelInfo extends PureComponent {
<ChannelInfoRow
action={this.goToPinnedPosts}
defaultMessage='Pinned Posts'
detail={currentChannelPinnedPostCount}
image={pinIcon}
textId={t('channel_header.pinnedPosts')}
theme={theme}

View file

@ -55,6 +55,7 @@ function mapStateToProps(state) {
const currentChannelCreatorName = currentChannelCreator && currentChannelCreator.username;
const currentChannelStats = getCurrentChannelStats(state);
const currentChannelMemberCount = currentChannelStats && currentChannelStats.member_count;
const currentChannelPinnedPostCount = currentChannelStats && currentChannelStats.pinnedpost_count;
let currentChannelGuestCount = (currentChannelStats && currentChannelStats.guest_count) || 0;
const currentChannelMember = getMyCurrentChannelMembership(state);
const currentUserId = getCurrentUserId(state);
@ -108,6 +109,7 @@ function mapStateToProps(state) {
currentChannelCreatorName,
currentChannelMemberCount,
currentChannelGuestCount,
currentChannelPinnedPostCount,
currentUserId,
currentUserIsGuest,
isChannelMuted: isChannelMuted(currentChannelMember),

View file

@ -68,6 +68,7 @@ exports[`ChannelMembers should match snapshot 1`] = `
"dndIndicator": "#f74343",
"errorTextColor": "#fd5960",
"linkColor": "#2389d7",
"mentionBg": "#ffffff",
"mentionBj": "#ffffff",
"mentionColor": "#145dbf",
"mentionHighlightBg": "#ffe577",

View file

@ -108,6 +108,10 @@ export default class ChannelMembers extends PureComponent {
}
}
setSearchBarRef = (ref) => {
this.searchBarRef = ref;
}
clearSearch = () => {
this.setState({term: '', searchResults: []});
};
@ -371,7 +375,7 @@ export default class ChannelMembers extends PureComponent {
<StatusBar/>
<View style={[style.searchBar, padding(isLandscape)]}>
<SearchBar
ref='search_bar'
ref={this.setSearchBarRef}
placeholder={formatMessage({id: 'search_bar.search', defaultMessage: 'Search'})}
cancelTitle={formatMessage({id: 'mobile.post.cancel', defaultMessage: 'Cancel'})}
backgroundColor='transparent'

View file

@ -13,17 +13,12 @@ import {
View,
} from 'react-native';
import SyntaxHighlighter from 'react-native-syntax-highlighter';
import CustomPropTypes from 'app/constants/custom_prop_types';
import {getCodeFont} from 'app/utils/markdown';
import {
changeOpacity,
makeStyleSheetFromTheme,
setNavigatorStyles,
getKeyboardAppearanceFromTheme,
getHighlightStyleFromTheme,
} from 'app/utils/theme';
import {popTopScreen} from 'app/actions/navigation';
@ -32,8 +27,6 @@ export default class Code extends React.PureComponent {
componentId: PropTypes.string,
theme: PropTypes.object.isRequired,
content: PropTypes.string.isRequired,
language: PropTypes.string,
textStyle: CustomPropTypes.Style,
};
componentDidMount() {
@ -80,20 +73,13 @@ export default class Code extends React.PureComponent {
let textComponent;
if (Platform.OS === 'ios') {
textComponent = (
<SyntaxHighlighter
language={this.props.language}
style={getHighlightStyleFromTheme(this.props.theme)}
highlighter={'hljs'}
CodeTag={TextInput}
codeTagProps={{
editable: false,
multiline: true,
keyboardAppearance: getKeyboardAppearanceFromTheme(this.props.theme),
style: {...style.codeText, ...this.props.textStyle},
}}
>
{this.props.content}
</SyntaxHighlighter>
<TextInput
editable={false}
multiline={true}
value={this.props.content}
style={[style.codeText]}
keyboardAppearance={getKeyboardAppearanceFromTheme(this.props.theme)}
/>
);
} else {
textComponent = (
@ -159,7 +145,6 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
flexGrow: 0,
flexShrink: 1,
width: '100%',
backgroundColor: getHighlightStyleFromTheme(theme).hljs.background,
},
code: {
paddingHorizontal: 6,
@ -167,6 +152,9 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
android: {
paddingVertical: 4,
},
ios: {
top: -4,
},
}),
},
codeText: {
@ -176,7 +164,8 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
lineHeight: 18,
...Platform.select({
ios: {
top: -11,
margin: 0,
padding: 0,
},
}),
},

View file

@ -8,9 +8,11 @@ import {
View,
} from 'react-native';
import {Navigation} from 'react-native-navigation';
import {KeyboardTrackingView} from 'react-native-keyboard-tracking-view';
import {RequestStatus} from 'mattermost-redux/constants';
import Autocomplete, {AUTOCOMPLETE_MAX_HEIGHT} from 'app/components/autocomplete';
import ErrorText from 'app/components/error_text';
import Loading from 'app/components/loading';
import StatusBar from 'app/components/status_bar';
@ -56,7 +58,11 @@ export default class EditPost extends PureComponent {
constructor(props, context) {
super(props);
this.state = {message: props.post.message};
this.state = {
message: props.post.message,
cursorPosition: 0,
};
this.rightButton.color = props.theme.sidebarHeaderTextColor;
this.rightButton.text = context.intl.formatMessage({id: 'edit_post.save', defaultMessage: 'Save'});
@ -152,6 +158,11 @@ export default class EditPost extends PureComponent {
}
};
onPostSelectionChange = (event) => {
const cursorPosition = event.nativeEvent.selection.end;
this.setState({cursorPosition});
};
render() {
const {deviceHeight, deviceWidth, theme, isLandscape} = this.props;
const {editing, message, error} = this.state;
@ -200,9 +211,19 @@ export default class EditPost extends PureComponent {
underlineColorAndroid='transparent'
disableFullscreenUI={true}
keyboardAppearance={getKeyboardAppearanceFromTheme(this.props.theme)}
onSelectionChange={this.onPostSelectionChange}
/>
</View>
</View>
<KeyboardTrackingView style={style.autocompleteContainer}>
<Autocomplete
cursorPosition={this.state.cursorPosition}
maxHeight={AUTOCOMPLETE_MAX_HEIGHT}
onChangeText={this.onPostChangeText}
value={message}
nestedScrollEnabled={true}
/>
</KeyboardTrackingView>
</View>
);
}
@ -238,5 +259,9 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
padding: 15,
textAlignVertical: 'top',
},
autocompleteContainer: {
flex: 1,
justifyContent: 'flex-end',
},
};
});

View file

@ -40,6 +40,7 @@ exports[`edit_profile should match snapshot 1`] = `
"dndIndicator": "#f74343",
"errorTextColor": "#fd5960",
"linkColor": "#2389d7",
"mentionBg": "#ffffff",
"mentionBj": "#ffffff",
"mentionColor": "#145dbf",
"mentionHighlightBg": "#ffe577",

View file

@ -7,7 +7,7 @@ import {intlShape} from 'react-intl';
import {Alert, View} from 'react-native';
import RNFetchBlob from 'rn-fetch-blob';
import {KeyboardAwareScrollView} from 'react-native-keyboard-aware-scroll-view';
import {DocumentPickerUtil} from 'react-native-document-picker';
import DocumentPicker from 'react-native-document-picker';
import {Navigation} from 'react-native-navigation';
import {Client4} from 'mattermost-redux/client';
@ -521,7 +521,7 @@ export default class EditProfile extends PureComponent {
currentUser={currentUser}
theme={theme}
blurTextBox={emptyFunction}
browseFileTypes={DocumentPickerUtil.images()}
browseFileTypes={DocumentPicker.types.images}
canTakeVideo={false}
canBrowseVideoLibrary={false}
maxFileSize={MAX_SIZE}

View file

@ -27,6 +27,7 @@ exports[`ErrorTeamsList should match snapshot 1`] = `
"dndIndicator": "#f74343",
"errorTextColor": "#fd5960",
"linkColor": "#2389d7",
"mentionBg": "#ffffff",
"mentionBj": "#ffffff",
"mentionColor": "#145dbf",
"mentionHighlightBg": "#ffe577",

View file

@ -23,6 +23,7 @@ exports[`FlaggedPosts should match snapshot 1`] = `
"dndIndicator": "#f74343",
"errorTextColor": "#fd5960",
"linkColor": "#2389d7",
"mentionBg": "#ffffff",
"mentionBj": "#ffffff",
"mentionColor": "#145dbf",
"mentionHighlightBg": "#ffe577",

View file

@ -72,6 +72,10 @@ export default class FlaggedPosts extends PureComponent {
}
}
setListRef = (ref) => {
this.listRef = ref;
}
goToThread = (post) => {
const {actions} = this.props;
const channelId = post.channel_id;
@ -212,7 +216,7 @@ export default class FlaggedPosts extends PureComponent {
} else if (postIds.length) {
component = (
<FlatList
ref='list'
ref={this.setListRef}
contentContainerStyle={style.sectionList}
data={postIds}
keyExtractor={this.keyExtractor}

View file

@ -276,7 +276,7 @@ export default class Downloader extends PureComponent {
let path = res.path();
if (saveToCameraRoll) {
path = await CameraRoll.saveToCameraRoll(path, 'photo');
path = await CameraRoll.saveToCameraRoll(path, 'photo'); /* eslint-disable-line require-atomic-updates */
}
if (this.mounted) {

View file

@ -99,6 +99,14 @@ export default class ImagePreview extends PureComponent {
StatusBar.setHidden(false, 'fade');
}
setDocumentRef = (ref) => {
this.documents[this.state.index] = ref;
}
setDownloaderRef = (ref) => {
this.downloaderRef = ref;
}
animateOpenAnimToValue = (toValue, onComplete) => {
Animated.timing(this.openAnim, {
...ANIM_CONFIG,
@ -194,9 +202,7 @@ export default class ImagePreview extends PureComponent {
return (
<View style={[style.flex, style.center]}>
<FileAttachmentDocument
ref={(ref) => {
this.documents[this.state.index] = ref;
}}
ref={this.setDocumentRef}
backgroundColor='transparent'
canDownloadFiles={canDownloadFiles}
file={file}
@ -278,7 +284,7 @@ export default class ImagePreview extends PureComponent {
return (
<Downloader
ref='downloader'
ref={this.setDownloaderRef}
show={this.state.showDownloader}
file={file}
deviceHeight={deviceHeight}
@ -414,9 +420,9 @@ export default class ImagePreview extends PureComponent {
saveVideoIOS = () => {
const file = this.getCurrentFile();
if (this.refs.downloader) {
if (this.downloaderRef) {
EventEmitter.emit(NavigationTypes.NAVIGATION_CLOSE_MODAL);
this.refs.downloader.saveVideo(getLocalFilePathFromFile(VIDEOS_PATH, file));
this.downloaderRef.saveVideo(getLocalFilePathFromFile(VIDEOS_PATH, file));
}
};

View file

@ -68,6 +68,14 @@ export default class VideoPreview extends PureComponent {
EventEmitter.off('stop-video-playback', this.stopPlayback);
}
setVideoPlayerRef = (ref) => {
this.videoPlayerRef = ref;
}
setControlsRef = (ref) => {
this.controlsRef = ref;
}
async initializeComponent() {
const {file} = this.props;
const prefix = Platform.OS === 'android' ? 'file:/' : '';
@ -210,7 +218,7 @@ export default class VideoPreview extends PureComponent {
onPress={() => this.refs.controls.fadeInControls()}
>
<Video
ref='videoPlayer'
ref={this.setVideoPlayerRef}
style={[StyleSheet.absoluteFill, {position: 'absolute'}]}
resizeMode='contain'
source={{uri: path}}
@ -224,7 +232,7 @@ export default class VideoPreview extends PureComponent {
/>
</TouchableOpacity>
<VideoControls
ref='controls'
ref={this.setControlsRef}
mainColor={theme.linkColor}
playerState={playerState}
isFullScreen={isFullScreen}

View file

@ -30,6 +30,7 @@ exports[`InteractiveDialog should display introduction text if present 1`] = `
"dndIndicator": "#f74343",
"errorTextColor": "#fd5960",
"linkColor": "#2389d7",
"mentionBg": "#ffffff",
"mentionBj": "#ffffff",
"mentionColor": "#145dbf",
"mentionHighlightBg": "#ffe577",

View file

@ -0,0 +1,349 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`LongPost should match snapshot 1`] = `
LongPost {
"context": Object {
"intl": Object {
"defaultFormats": Object {},
"defaultLocale": "en",
"formatDate": [Function],
"formatHTMLMessage": [Function],
"formatMessage": [Function],
"formatNumber": [Function],
"formatPlural": [Function],
"formatRelative": [Function],
"formatTime": [Function],
"formats": Object {},
"formatters": Object {
"getDateTimeFormat": [Function],
"getMessageFormat": [Function],
"getNumberFormat": [Function],
"getPluralFormat": [Function],
"getRelativeFormat": [Function],
},
"locale": "en",
"messages": Object {},
"now": [Function],
"onError": [Function],
"textComponent": "span",
"timeZone": null,
},
},
"goToThread": [Function],
"handleClose": [Function],
"handlePress": [Function],
"navigationEventListener": undefined,
"props": Object {
"actions": Object {
"loadThreadIfNecessary": [MockFunction],
"selectPost": [MockFunction],
},
"fileIds": Array [],
"intl": Object {
"defaultFormats": Object {},
"defaultLocale": "en",
"formatDate": [Function],
"formatHTMLMessage": [Function],
"formatMessage": [Function],
"formatNumber": [Function],
"formatPlural": [Function],
"formatRelative": [Function],
"formatTime": [Function],
"formats": Object {},
"formatters": Object {
"getDateTimeFormat": [Function],
"getMessageFormat": [Function],
"getNumberFormat": [Function],
"getPluralFormat": [Function],
"getRelativeFormat": [Function],
},
"locale": "en",
"messages": Object {},
"now": [Function],
"onError": [Function],
"textComponent": "span",
"timeZone": null,
},
"isLandscape": false,
"postId": "post-id",
"theme": Object {
"awayIndicator": "#ffbc42",
"buttonBg": "#166de0",
"buttonColor": "#ffffff",
"centerChannelBg": "#ffffff",
"centerChannelColor": "#3d3c40",
"codeTheme": "github",
"dndIndicator": "#f74343",
"errorTextColor": "#fd5960",
"linkColor": "#2389d7",
"mentionBg": "#ffffff",
"mentionBj": "#ffffff",
"mentionColor": "#145dbf",
"mentionHighlightBg": "#ffe577",
"mentionHighlightLink": "#166de0",
"newMessageSeparator": "#ff8800",
"onlineIndicator": "#06d6a0",
"sidebarBg": "#145dbf",
"sidebarHeaderBg": "#1153ab",
"sidebarHeaderTextColor": "#ffffff",
"sidebarText": "#ffffff",
"sidebarTextActiveBorder": "#579eff",
"sidebarTextActiveColor": "#ffffff",
"sidebarTextHoverBg": "#4578bf",
"sidebarUnreadText": "#ffffff",
"type": "Mattermost",
},
},
"refs": Object {},
"setState": [Function],
"setViewRef": [Function],
"state": null,
"updater": Updater {
"_callbacks": Array [],
"_renderer": ReactShallowRenderer {
"_context": Object {
"intl": Object {
"defaultFormats": Object {},
"defaultLocale": "en",
"formatDate": [Function],
"formatHTMLMessage": [Function],
"formatMessage": [Function],
"formatNumber": [Function],
"formatPlural": [Function],
"formatRelative": [Function],
"formatTime": [Function],
"formats": Object {},
"formatters": Object {
"getDateTimeFormat": [Function],
"getMessageFormat": [Function],
"getNumberFormat": [Function],
"getPluralFormat": [Function],
"getRelativeFormat": [Function],
},
"locale": "en",
"messages": Object {},
"now": [Function],
"onError": [Function],
"textComponent": "span",
"timeZone": null,
},
},
"_didScheduleRenderPhaseUpdate": false,
"_dispatcher": Object {
"readContext": [Function],
"useCallback": [Function],
"useContext": [Function],
"useDebugValue": [Function],
"useEffect": [Function],
"useImperativeHandle": [Function],
"useLayoutEffect": [Function],
"useMemo": [Function],
"useReducer": [Function],
"useRef": [Function],
"useResponder": [Function],
"useState": [Function],
},
"_element": <LongPost
actions={
Object {
"loadThreadIfNecessary": [MockFunction],
"selectPost": [MockFunction],
}
}
fileIds={Array []}
intl={
Object {
"defaultFormats": Object {},
"defaultLocale": "en",
"formatDate": [Function],
"formatHTMLMessage": [Function],
"formatMessage": [Function],
"formatNumber": [Function],
"formatPlural": [Function],
"formatRelative": [Function],
"formatTime": [Function],
"formats": Object {},
"formatters": Object {
"getDateTimeFormat": [Function],
"getMessageFormat": [Function],
"getNumberFormat": [Function],
"getPluralFormat": [Function],
"getRelativeFormat": [Function],
},
"locale": "en",
"messages": Object {},
"now": [Function],
"onError": [Function],
"textComponent": "span",
"timeZone": null,
}
}
isLandscape={false}
postId="post-id"
theme={
Object {
"awayIndicator": "#ffbc42",
"buttonBg": "#166de0",
"buttonColor": "#ffffff",
"centerChannelBg": "#ffffff",
"centerChannelColor": "#3d3c40",
"codeTheme": "github",
"dndIndicator": "#f74343",
"errorTextColor": "#fd5960",
"linkColor": "#2389d7",
"mentionBg": "#ffffff",
"mentionBj": "#ffffff",
"mentionColor": "#145dbf",
"mentionHighlightBg": "#ffe577",
"mentionHighlightLink": "#166de0",
"newMessageSeparator": "#ff8800",
"onlineIndicator": "#06d6a0",
"sidebarBg": "#145dbf",
"sidebarHeaderBg": "#1153ab",
"sidebarHeaderTextColor": "#ffffff",
"sidebarText": "#ffffff",
"sidebarTextActiveBorder": "#579eff",
"sidebarTextActiveColor": "#ffffff",
"sidebarTextHoverBg": "#4578bf",
"sidebarUnreadText": "#ffffff",
"type": "Mattermost",
}
}
/>,
"_firstWorkInProgressHook": null,
"_forcedUpdate": false,
"_instance": [Circular],
"_isReRender": false,
"_newState": null,
"_numberOfReRenders": 0,
"_renderPhaseUpdates": null,
"_rendered": <Connect(SafeAreaIos)
backgroundColor="transparent"
excludeHeader={true}
footerColor="transparent"
forceTop={44}
>
<View
style={
Array [
Object {
"flex": 1,
"marginTop": 20,
},
null,
]
}
>
<withAnimatable(View)
animation="zoomIn"
delay={0}
direction="normal"
duration={200}
iterationCount={1}
iterationDelay={0}
onAnimationBegin={[Function]}
onAnimationEnd={[Function]}
onTransitionBegin={[Function]}
onTransitionEnd={[Function]}
style={
Object {
"backgroundColor": "#ffffff",
"borderRadius": 6,
"flex": 1,
"margin": 10,
"opacity": 0,
}
}
useNativeDriver={true}
>
<View
style={
Object {
"alignItems": "center",
"backgroundColor": "#ffffff",
"borderBottomColor": "rgba(61,60,64,0.2)",
"borderBottomWidth": 1,
"borderTopLeftRadius": 6,
"borderTopRightRadius": 6,
"flexDirection": "row",
"height": 44,
"paddingRight": 16,
"width": "100%",
}
}
>
<TouchableOpacity
activeOpacity={0.2}
onPress={[Function]}
style={
Object {
"height": 44,
"justifyContent": "center",
"paddingLeft": 7,
"width": 40,
}
}
>
<Icon
allowFontScaling={false}
color="#3d3c40"
name="close"
size={20}
/>
</TouchableOpacity>
<View
style={
Object {
"alignItems": "center",
"flex": 1,
"paddingRight": 40,
}
}
>
<FormattedText
defaultMessage="{channelName} - Post"
ellipsizeMode="tail"
id="mobile.long_post_title"
numberOfLines={1}
style={
Object {
"color": "#3d3c40",
"fontSize": 17,
"fontWeight": "600",
}
}
values={
Object {
"channelName": undefined,
}
}
/>
</View>
</View>
<ScrollViewMock
style={
Object {
"backgroundColor": "#ffffff",
"flex": 1,
}
}
>
<Connect(Post)
isSearchResult={false}
onPress={[Function]}
postId="post-id"
shouldRenderReplyButton={false}
showAddReaction={false}
showLongPost={true}
/>
</ScrollViewMock>
</withAnimatable(View)>
</View>
</Connect(SafeAreaIos)>,
"_rendering": false,
"_updater": [Circular],
"_workInProgressHook": null,
},
},
}
`;

View file

@ -6,7 +6,7 @@ import {connect} from 'react-redux';
import {selectPost} from 'mattermost-redux/actions/posts';
import {makeGetChannel} from 'mattermost-redux/selectors/entities/channels';
import {getPost, makeGetReactionsForPost} from 'mattermost-redux/selectors/entities/posts';
import {getPost} from 'mattermost-redux/selectors/entities/posts';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {loadThreadIfNecessary} from 'app/actions/views/channel';
@ -15,16 +15,13 @@ import LongPost from './long_post';
function makeMapStateToProps() {
const getChannel = makeGetChannel();
const getReactionsForPost = makeGetReactionsForPost();
return function mapStateToProps(state, ownProps) {
const post = getPost(state, ownProps.postId);
const channel = post ? getChannel(state, {id: post.channel_id}) : null;
const reactions = getReactionsForPost(state, post.id);
return {
channelName: channel ? channel.display_name : '',
hasReactions: (reactions && Object.keys(reactions).length > 0) || Boolean(post.has_reactions),
inThreadView: Boolean(state.entities.posts.selectedPostId),
fileIds: post ? post.file_ids : false,
theme: getTheme(state),

View file

@ -16,7 +16,6 @@ import {Navigation} from 'react-native-navigation';
import FileAttachmentList from 'app/components/file_attachment_list';
import FormattedText from 'app/components/formatted_text';
import Post from 'app/components/post';
import Reactions from 'app/components/reactions';
import SafeAreaView from 'app/components/safe_area_view';
import {marginHorizontal as margin} from 'app/components/safe_area_view/iphone_x_spacing';
import {emptyFunction} from 'app/utils/general';
@ -49,7 +48,6 @@ export default class LongPost extends PureComponent {
}).isRequired,
channelName: PropTypes.string,
fileIds: PropTypes.array,
hasReactions: PropTypes.bool,
isPermalink: PropTypes.bool,
inThreadView: PropTypes.bool,
managedConfig: PropTypes.object,
@ -78,6 +76,10 @@ export default class LongPost extends PureComponent {
}
}
setViewRef = (ref) => {
this.viewRef = ref;
}
goToThread = preventDoubleTap((post) => {
const {actions} = this.props;
const channelId = post.channel_id;
@ -96,8 +98,8 @@ export default class LongPost extends PureComponent {
});
handleClose = () => {
if (this.refs.view) {
this.refs.view.zoomOut().then(() => {
if (this.viewRef) {
this.viewRef.zoomOut().then(() => {
dismissModal();
});
}
@ -136,28 +138,10 @@ export default class LongPost extends PureComponent {
return attachments;
}
renderReactions = (style) => {
const {hasReactions, postId} = this.props;
if (!hasReactions) {
return null;
}
return (
<View style={style.reactions}>
<Reactions
position='left'
postId={postId}
/>
</View>
);
};
render() {
const {
channelName,
fileIds,
hasReactions,
managedConfig,
onHashtagPress,
onPermalinkPress,
@ -168,11 +152,10 @@ export default class LongPost extends PureComponent {
const style = getStyleSheet(theme);
let footer;
if (hasReactions || fileIds.length) {
if (fileIds.length) {
footer = (
<View style={style.footer}>
{this.renderFileAttachments(style)}
{this.renderReactions(style)}
</View>
);
}
@ -186,7 +169,7 @@ export default class LongPost extends PureComponent {
>
<View style={[style.container, margin(isLandscape)]}>
<Animatable.View
ref='view'
ref={this.setViewRef}
animation='zoomIn'
duration={200}
delay={0}
@ -222,6 +205,7 @@ export default class LongPost extends PureComponent {
onPress={this.handlePress}
isSearchResult={false}
showLongPost={true}
showAddReaction={false}
onHashtagPress={onHashtagPress}
onPermalinkPress={onPermalinkPress}
managedConfig={managedConfig}
@ -297,9 +281,5 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
height: 95,
width: '100%',
},
reactions: {
height: 47,
width: '100%',
},
};
});

View file

@ -0,0 +1,32 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import Preferences from 'mattermost-redux/constants/preferences';
import {shallowWithIntl} from 'test/intl-test-helper';
import LongPost from './long_post';
jest.mock('react-native-doc-viewer', () => ({
openDoc: jest.fn(),
}));
describe('LongPost', () => {
const baseProps = {
actions: {
loadThreadIfNecessary: jest.fn(),
selectPost: jest.fn(),
},
postId: 'post-id',
theme: Preferences.THEMES.default,
isLandscape: false,
};
test('should match snapshot', () => {
const wrapper = shallowWithIntl(<LongPost {...baseProps}/>);
expect(wrapper.instance()).toMatchSnapshot();
});
});

View file

@ -96,6 +96,7 @@ exports[`MoreChannels should match snapshot 1`] = `
"dndIndicator": "#f74343",
"errorTextColor": "#fd5960",
"linkColor": "#2389d7",
"mentionBg": "#ffffff",
"mentionBj": "#ffffff",
"mentionColor": "#145dbf",
"mentionHighlightBg": "#ffe577",

View file

@ -125,6 +125,10 @@ export default class MoreChannels extends PureComponent {
}
}
setSearchBarRef = (ref) => {
this.searchBarRef = ref;
}
cancelSearch = () => {
const {channels} = this.props;
@ -340,7 +344,7 @@ export default class MoreChannels extends PureComponent {
<React.Fragment>
<View style={[style.searchBar, padding(isLandscape)]}>
<SearchBar
ref='search_bar'
ref={this.setSearchBarRef}
placeholder={formatMessage({id: 'search_bar.search', defaultMessage: 'Search'})}
cancelTitle={formatMessage({id: 'mobile.post.cancel', defaultMessage: 'Cancel'})}
backgroundColor='transparent'

View file

@ -113,6 +113,10 @@ export default class MoreDirectMessages extends PureComponent {
}
}
setSearchBarRef = (ref) => {
this.searchBarRef = ref;
}
close = () => {
dismissModal();
};
@ -460,7 +464,7 @@ export default class MoreDirectMessages extends PureComponent {
<StatusBar/>
<View style={[style.searchBar, padding(isLandscape)]}>
<SearchBar
ref='search_bar'
ref={this.setSearchBarRef}
placeholder={formatMessage({id: 'search_bar.search', defaultMessage: 'Search'})}
cancelTitle={formatMessage({id: 'mobile.post.cancel', defaultMessage: 'Cancel'})}
backgroundColor='transparent'

View file

@ -32,6 +32,7 @@ exports[`SelectedUsers should match snapshot 1`] = `
"dndIndicator": "#f74343",
"errorTextColor": "#fd5960",
"linkColor": "#2389d7",
"mentionBg": "#ffffff",
"mentionBj": "#ffffff",
"mentionColor": "#145dbf",
"mentionHighlightBg": "#ffe577",
@ -109,6 +110,7 @@ exports[`SelectedUsers should match snapshot for no warning message 1`] = `
"dndIndicator": "#f74343",
"errorTextColor": "#fd5960",
"linkColor": "#2389d7",
"mentionBg": "#ffffff",
"mentionBj": "#ffffff",
"mentionColor": "#145dbf",
"mentionHighlightBg": "#ffe577",
@ -168,6 +170,7 @@ exports[`SelectedUsers should match snapshot to show warning for ability to add
"dndIndicator": "#f74343",
"errorTextColor": "#fd5960",
"linkColor": "#2389d7",
"mentionBg": "#ffffff",
"mentionBj": "#ffffff",
"mentionColor": "#145dbf",
"mentionHighlightBg": "#ffe577",
@ -205,6 +208,7 @@ exports[`SelectedUsers should match snapshot to show warning for ability to add
"dndIndicator": "#f74343",
"errorTextColor": "#fd5960",
"linkColor": "#2389d7",
"mentionBg": "#ffffff",
"mentionBj": "#ffffff",
"mentionColor": "#145dbf",
"mentionHighlightBg": "#ffe577",

View file

@ -178,6 +178,10 @@ export default class Permalink extends PureComponent {
}
}
setViewRef = (ref) => {
this.viewRef = ref;
}
goToThread = preventDoubleTap((post) => {
const {actions} = this.props;
const channelId = post.channel_id;
@ -197,9 +201,9 @@ export default class Permalink extends PureComponent {
handleClose = () => {
const {actions, onClose} = this.props;
if (this.refs.view) {
if (this.viewRef) {
this.mounted = false;
this.refs.view.zoomOut().then(() => {
this.viewRef.zoomOut().then(() => {
actions.selectPost('');
dismissModal();
@ -221,8 +225,8 @@ export default class Permalink extends PureComponent {
handlePress = () => {
const {channelIdState} = this.state;
if (this.refs.view) {
this.refs.view.growOut().then(() => {
if (this.viewRef) {
this.viewRef.growOut().then(() => {
this.jumpToChannel(channelIdState);
});
}
@ -398,7 +402,7 @@ export default class Permalink extends PureComponent {
style={[style.container, margin(isLandscape)]}
>
<Animatable.View
ref='view'
ref={this.setViewRef}
animation='zoomIn'
duration={200}
delay={0}

View file

@ -71,6 +71,10 @@ export default class PinnedPosts extends PureComponent {
}
}
setListRef = (ref) => {
this.listRef = ref;
}
goToThread = (post) => {
const {actions} = this.props;
const channelId = post.channel_id;
@ -209,7 +213,7 @@ export default class PinnedPosts extends PureComponent {
} else if (postIds.length) {
component = (
<FlatList
ref='list'
ref={this.setListRef}
contentContainerStyle={style.sectionList}
data={postIds}
keyExtractor={this.keyExtractor}

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