forked from Ivasoft/mattermost-mobile
Compare commits
19 Commits
voice-mess
...
v2.0.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
70119fc026 | ||
|
|
0d9c6e0ad3 | ||
|
|
f7d8ed9e1f | ||
|
|
b8cc13d7fa | ||
|
|
317568b4c8 | ||
|
|
55f919dd27 | ||
|
|
da1b3dc71d | ||
|
|
57a9ff31bf | ||
|
|
4f86a87bdc | ||
|
|
9ab21b2f62 | ||
|
|
1934945d72 | ||
|
|
5162e6b6e7 | ||
|
|
9139a26967 | ||
|
|
56fbb3d842 | ||
|
|
56349f865f | ||
|
|
fdf593bcec | ||
|
|
8e2e016a6c | ||
|
|
4d9bc1fbed | ||
|
|
7351c7ccac |
11
.gitignore
vendored
11
.gitignore
vendored
@@ -46,8 +46,6 @@ local.properties
|
||||
*.iml
|
||||
*.hprof
|
||||
.cxx/
|
||||
*.keystore
|
||||
!debug.keystore
|
||||
android/app/bin
|
||||
android/app/build
|
||||
android/build
|
||||
@@ -63,6 +61,12 @@ npm-debug.log
|
||||
yarn-error.log
|
||||
.yarninstall
|
||||
|
||||
# BUCK
|
||||
buck-out/
|
||||
\.buckd/
|
||||
android/app/libs
|
||||
*.keystore
|
||||
|
||||
# Vim
|
||||
[._]*.s[a-w][a-z]
|
||||
[._]s[a-w][a-z]
|
||||
@@ -110,6 +114,3 @@ launch.json
|
||||
|
||||
# Notice.txt generation
|
||||
!build/notice-file
|
||||
|
||||
# Temporary files created by Metro to check the health of the file watcher
|
||||
.metro-health-check*
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Submit feature requests to https://portal.productboard.com/mattermost/33-what-matters-to-you. File non-security related bugs here in the following format:
|
||||
Submit feature requests to http://www.mattermost.org/feature-requests/. File non-security related bugs here in the following format:
|
||||
|
||||
#### Summary
|
||||
Issue in one concise sentence.
|
||||
|
||||
65
android/app/BUCK
Normal file
65
android/app/BUCK
Normal file
@@ -0,0 +1,65 @@
|
||||
# To learn about Buck see [Docs](https://buckbuild.com/).
|
||||
# To run your application with Buck:
|
||||
# - install Buck
|
||||
# - `npm start` - to start the packager
|
||||
# - `cd android`
|
||||
# - `keytool -genkey -v -keystore keystores/debug.keystore -storepass android -alias androiddebugkey -keypass android -dname "CN=Android Debug,O=Android,C=US"`
|
||||
# - `./gradlew :app:copyDownloadableDepsToLibs` - make all Gradle compile dependencies available to Buck
|
||||
# - `buck install -r android/app` - compile, install and run application
|
||||
#
|
||||
|
||||
lib_deps = []
|
||||
|
||||
for jarfile in glob(['libs/*.jar']):
|
||||
name = 'jars__' + jarfile[jarfile.rindex('/') + 1: jarfile.rindex('.jar')]
|
||||
lib_deps.append(':' + name)
|
||||
prebuilt_jar(
|
||||
name = name,
|
||||
binary_jar = jarfile,
|
||||
)
|
||||
|
||||
for aarfile in glob(['libs/*.aar']):
|
||||
name = 'aars__' + aarfile[aarfile.rindex('/') + 1: aarfile.rindex('.aar')]
|
||||
lib_deps.append(':' + name)
|
||||
android_prebuilt_aar(
|
||||
name = name,
|
||||
aar = aarfile,
|
||||
)
|
||||
|
||||
android_library(
|
||||
name = "all-libs",
|
||||
exported_deps = lib_deps,
|
||||
)
|
||||
|
||||
android_library(
|
||||
name = "app-code",
|
||||
srcs = glob([
|
||||
"src/main/java/**/*.java",
|
||||
]),
|
||||
deps = [
|
||||
":all-libs",
|
||||
":build_config",
|
||||
":res",
|
||||
],
|
||||
)
|
||||
|
||||
android_build_config(
|
||||
name = "build_config",
|
||||
package = "com.mattermost.rnbeta",
|
||||
)
|
||||
|
||||
android_resource(
|
||||
name = "res",
|
||||
package = "com.mattermost.rnbeta",
|
||||
res = "src/main/res",
|
||||
)
|
||||
|
||||
android_binary(
|
||||
name = "app",
|
||||
keystore = "//android/keystores:debug",
|
||||
manifest = "src/main/AndroidManifest.xml",
|
||||
package_type = "debug",
|
||||
deps = [
|
||||
":app-code",
|
||||
],
|
||||
)
|
||||
@@ -1,56 +1,88 @@
|
||||
apply plugin: "com.android.application"
|
||||
apply plugin: "com.facebook.react"
|
||||
apply plugin: 'kotlin-android'
|
||||
import com.android.build.OutputFile
|
||||
|
||||
import org.apache.tools.ant.taskdefs.condition.Os
|
||||
|
||||
/**
|
||||
* This is the configuration block to customize your React Native Android app.
|
||||
* By default you don't need to apply any configuration, just uncomment the lines you need.
|
||||
* The react.gradle file registers a task for each build variant (e.g. bundleDebugJsAndAssets
|
||||
* and bundleReleaseJsAndAssets).
|
||||
* These basically call `react-native bundle` with the correct arguments during the Android build
|
||||
* cycle. By default, bundleDebugJsAndAssets is skipped, as in debug/dev mode we prefer to load the
|
||||
* bundle directly from the development server. Below you can see all the possible configurations
|
||||
* and their defaults. If you decide to add a configuration block, make sure to add it before the
|
||||
* `apply from: "../../node_modules/react-native/react.gradle"` line.
|
||||
*
|
||||
* project.ext.react = [
|
||||
* // the name of the generated asset file containing your JS bundle
|
||||
* bundleAssetName: "index.android.bundle",
|
||||
*
|
||||
* // the entry file for bundle generation. If none specified and
|
||||
* // "index.android.js" exists, it will be used. Otherwise "index.js" is
|
||||
* // default. Can be overridden with ENTRY_FILE environment variable.
|
||||
* entryFile: "index.android.js",
|
||||
*
|
||||
* // whether to bundle JS and assets in debug mode
|
||||
* bundleInDebug: false,
|
||||
*
|
||||
* // whether to bundle JS and assets in release mode
|
||||
* bundleInRelease: true,
|
||||
*
|
||||
* // whether to bundle JS and assets in another build variant (if configured).
|
||||
* // See http://tools.android.com/tech-docs/new-build-system/user-guide#TOC-Build-Variants
|
||||
* // The configuration property can be in the following formats
|
||||
* // 'bundleIn${productFlavor}${buildType}'
|
||||
* // 'bundleIn${buildType}'
|
||||
* // bundleInFreeDebug: true,
|
||||
* // bundleInPaidRelease: true,
|
||||
* // bundleInBeta: true,
|
||||
*
|
||||
* // whether to disable dev mode in custom build variants (by default only disabled in release)
|
||||
* // for example: to disable dev mode in the staging build type (if configured)
|
||||
* devDisabledInStaging: true,
|
||||
* // The configuration property can be in the following formats
|
||||
* // 'devDisabledIn${productFlavor}${buildType}'
|
||||
* // 'devDisabledIn${buildType}'
|
||||
*
|
||||
* // the root of your project, i.e. where "package.json" lives
|
||||
* root: "../../",
|
||||
*
|
||||
* // where to put the JS bundle asset in debug mode
|
||||
* jsBundleDirDebug: "$buildDir/intermediates/assets/debug",
|
||||
*
|
||||
* // where to put the JS bundle asset in release mode
|
||||
* jsBundleDirRelease: "$buildDir/intermediates/assets/release",
|
||||
*
|
||||
* // where to put drawable resources / React Native assets, e.g. the ones you use via
|
||||
* // require('./image.png')), in debug mode
|
||||
* resourcesDirDebug: "$buildDir/intermediates/res/merged/debug",
|
||||
*
|
||||
* // where to put drawable resources / React Native assets, e.g. the ones you use via
|
||||
* // require('./image.png')), in release mode
|
||||
* resourcesDirRelease: "$buildDir/intermediates/res/merged/release",
|
||||
*
|
||||
* // by default the gradle tasks are skipped if none of the JS files or assets change; this means
|
||||
* // that we don't look at files in android/ or ios/ to determine whether the tasks are up to
|
||||
* // date; if you have any other folders that you want to ignore for performance reasons (gradle
|
||||
* // indexes the entire tree), add them here. Alternatively, if you have JS files in android/
|
||||
* // for example, you might want to remove it from here.
|
||||
* inputExcludes: ["android/**", "ios/**"],
|
||||
*
|
||||
* // override which node gets called and with what additional arguments
|
||||
* nodeExecutableAndArgs: ["node"],
|
||||
*
|
||||
* // supply additional arguments to the packager
|
||||
* extraPackagerArgs: []
|
||||
* ]
|
||||
*/
|
||||
|
||||
react {
|
||||
/* Folders */
|
||||
// The root of your project, i.e. where "package.json" lives. Default is '..'
|
||||
// root = file("../")
|
||||
// The folder where the react-native NPM package is. Default is ../node_modules/react-native
|
||||
// reactNativeDir = file("../node_modules/react-native")
|
||||
// The folder where the react-native Codegen package is. Default is ../node_modules/react-native-codegen
|
||||
// codegenDir = file("../node_modules/react-native-codegen")
|
||||
// The cli.js file which is the React Native CLI entrypoint. Default is ../node_modules/react-native/cli.js
|
||||
// cliFile = file("../node_modules/react-native/cli.js")
|
||||
/* Variants */
|
||||
// The list of variants to that are debuggable. For those we're going to
|
||||
// skip the bundling of the JS bundle and the assets. By default is just 'debug'.
|
||||
// If you add flavors like lite, prod, etc. you'll have to list your debuggableVariants.
|
||||
// debuggableVariants = ["liteDebug", "prodDebug"]
|
||||
/* Bundling */
|
||||
// A list containing the node command and its flags. Default is just 'node'.
|
||||
// nodeExecutableAndArgs = ["node"]
|
||||
//
|
||||
// The command to run when bundling. By default is 'bundle'
|
||||
// bundleCommand = "ram-bundle"
|
||||
//
|
||||
// The path to the CLI configuration file. Default is empty.
|
||||
// bundleConfig = file(../rn-cli.config.js)
|
||||
//
|
||||
// The name of the generated asset file containing your JS bundle
|
||||
// bundleAssetName = "MyApplication.android.bundle"
|
||||
//
|
||||
// The entry file for bundle generation. Default is 'index.android.js' or 'index.js'
|
||||
entryFile = file("../../index.ts")
|
||||
//
|
||||
// A list of extra flags to pass to the 'bundle' commands.
|
||||
// See https://github.com/react-native-community/cli/blob/main/docs/commands.md#bundle
|
||||
// extraPackagerArgs = []
|
||||
/* Hermes Commands */
|
||||
// The hermes compiler command to run. By default it is 'hermesc'
|
||||
// hermesCommand = "$rootDir/my-custom-hermesc/bin/hermesc"
|
||||
//
|
||||
// The list of flags to pass to the Hermes compiler. By default is "-O", "-output-source-map"
|
||||
// hermesFlags = ["-O", "-output-source-map"]
|
||||
}
|
||||
project.ext.react = [
|
||||
entryFile: "index.ts",
|
||||
bundleConfig: "metro.config.js",
|
||||
bundleCommand: "bundle",
|
||||
enableHermes: true,
|
||||
]
|
||||
|
||||
apply from: "../../node_modules/react-native/react.gradle"
|
||||
apply from: "../../node_modules/react-native-vector-icons/fonts.gradle"
|
||||
|
||||
if (System.getenv("SENTRY_ENABLED") == "true") {
|
||||
@@ -63,35 +95,33 @@ if (System.getenv("SENTRY_ENABLED") == "true") {
|
||||
}
|
||||
|
||||
/**
|
||||
* Set this to true to create four separate APKs instead of one,
|
||||
* one for each native architecture. This is useful if you don't
|
||||
* use App Bundles (https://developer.android.com/guide/app-bundle/)
|
||||
* and want to have separate APKs to upload to the Play Store
|
||||
* Set this to true to create two separate APKs instead of one:
|
||||
* - An APK that only works on ARM devices
|
||||
* - An APK that only works on x86 devices
|
||||
* The advantage is the size of the APK is reduced by about 4MB.
|
||||
* Upload all the APKs to the Play Store and people will download
|
||||
* the correct one based on the CPU architecture of their device.
|
||||
*/
|
||||
def enableSeparateBuildPerCPUArchitecture = project.hasProperty('separateApk') ? project.property('separateApk').toBoolean() : false
|
||||
|
||||
/**
|
||||
* Set this to true to Run Proguard on Release builds to minify the Java bytecode.
|
||||
* Run Proguard to shrink the Java bytecode in release builds.
|
||||
*/
|
||||
def enableProguardInReleaseBuilds = false
|
||||
|
||||
/**
|
||||
* The preferred build flavor of JavaScriptCore (JSC)
|
||||
*
|
||||
* 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:+'
|
||||
|
||||
/**
|
||||
* Private function to get the list of Native Architectures you want to build.
|
||||
* This reads the value from reactNativeArchitectures in your gradle.properties
|
||||
* file and works together with the --active-arch-only flag of react-native run-android.
|
||||
* Whether to enable the Hermes VM.
|
||||
*
|
||||
* This should be set on project.ext.react and that value will be read 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);
|
||||
|
||||
/**
|
||||
* Architectures to build native code for.
|
||||
*/
|
||||
def reactNativeArchitectures() {
|
||||
def value = project.getProperties().get("reactNativeArchitectures")
|
||||
@@ -101,21 +131,84 @@ def reactNativeArchitectures() {
|
||||
android {
|
||||
ndkVersion rootProject.ext.ndkVersion
|
||||
compileSdkVersion rootProject.ext.compileSdkVersion
|
||||
namespace "com.mattermost.rnbeta"
|
||||
|
||||
lintOptions {
|
||||
checkReleaseBuilds false
|
||||
abortOnError false
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
pickFirst '**/libc++_shared.so'
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.mattermost.rnbeta"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 453
|
||||
versionCode 452
|
||||
versionName "2.0.0"
|
||||
testBuildType System.getProperty('testBuildType', 'debug')
|
||||
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
||||
buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString()
|
||||
|
||||
if (isNewArchitectureEnabled()) {
|
||||
// We configure the CMake build only if you decide to opt-in for the New Architecture.
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
arguments "-DPROJECT_BUILD_DIR=$buildDir",
|
||||
"-DREACT_ANDROID_DIR=$rootDir/../node_modules/react-native/ReactAndroid",
|
||||
"-DREACT_ANDROID_BUILD_DIR=$rootDir/../node_modules/react-native/ReactAndroid/build",
|
||||
"-DNODE_MODULES_DIR=$rootDir/../node_modules",
|
||||
"-DANDROID_STL=c++_shared"
|
||||
}
|
||||
}
|
||||
if (!enableSeparateBuildPerCPUArchitecture) {
|
||||
ndk {
|
||||
abiFilters (*reactNativeArchitectures())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isNewArchitectureEnabled()) {
|
||||
// We configure the NDK build only if you decide to opt-in for the New Architecture.
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
path "$projectDir/src/main/jni/CMakeLists.txt"
|
||||
}
|
||||
}
|
||||
def reactAndroidProjectDir = project(':ReactAndroid').projectDir
|
||||
def packageReactNdkDebugLibs = tasks.register("packageReactNdkDebugLibs", Copy) {
|
||||
dependsOn(":ReactAndroid:packageReactNdkDebugLibsForBuck")
|
||||
from("$reactAndroidProjectDir/src/main/jni/prebuilt/lib")
|
||||
into("$buildDir/react-ndk/exported")
|
||||
}
|
||||
def packageReactNdkReleaseLibs = tasks.register("packageReactNdkReleaseLibs", Copy) {
|
||||
dependsOn(":ReactAndroid:packageReactNdkReleaseLibsForBuck")
|
||||
from("$reactAndroidProjectDir/src/main/jni/prebuilt/lib")
|
||||
into("$buildDir/react-ndk/exported")
|
||||
}
|
||||
afterEvaluate {
|
||||
// If you wish to add a custom TurboModule or component locally,
|
||||
// you should uncomment this line.
|
||||
// preBuild.dependsOn("generateCodegenArtifactsFromSchema")
|
||||
preDebugBuild.dependsOn(packageReactNdkDebugLibs)
|
||||
preReleaseBuild.dependsOn(packageReactNdkReleaseLibs)
|
||||
|
||||
// Due to a bug inside AGP, we have to explicitly set a dependency
|
||||
// between configureCMakeDebug* tasks and the preBuild tasks.
|
||||
// This can be removed once this is solved: https://issuetracker.google.com/issues/207403732
|
||||
configureCMakeDebugRelease.dependsOn(preReleaseBuild)
|
||||
configureCMakeDebugDebug.dependsOn(preDebugBuild)
|
||||
reactNativeArchitectures().each { architecture ->
|
||||
tasks.findByName("configureCMakeDebugDebug[${architecture}]")?.configure {
|
||||
dependsOn("preDebugBuild")
|
||||
}
|
||||
tasks.findByName("configureCMakeDebugRelease[${architecture}]")?.configure {
|
||||
dependsOn("preReleaseBuild")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
@@ -188,36 +281,70 @@ repositories {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// The version of react-native is set by the React Native Gradle Plugin
|
||||
implementation("com.facebook.react:react-android")
|
||||
implementation fileTree(dir: "libs", include: ["*.jar"])
|
||||
|
||||
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.0.0")
|
||||
//noinspection GradleDynamicVersio
|
||||
implementation "com.facebook.react:react-native:+" // From node_modules
|
||||
|
||||
debugImplementation("com.facebook.flipper:flipper:${FLIPPER_VERSION}")
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2'
|
||||
|
||||
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.0.0"
|
||||
|
||||
debugImplementation("com.facebook.flipper:flipper:${FLIPPER_VERSION}") {
|
||||
exclude group:'com.facebook.fbjni'
|
||||
}
|
||||
debugImplementation("com.facebook.flipper:flipper-network-plugin:${FLIPPER_VERSION}") {
|
||||
exclude group:'com.squareup.okhttp3', module:'okhttp'
|
||||
exclude group:'com.facebook.flipper'
|
||||
}
|
||||
debugImplementation("com.facebook.flipper:flipper-fresco-plugin:${FLIPPER_VERSION}") {
|
||||
exclude group:'com.facebook.flipper'
|
||||
}
|
||||
|
||||
debugImplementation("com.facebook.flipper:flipper-fresco-plugin:${FLIPPER_VERSION}")
|
||||
if (hermesEnabled.toBoolean()) {
|
||||
implementation("com.facebook.react:hermes-android")
|
||||
|
||||
if (enableHermes) {
|
||||
//noinspection GradleDynamicVersion
|
||||
implementation("com.facebook.react:hermes-engine:+") { // From node_modules
|
||||
exclude group:'com.facebook.fbjni'
|
||||
}
|
||||
} else {
|
||||
implementation jscFlavor
|
||||
}
|
||||
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2'
|
||||
implementation 'androidx.appcompat:appcompat:1.4.1'
|
||||
implementation 'com.google.android.material:material:1.0.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
||||
|
||||
implementation project(':reactnativenotifications')
|
||||
implementation "com.google.firebase:firebase-messaging:$firebaseVersion"
|
||||
|
||||
// For animated GIF support
|
||||
implementation 'com.facebook.fresco:fresco:2.6.0'
|
||||
implementation 'com.facebook.fresco:animated-gif:2.6.0'
|
||||
// For WebP support, including animated WebP
|
||||
implementation 'com.facebook.fresco:animated-webp:2.6.0'
|
||||
implementation 'com.facebook.fresco:webpsupport:2.6.0'
|
||||
|
||||
androidTestImplementation('com.wix:detox:+')
|
||||
implementation project(':reactnativenotifications')
|
||||
|
||||
implementation project(':watermelondb-jsi')
|
||||
}
|
||||
|
||||
configurations.all {
|
||||
if (isNewArchitectureEnabled()) {
|
||||
// If new architecture is enabled, we let you build RN from source
|
||||
// Otherwise we fallback to a prebuilt .aar bundled in the NPM package.
|
||||
// This will be applied to all the imported transtitive dependency.
|
||||
resolutionStrategy.dependencySubstitution {
|
||||
substitute(module("com.facebook.react:react-native"))
|
||||
.using(project(":ReactAndroid"))
|
||||
.because("On New Architecture we're building React Native from source")
|
||||
substitute(module("com.facebook.react:hermes-engine"))
|
||||
.using(project(":ReactAndroid:hermes-engine"))
|
||||
.because("On New Architecture we're building Hermes from source")
|
||||
}
|
||||
}
|
||||
resolutionStrategy {
|
||||
force "com.facebook.soloader:soloader:0.10.1"
|
||||
eachDependency { DependencyResolveDetails details ->
|
||||
if (details.requested.name == 'play-services-base') {
|
||||
details.useTarget group: details.requested.group, name: details.requested.name, version: '15.0.1'
|
||||
@@ -232,13 +359,13 @@ configurations.all {
|
||||
details.useTarget group: details.requested.group, name: details.requested.name, version: '15.0.1'
|
||||
}
|
||||
if (details.requested.name == 'okhttp') {
|
||||
details.useTarget group: details.requested.group, name: details.requested.name, version: '4.10.0'
|
||||
details.useTarget group: details.requested.group, name: details.requested.name, version: '4.9.2'
|
||||
}
|
||||
if (details.requested.name == 'okhttp-tls') {
|
||||
details.useTarget group: details.requested.group, name: details.requested.name, version: '4.10.0'
|
||||
details.useTarget group: details.requested.group, name: details.requested.name, version: '4.9.2'
|
||||
}
|
||||
if (details.requested.name == 'okhttp-urlconnection') {
|
||||
details.useTarget group: details.requested.group, name: details.requested.name, version: '4.10.0'
|
||||
details.useTarget group: details.requested.group, name: details.requested.name, version: '4.9.2'
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -253,3 +380,11 @@ task copyDownloadableDepsToLibs(type: Copy) {
|
||||
|
||||
apply plugin: 'com.google.gms.google-services'
|
||||
apply from: file("../../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesAppBuildGradle(project)
|
||||
|
||||
def isNewArchitectureEnabled() {
|
||||
// To opt-in for the New Architecture, you can either:
|
||||
// - Set `newArchEnabled` to true inside the `gradle.properties` file
|
||||
// - Invoke gradle with `-newArchEnabled=true`
|
||||
// - Set an environment variable `ORG_GRADLE_PROJECT_newArchEnabled=true`
|
||||
return project.hasProperty("newArchEnabled") && project.newArchEnabled == "true"
|
||||
}
|
||||
|
||||
Binary file not shown.
@@ -4,7 +4,7 @@
|
||||
* <p>This source code is licensed under the MIT license found in the LICENSE file in the root
|
||||
* directory of this source tree.
|
||||
*/
|
||||
package com.mattermost.flipper;
|
||||
package com.rn;
|
||||
|
||||
import android.content.Context;
|
||||
import com.facebook.flipper.android.AndroidFlipperClient;
|
||||
@@ -17,22 +17,19 @@ import com.facebook.flipper.plugins.inspector.DescriptorMapping;
|
||||
import com.facebook.flipper.plugins.inspector.InspectorFlipperPlugin;
|
||||
import com.facebook.flipper.plugins.network.FlipperOkhttpInterceptor;
|
||||
import com.facebook.flipper.plugins.network.NetworkFlipperPlugin;
|
||||
import com.facebook.flipper.plugins.react.ReactFlipperPlugin;
|
||||
import com.facebook.flipper.plugins.sharedpreferences.SharedPreferencesFlipperPlugin;
|
||||
import com.facebook.react.ReactInstanceEventListener;
|
||||
import com.facebook.react.ReactInstanceManager;
|
||||
import com.facebook.react.bridge.ReactContext;
|
||||
import com.facebook.react.modules.network.NetworkingModule;
|
||||
import okhttp3.OkHttpClient;
|
||||
|
||||
/**
|
||||
* Class responsible of loading Flipper inside your React Native application. This is the debug
|
||||
* flavor of it. Here you can add your own plugins and customize the Flipper setup.
|
||||
*/
|
||||
public class ReactNativeFlipper {
|
||||
public static void initializeFlipper(Context context, ReactInstanceManager reactInstanceManager) {
|
||||
if (FlipperUtils.shouldEnableFlipper(context)) {
|
||||
final FlipperClient client = AndroidFlipperClient.getInstance(context);
|
||||
client.addPlugin(new InspectorFlipperPlugin(context, DescriptorMapping.withDefaults()));
|
||||
client.addPlugin(new ReactFlipperPlugin());
|
||||
client.addPlugin(new DatabasesFlipperPlugin(context));
|
||||
client.addPlugin(new SharedPreferencesFlipperPlugin(context));
|
||||
client.addPlugin(CrashReporterPlugin.getInstance());
|
||||
@@ -1,10 +1,10 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.mattermost.rnbeta">
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
<uses-permission-sdk-23 android:name="android.permission.VIBRATE"/>
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
||||
<uses-permission android:name="android.permission.CAMERA"/>
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO"/>
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO"/>
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||
@@ -40,7 +40,6 @@
|
||||
android:resizeableActivity="true"
|
||||
android:requestLegacyExternalStorage="true"
|
||||
android:usesCleartextTraffic="true"
|
||||
tools:replace="android:allowBackup"
|
||||
>
|
||||
<meta-data android:name="firebase_analytics_collection_deactivated" android:value="true" />
|
||||
<meta-data android:name="android.content.APP_RESTRICTIONS"
|
||||
|
||||
@@ -58,7 +58,6 @@ public class CustomPushNotificationHelper {
|
||||
String senderId = bundle.getString("sender_id");
|
||||
String serverUrl = bundle.getString("server_url");
|
||||
String type = bundle.getString("type");
|
||||
String urlOverride = bundle.getString("override_icon_url");
|
||||
if (senderId == null) {
|
||||
senderId = "sender_id";
|
||||
}
|
||||
@@ -75,7 +74,7 @@ public class CustomPushNotificationHelper {
|
||||
|
||||
if (serverUrl != null && !type.equals(CustomPushNotificationHelper.PUSH_TYPE_SESSION)) {
|
||||
try {
|
||||
Bitmap avatar = userAvatar(context, serverUrl, senderId, urlOverride);
|
||||
Bitmap avatar = userAvatar(context, serverUrl, senderId, null);
|
||||
if (avatar != null) {
|
||||
sender.setIcon(IconCompat.createWithBitmap(avatar));
|
||||
}
|
||||
@@ -267,7 +266,6 @@ public class CustomPushNotificationHelper {
|
||||
final String senderId = "me";
|
||||
final String serverUrl = bundle.getString("server_url");
|
||||
final String type = bundle.getString("type");
|
||||
String urlOverride = bundle.getString("override_icon_url");
|
||||
|
||||
Person.Builder sender = new Person.Builder()
|
||||
.setKey(senderId)
|
||||
@@ -275,7 +273,7 @@ public class CustomPushNotificationHelper {
|
||||
|
||||
if (serverUrl != null && !type.equals(CustomPushNotificationHelper.PUSH_TYPE_SESSION)) {
|
||||
try {
|
||||
Bitmap avatar = userAvatar(context, serverUrl, "me", urlOverride);
|
||||
Bitmap avatar = userAvatar(context, serverUrl, "me", null);
|
||||
if (avatar != null) {
|
||||
sender.setIcon(IconCompat.createWithBitmap(avatar));
|
||||
}
|
||||
@@ -428,7 +426,7 @@ public class CustomPushNotificationHelper {
|
||||
final OkHttpClient client = new OkHttpClient();
|
||||
Request request;
|
||||
String url;
|
||||
if (!TextUtils.isEmpty(urlOverride)) {
|
||||
if (urlOverride != null) {
|
||||
request = new Request.Builder().url(urlOverride).build();
|
||||
Log.i("ReactNative", String.format("Fetch override profile image %s", urlOverride));
|
||||
} else {
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
package com.mattermost.newarchitecture;
|
||||
|
||||
import android.app.Application;
|
||||
import androidx.annotation.NonNull;
|
||||
import com.facebook.react.PackageList;
|
||||
import com.facebook.react.ReactInstanceManager;
|
||||
import com.facebook.react.ReactNativeHost;
|
||||
import com.facebook.react.ReactPackage;
|
||||
import com.facebook.react.ReactPackageTurboModuleManagerDelegate;
|
||||
import com.facebook.react.bridge.JSIModulePackage;
|
||||
import com.facebook.react.bridge.JSIModuleProvider;
|
||||
import com.facebook.react.bridge.JSIModuleSpec;
|
||||
import com.facebook.react.bridge.JSIModuleType;
|
||||
import com.facebook.react.bridge.JavaScriptContextHolder;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.bridge.UIManager;
|
||||
import com.facebook.react.fabric.ComponentFactory;
|
||||
import com.facebook.react.fabric.CoreComponentsRegistry;
|
||||
import com.facebook.react.fabric.FabricJSIModuleProvider;
|
||||
import com.facebook.react.fabric.ReactNativeConfig;
|
||||
import com.facebook.react.uimanager.ViewManagerRegistry;
|
||||
import com.mattermost.rnbeta.BuildConfig;
|
||||
import com.mattermost.newarchitecture.components.MainComponentsRegistry;
|
||||
import com.mattermost.newarchitecture.modules.MainApplicationTurboModuleManagerDelegate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A {@link ReactNativeHost} that helps you load everything needed for the New Architecture, both
|
||||
* TurboModule delegates and the Fabric Renderer.
|
||||
*
|
||||
* <p>Please note that this class is used ONLY if you opt-in for the New Architecture (see the
|
||||
* `newArchEnabled` property). Is ignored otherwise.
|
||||
*/
|
||||
public class MainApplicationReactNativeHost extends ReactNativeHost {
|
||||
public MainApplicationReactNativeHost(Application application) {
|
||||
super(application);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getUseDeveloperSupport() {
|
||||
return BuildConfig.DEBUG;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<ReactPackage> getPackages() {
|
||||
List<ReactPackage> packages = new PackageList(this).getPackages();
|
||||
// Packages that cannot be autolinked yet can be added manually here, for example:
|
||||
// packages.add(new MyReactNativePackage());
|
||||
// TurboModules must also be loaded here providing a valid TurboReactPackage implementation:
|
||||
// packages.add(new TurboReactPackage() { ... });
|
||||
// If you have custom Fabric Components, their ViewManagers should also be loaded here
|
||||
// inside a ReactPackage.
|
||||
return packages;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getJSMainModuleName() {
|
||||
return "index";
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
protected ReactPackageTurboModuleManagerDelegate.Builder
|
||||
getReactPackageTurboModuleManagerDelegateBuilder() {
|
||||
// Here we provide the ReactPackageTurboModuleManagerDelegate Builder. This is necessary
|
||||
// for the new architecture and to use TurboModules correctly.
|
||||
return new MainApplicationTurboModuleManagerDelegate.Builder();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected JSIModulePackage getJSIModulePackage() {
|
||||
return new JSIModulePackage() {
|
||||
@Override
|
||||
public List<JSIModuleSpec> getJSIModules(
|
||||
final ReactApplicationContext reactApplicationContext,
|
||||
final JavaScriptContextHolder jsContext) {
|
||||
final List<JSIModuleSpec> specs = new ArrayList<>();
|
||||
|
||||
// Here we provide a new JSIModuleSpec that will be responsible of providing the
|
||||
// custom Fabric Components.
|
||||
specs.add(
|
||||
new JSIModuleSpec() {
|
||||
@Override
|
||||
public JSIModuleType getJSIModuleType() {
|
||||
return JSIModuleType.UIManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JSIModuleProvider<UIManager> getJSIModuleProvider() {
|
||||
final ComponentFactory componentFactory = new ComponentFactory();
|
||||
CoreComponentsRegistry.register(componentFactory);
|
||||
|
||||
// Here we register a Components Registry.
|
||||
// The one that is generated with the template contains no components
|
||||
// and just provides you the one from React Native core.
|
||||
MainComponentsRegistry.register(componentFactory);
|
||||
|
||||
final ReactInstanceManager reactInstanceManager = getReactInstanceManager();
|
||||
|
||||
ViewManagerRegistry viewManagerRegistry =
|
||||
new ViewManagerRegistry(
|
||||
reactInstanceManager.getOrCreateViewManagers(reactApplicationContext));
|
||||
|
||||
return new FabricJSIModuleProvider(
|
||||
reactApplicationContext,
|
||||
componentFactory,
|
||||
ReactNativeConfig.DEFAULT_CONFIG,
|
||||
viewManagerRegistry);
|
||||
}
|
||||
});
|
||||
return specs;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.mattermost.newarchitecture.components;
|
||||
|
||||
import com.facebook.jni.HybridData;
|
||||
import com.facebook.proguard.annotations.DoNotStrip;
|
||||
import com.facebook.react.fabric.ComponentFactory;
|
||||
import com.facebook.soloader.SoLoader;
|
||||
|
||||
/**
|
||||
* Class responsible to load the custom Fabric Components. This class has native methods and needs a
|
||||
* corresponding C++ implementation/header file to work correctly (already placed inside the jni/
|
||||
* folder for you).
|
||||
*
|
||||
* <p>Please note that this class is used ONLY if you opt-in for the New Architecture (see the
|
||||
* `newArchEnabled` property). Is ignored otherwise.
|
||||
*/
|
||||
@DoNotStrip
|
||||
public class MainComponentsRegistry {
|
||||
static {
|
||||
SoLoader.loadLibrary("fabricjni");
|
||||
}
|
||||
|
||||
@DoNotStrip private final HybridData mHybridData;
|
||||
|
||||
@DoNotStrip
|
||||
private native HybridData initHybrid(ComponentFactory componentFactory);
|
||||
|
||||
@DoNotStrip
|
||||
private MainComponentsRegistry(ComponentFactory componentFactory) {
|
||||
mHybridData = initHybrid(componentFactory);
|
||||
}
|
||||
|
||||
@DoNotStrip
|
||||
public static MainComponentsRegistry register(ComponentFactory componentFactory) {
|
||||
return new MainComponentsRegistry(componentFactory);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package com.mattermost.newarchitecture.modules;
|
||||
|
||||
import com.facebook.jni.HybridData;
|
||||
import com.facebook.react.ReactPackage;
|
||||
import com.facebook.react.ReactPackageTurboModuleManagerDelegate;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.soloader.SoLoader;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Class responsible to load the TurboModules. This class has native methods and needs a
|
||||
* corresponding C++ implementation/header file to work correctly (already placed inside the jni/
|
||||
* folder for you).
|
||||
*
|
||||
* <p>Please note that this class is used ONLY if you opt-in for the New Architecture (see the
|
||||
* `newArchEnabled` property). Is ignored otherwise.
|
||||
*/
|
||||
public class MainApplicationTurboModuleManagerDelegate
|
||||
extends ReactPackageTurboModuleManagerDelegate {
|
||||
|
||||
private static volatile boolean sIsSoLibraryLoaded;
|
||||
|
||||
protected MainApplicationTurboModuleManagerDelegate(
|
||||
ReactApplicationContext reactApplicationContext, List<ReactPackage> packages) {
|
||||
super(reactApplicationContext, packages);
|
||||
}
|
||||
|
||||
protected native HybridData initHybrid();
|
||||
|
||||
native boolean canCreateTurboModule(String moduleName);
|
||||
|
||||
public static class Builder extends ReactPackageTurboModuleManagerDelegate.Builder {
|
||||
protected MainApplicationTurboModuleManagerDelegate build(
|
||||
ReactApplicationContext context, List<ReactPackage> packages) {
|
||||
return new MainApplicationTurboModuleManagerDelegate(context, packages);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected synchronized void maybeLoadOtherSoLibraries() {
|
||||
if (!sIsSoLibraryLoaded) {
|
||||
// If you change the name of your application .so file in the Android.mk file,
|
||||
// make sure you update the name here as well.
|
||||
SoLoader.loadLibrary("rndiffapp_appmodules");
|
||||
sIsSoLibraryLoaded = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,36 +6,37 @@ import androidx.annotation.Nullable;
|
||||
import android.view.KeyEvent;
|
||||
import android.content.res.Configuration;
|
||||
|
||||
import com.facebook.react.bridge.Arguments;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.bridge.ReactMethod;
|
||||
import com.facebook.react.bridge.WritableMap;
|
||||
import com.facebook.react.ReactActivityDelegate;
|
||||
import com.facebook.react.ReactRootView;
|
||||
import com.reactnativenavigation.NavigationActivity;
|
||||
import com.github.emilioicai.hwkeyboardevent.HWKeyboardEventModule;
|
||||
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint;
|
||||
import com.facebook.react.defaults.DefaultReactActivityDelegate;
|
||||
|
||||
|
||||
public class MainActivity extends NavigationActivity {
|
||||
private boolean HWKeyboardConnected = false;
|
||||
|
||||
@Override
|
||||
protected String getMainComponentName() {
|
||||
return "Mattermost";
|
||||
}
|
||||
public static class MainActivityDelegate extends ReactActivityDelegate {
|
||||
public MainActivityDelegate(NavigationActivity activity, String mainComponentName) {
|
||||
super(activity, mainComponentName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the instance of the {@link ReactActivityDelegate}. Here we use a util class {@link
|
||||
* DefaultReactActivityDelegate} which allows you to easily enable Fabric and Concurrent React
|
||||
* (aka React 18) with two boolean flags.
|
||||
*/
|
||||
@Override
|
||||
protected ReactActivityDelegate createReactActivityDelegate() {
|
||||
return new DefaultReactActivityDelegate(
|
||||
this,
|
||||
getMainComponentName(),
|
||||
// If you opted-in for the New Architecture, we enable the Fabric Renderer.
|
||||
DefaultNewArchitectureEntryPoint.getFabricEnabled(), // fabricEnabled
|
||||
// If you opted-in for the New Architecture, we enable Concurrent React (i.e. React 18).
|
||||
DefaultNewArchitectureEntryPoint.getConcurrentReactEnabled() // concurrentRootEnabled
|
||||
);
|
||||
@Override
|
||||
protected ReactRootView createRootView() {
|
||||
ReactRootView reactRootView = new ReactRootView(getContext());
|
||||
// If you opted-in for the New Architecture, we enable the Fabric Renderer.
|
||||
reactRootView.setIsFabric(BuildConfig.IS_NEW_ARCHITECTURE_ENABLED);
|
||||
return reactRootView;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isConcurrentRootEnabled() {
|
||||
// If you opted-in for the New Architecture, we enable Concurrent Root (i.e. React 18).
|
||||
// More on this on https://reactjs.org/blog/2022/03/29/react-v18.html
|
||||
return BuildConfig.IS_NEW_ARCHITECTURE_ENABLED;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
package com.mattermost.rnbeta;
|
||||
|
||||
import com.facebook.react.bridge.JSIModuleSpec;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
@@ -20,12 +23,11 @@ import com.wix.reactnativenotifications.core.AppLifecycleFacade;
|
||||
import com.wix.reactnativenotifications.core.JsIOHelper;
|
||||
|
||||
import com.facebook.react.PackageList;
|
||||
import com.facebook.react.ReactInstanceManager;
|
||||
import com.facebook.react.ReactPackage;
|
||||
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint;
|
||||
import com.facebook.react.defaults.DefaultReactNativeHost;
|
||||
import com.facebook.react.config.ReactFeatureFlags;
|
||||
import com.facebook.react.ReactNativeHost;
|
||||
import com.facebook.react.TurboReactPackage;
|
||||
import com.facebook.react.bridge.JSIModuleSpec;
|
||||
import com.facebook.react.bridge.NativeModule;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.bridge.JSIModulePackage;
|
||||
@@ -34,8 +36,8 @@ import com.facebook.react.module.model.ReactModuleInfoProvider;
|
||||
import com.facebook.react.modules.network.OkHttpClientProvider;
|
||||
import com.facebook.soloader.SoLoader;
|
||||
|
||||
import com.mattermost.flipper.ReactNativeFlipper;
|
||||
import com.mattermost.networkclient.RCTOkHttpClientFactory;
|
||||
import com.mattermost.newarchitecture.MainApplicationReactNativeHost;
|
||||
import com.nozbe.watermelondb.jsi.WatermelonDBJSIPackage;
|
||||
|
||||
public class MainApplication extends NavigationApplication implements INotificationsApplication {
|
||||
@@ -44,7 +46,7 @@ public class MainApplication extends NavigationApplication implements INotificat
|
||||
public Boolean sharedExtensionIsOpened = false;
|
||||
|
||||
private final ReactNativeHost mReactNativeHost =
|
||||
new DefaultReactNativeHost(this) {
|
||||
new ReactNativeHost(this) {
|
||||
@Override
|
||||
public boolean getUseDeveloperSupport() {
|
||||
return BuildConfig.DEBUG;
|
||||
@@ -104,26 +106,30 @@ public class MainApplication extends NavigationApplication implements INotificat
|
||||
protected String getJSMainModuleName() {
|
||||
return "index";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isNewArchEnabled() {
|
||||
return BuildConfig.IS_NEW_ARCHITECTURE_ENABLED;
|
||||
}
|
||||
@Override
|
||||
protected Boolean isHermesEnabled() {
|
||||
return BuildConfig.IS_HERMES_ENABLED;
|
||||
}
|
||||
};
|
||||
|
||||
private final ReactNativeHost mNewArchitectureNativeHost =
|
||||
new MainApplicationReactNativeHost(this);
|
||||
|
||||
@Override
|
||||
public ReactNativeHost getReactNativeHost() {
|
||||
return mReactNativeHost;
|
||||
if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
|
||||
return mNewArchitectureNativeHost;
|
||||
} else {
|
||||
return mReactNativeHost;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
instance = this;
|
||||
|
||||
// If you opted-in for the New Architecture, we enable the TurboModule system
|
||||
ReactFeatureFlags.useTurboModules = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED;
|
||||
SoLoader.init(this, /* native exopackage */ false);
|
||||
initializeFlipper(this, getReactNativeHost().getReactInstanceManager());
|
||||
|
||||
Context context = getApplicationContext();
|
||||
|
||||
// Delete any previous temp files created by the app
|
||||
@@ -135,13 +141,6 @@ public class MainApplication extends NavigationApplication implements INotificat
|
||||
// with a cookie jar defined in APIClientModule and an interceptor to intercept all
|
||||
// requests that originate from React Native's OKHttpClient
|
||||
OkHttpClientProvider.setOkHttpClientFactory(new RCTOkHttpClientFactory());
|
||||
|
||||
SoLoader.init(this, /* native exopackage */ false);
|
||||
if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
|
||||
// If you opted-in for the New Architecture, we load the native entry point for this app.
|
||||
DefaultNewArchitectureEntryPoint.load();
|
||||
}
|
||||
ReactNativeFlipper.initializeFlipper(this, getReactNativeHost().getReactInstanceManager());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -154,4 +153,26 @@ public class MainApplication extends NavigationApplication implements INotificat
|
||||
new JsIOHelper()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads Flipper in React Native templates. Call this in the onCreate method with something like
|
||||
* initializeFlipper(this, getReactNativeHost().getReactInstanceManager());
|
||||
*/
|
||||
private static void initializeFlipper(
|
||||
Context context, ReactInstanceManager reactInstanceManager) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
try {
|
||||
/*
|
||||
We use reflection here to pick up the class that initializes Flipper,
|
||||
since Flipper library is not available in release mode
|
||||
*/
|
||||
Class<?> aClass = Class.forName("com.rn.ReactNativeFlipper");
|
||||
aClass
|
||||
.getMethod("initializeFlipper", Context.class, ReactInstanceManager.class)
|
||||
.invoke(null, context, reactInstanceManager);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,26 +90,22 @@ public class ShareUtils {
|
||||
}
|
||||
|
||||
private static Bitmap getBitmapAtTime(Context context, String filePath, int time) {
|
||||
try {
|
||||
MediaMetadataRetriever retriever = new MediaMetadataRetriever();
|
||||
if (URLUtil.isFileUrl(filePath)) {
|
||||
String decodedPath;
|
||||
try {
|
||||
decodedPath = URLDecoder.decode(filePath, "UTF-8");
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
decodedPath = filePath;
|
||||
}
|
||||
|
||||
retriever.setDataSource(decodedPath.replace("file://", ""));
|
||||
} else if (filePath.contains("content://")) {
|
||||
retriever.setDataSource(context, Uri.parse(filePath));
|
||||
MediaMetadataRetriever retriever = new MediaMetadataRetriever();
|
||||
if (URLUtil.isFileUrl(filePath)) {
|
||||
String decodedPath;
|
||||
try {
|
||||
decodedPath = URLDecoder.decode(filePath, "UTF-8");
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
decodedPath = filePath;
|
||||
}
|
||||
|
||||
Bitmap image = retriever.getFrameAtTime(time * 1000, MediaMetadataRetriever.OPTION_CLOSEST_SYNC);
|
||||
retriever.release();
|
||||
return image;
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException("File doesn't exist or not supported");
|
||||
retriever.setDataSource(decodedPath.replace("file://", ""));
|
||||
} else if (filePath.contains("content://")) {
|
||||
retriever.setDataSource(context, Uri.parse(filePath));
|
||||
}
|
||||
|
||||
Bitmap image = retriever.getFrameAtTime(time * 1000, MediaMetadataRetriever.OPTION_CLOSEST_SYNC);
|
||||
retriever.release();
|
||||
return image;
|
||||
}
|
||||
}
|
||||
|
||||
7
android/app/src/main/jni/CMakeLists.txt
Normal file
7
android/app/src/main/jni/CMakeLists.txt
Normal file
@@ -0,0 +1,7 @@
|
||||
cmake_minimum_required(VERSION 3.13)
|
||||
|
||||
# Define the library name here.
|
||||
project(rndiffapp_appmodules)
|
||||
|
||||
# This file includes all the necessary to let you build your application with the New Architecture.
|
||||
include(${REACT_ANDROID_DIR}/cmake-utils/ReactNative-application.cmake)
|
||||
32
android/app/src/main/jni/MainApplicationModuleProvider.cpp
Normal file
32
android/app/src/main/jni/MainApplicationModuleProvider.cpp
Normal file
@@ -0,0 +1,32 @@
|
||||
#include "MainApplicationModuleProvider.h"
|
||||
|
||||
#include <rncli.h>
|
||||
#include <rncore.h>
|
||||
|
||||
namespace facebook {
|
||||
namespace react {
|
||||
|
||||
std::shared_ptr<TurboModule> MainApplicationModuleProvider(
|
||||
const std::string &moduleName,
|
||||
const JavaTurboModule::InitParams ¶ms) {
|
||||
// Here you can provide your own module provider for TurboModules coming from
|
||||
// either your application or from external libraries. The approach to follow
|
||||
// is similar to the following (for a library called `samplelibrary`:
|
||||
//
|
||||
// auto module = samplelibrary_ModuleProvider(moduleName, params);
|
||||
// if (module != nullptr) {
|
||||
// return module;
|
||||
// }
|
||||
// return rncore_ModuleProvider(moduleName, params);
|
||||
|
||||
// Module providers autolinked by RN CLI
|
||||
auto rncli_module = rncli_ModuleProvider(moduleName, params);
|
||||
if (rncli_module != nullptr) {
|
||||
return rncli_module;
|
||||
}
|
||||
|
||||
return rncore_ModuleProvider(moduleName, params);
|
||||
}
|
||||
|
||||
} // namespace react
|
||||
} // namespace facebook
|
||||
16
android/app/src/main/jni/MainApplicationModuleProvider.h
Normal file
16
android/app/src/main/jni/MainApplicationModuleProvider.h
Normal file
@@ -0,0 +1,16 @@
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include <ReactCommon/JavaTurboModule.h>
|
||||
|
||||
namespace facebook {
|
||||
namespace react {
|
||||
|
||||
std::shared_ptr<TurboModule> MainApplicationModuleProvider(
|
||||
const std::string &moduleName,
|
||||
const JavaTurboModule::InitParams ¶ms);
|
||||
|
||||
} // namespace react
|
||||
} // namespace facebook
|
||||
@@ -0,0 +1,45 @@
|
||||
#include "MainApplicationTurboModuleManagerDelegate.h"
|
||||
#include "MainApplicationModuleProvider.h"
|
||||
|
||||
namespace facebook {
|
||||
namespace react {
|
||||
|
||||
jni::local_ref<MainApplicationTurboModuleManagerDelegate::jhybriddata>
|
||||
MainApplicationTurboModuleManagerDelegate::initHybrid(
|
||||
jni::alias_ref<jhybridobject>) {
|
||||
return makeCxxInstance();
|
||||
}
|
||||
|
||||
void MainApplicationTurboModuleManagerDelegate::registerNatives() {
|
||||
registerHybrid({
|
||||
makeNativeMethod(
|
||||
"initHybrid", MainApplicationTurboModuleManagerDelegate::initHybrid),
|
||||
makeNativeMethod(
|
||||
"canCreateTurboModule",
|
||||
MainApplicationTurboModuleManagerDelegate::canCreateTurboModule),
|
||||
});
|
||||
}
|
||||
|
||||
std::shared_ptr<TurboModule>
|
||||
MainApplicationTurboModuleManagerDelegate::getTurboModule(
|
||||
const std::string &name,
|
||||
const std::shared_ptr<CallInvoker> &jsInvoker) {
|
||||
// Not implemented yet: provide pure-C++ NativeModules here.
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::shared_ptr<TurboModule>
|
||||
MainApplicationTurboModuleManagerDelegate::getTurboModule(
|
||||
const std::string &name,
|
||||
const JavaTurboModule::InitParams ¶ms) {
|
||||
return MainApplicationModuleProvider(name, params);
|
||||
}
|
||||
|
||||
bool MainApplicationTurboModuleManagerDelegate::canCreateTurboModule(
|
||||
const std::string &name) {
|
||||
return getTurboModule(name, nullptr) != nullptr ||
|
||||
getTurboModule(name, {.moduleName = name}) != nullptr;
|
||||
}
|
||||
|
||||
} // namespace react
|
||||
} // namespace facebook
|
||||
@@ -0,0 +1,38 @@
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include <ReactCommon/TurboModuleManagerDelegate.h>
|
||||
#include <fbjni/fbjni.h>
|
||||
|
||||
namespace facebook {
|
||||
namespace react {
|
||||
|
||||
class MainApplicationTurboModuleManagerDelegate
|
||||
: public jni::HybridClass<
|
||||
MainApplicationTurboModuleManagerDelegate,
|
||||
TurboModuleManagerDelegate> {
|
||||
public:
|
||||
// Adapt it to the package you used for your Java class.
|
||||
static constexpr auto kJavaDescriptor =
|
||||
"Lcom/rndiffapp/newarchitecture/modules/MainApplicationTurboModuleManagerDelegate;";
|
||||
|
||||
static jni::local_ref<jhybriddata> initHybrid(jni::alias_ref<jhybridobject>);
|
||||
|
||||
static void registerNatives();
|
||||
|
||||
std::shared_ptr<TurboModule> getTurboModule(
|
||||
const std::string &name,
|
||||
const std::shared_ptr<CallInvoker> &jsInvoker) override;
|
||||
std::shared_ptr<TurboModule> getTurboModule(
|
||||
const std::string &name,
|
||||
const JavaTurboModule::InitParams ¶ms) override;
|
||||
|
||||
/**
|
||||
* Test-only method. Allows user to verify whether a TurboModule can be
|
||||
* created by instances of this class.
|
||||
*/
|
||||
bool canCreateTurboModule(const std::string &name);
|
||||
};
|
||||
|
||||
} // namespace react
|
||||
} // namespace facebook
|
||||
65
android/app/src/main/jni/MainComponentsRegistry.cpp
Normal file
65
android/app/src/main/jni/MainComponentsRegistry.cpp
Normal file
@@ -0,0 +1,65 @@
|
||||
#include "MainComponentsRegistry.h"
|
||||
|
||||
#include <CoreComponentsRegistry.h>
|
||||
#include <fbjni/fbjni.h>
|
||||
#include <react/renderer/componentregistry/ComponentDescriptorProviderRegistry.h>
|
||||
#include <react/renderer/components/rncore/ComponentDescriptors.h>
|
||||
#include <rncli.h>
|
||||
|
||||
namespace facebook {
|
||||
namespace react {
|
||||
|
||||
MainComponentsRegistry::MainComponentsRegistry(ComponentFactory *delegate) {}
|
||||
|
||||
std::shared_ptr<ComponentDescriptorProviderRegistry const>
|
||||
MainComponentsRegistry::sharedProviderRegistry() {
|
||||
auto providerRegistry = CoreComponentsRegistry::sharedProviderRegistry();
|
||||
|
||||
// Autolinked providers registered by RN CLI
|
||||
rncli_registerProviders(providerRegistry);
|
||||
|
||||
// Custom Fabric Components go here. You can register custom
|
||||
// components coming from your App or from 3rd party libraries here.
|
||||
//
|
||||
// providerRegistry->add(concreteComponentDescriptorProvider<
|
||||
// AocViewerComponentDescriptor>());
|
||||
return providerRegistry;
|
||||
}
|
||||
|
||||
jni::local_ref<MainComponentsRegistry::jhybriddata>
|
||||
MainComponentsRegistry::initHybrid(
|
||||
jni::alias_ref<jclass>,
|
||||
ComponentFactory *delegate) {
|
||||
auto instance = makeCxxInstance(delegate);
|
||||
|
||||
auto buildRegistryFunction =
|
||||
[](EventDispatcher::Weak const &eventDispatcher,
|
||||
ContextContainer::Shared const &contextContainer)
|
||||
-> ComponentDescriptorRegistry::Shared {
|
||||
auto registry = MainComponentsRegistry::sharedProviderRegistry()
|
||||
->createComponentDescriptorRegistry(
|
||||
{eventDispatcher, contextContainer});
|
||||
|
||||
auto mutableRegistry =
|
||||
std::const_pointer_cast<ComponentDescriptorRegistry>(registry);
|
||||
|
||||
mutableRegistry->setFallbackComponentDescriptor(
|
||||
std::make_shared<UnimplementedNativeViewComponentDescriptor>(
|
||||
ComponentDescriptorParameters{
|
||||
eventDispatcher, contextContainer, nullptr}));
|
||||
|
||||
return registry;
|
||||
};
|
||||
|
||||
delegate->buildRegistryFunction = buildRegistryFunction;
|
||||
return instance;
|
||||
}
|
||||
|
||||
void MainComponentsRegistry::registerNatives() {
|
||||
registerHybrid({
|
||||
makeNativeMethod("initHybrid", MainComponentsRegistry::initHybrid),
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace react
|
||||
} // namespace facebook
|
||||
32
android/app/src/main/jni/MainComponentsRegistry.h
Normal file
32
android/app/src/main/jni/MainComponentsRegistry.h
Normal file
@@ -0,0 +1,32 @@
|
||||
#pragma once
|
||||
|
||||
#include <ComponentFactory.h>
|
||||
#include <fbjni/fbjni.h>
|
||||
#include <react/renderer/componentregistry/ComponentDescriptorProviderRegistry.h>
|
||||
#include <react/renderer/componentregistry/ComponentDescriptorRegistry.h>
|
||||
|
||||
namespace facebook {
|
||||
namespace react {
|
||||
|
||||
class MainComponentsRegistry
|
||||
: public facebook::jni::HybridClass<MainComponentsRegistry> {
|
||||
public:
|
||||
// Adapt it to the package you used for your Java class.
|
||||
constexpr static auto kJavaDescriptor =
|
||||
"Lcom/mattermost/newarchitecture/components/MainComponentsRegistry;";
|
||||
|
||||
static void registerNatives();
|
||||
|
||||
MainComponentsRegistry(ComponentFactory *delegate);
|
||||
|
||||
private:
|
||||
static std::shared_ptr<ComponentDescriptorProviderRegistry const>
|
||||
sharedProviderRegistry();
|
||||
|
||||
static jni::local_ref<jhybriddata> initHybrid(
|
||||
jni::alias_ref<jclass>,
|
||||
ComponentFactory *delegate);
|
||||
};
|
||||
|
||||
} // namespace react
|
||||
} // namespace facebook
|
||||
11
android/app/src/main/jni/OnLoad.cpp
Normal file
11
android/app/src/main/jni/OnLoad.cpp
Normal file
@@ -0,0 +1,11 @@
|
||||
#include <fbjni/fbjni.h>
|
||||
#include "MainApplicationTurboModuleManagerDelegate.h"
|
||||
#include "MainComponentsRegistry.h"
|
||||
|
||||
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *) {
|
||||
return facebook::jni::initialize(vm, [] {
|
||||
facebook::react::MainApplicationTurboModuleManagerDelegate::
|
||||
registerNatives();
|
||||
facebook::react::MainComponentsRegistry::registerNatives();
|
||||
});
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* <p>This source code is licensed under the MIT license found in the LICENSE file in the root
|
||||
* directory of this source tree.
|
||||
*/
|
||||
package com.mattermost.flipper;
|
||||
|
||||
import android.content.Context;
|
||||
import com.facebook.react.ReactInstanceManager;
|
||||
/**
|
||||
* Class responsible of loading Flipper inside your React Native application. This is the release
|
||||
* flavor of it so it's empty as we don't want to load Flipper.
|
||||
*/
|
||||
public class ReactNativeFlipper {
|
||||
public static void initializeFlipper(Context context, ReactInstanceManager reactInstanceManager) {
|
||||
// Do nothing as we don't want to initialize Flipper on Release.
|
||||
}
|
||||
}
|
||||
@@ -2,18 +2,22 @@
|
||||
|
||||
buildscript {
|
||||
ext {
|
||||
buildToolsVersion = "33.0.0"
|
||||
buildToolsVersion = "31.0.0"
|
||||
minSdkVersion = 24
|
||||
compileSdkVersion = 33
|
||||
targetSdkVersion = 33
|
||||
supportLibVersion = "33.0.0"
|
||||
compileSdkVersion = 31
|
||||
targetSdkVersion = 31
|
||||
supportLibVersion = "31.0.0"
|
||||
kotlinVersion = "1.5.30"
|
||||
kotlin_version = "1.5.30"
|
||||
firebaseVersion = "21.0.0"
|
||||
RNNKotlinVersion = kotlinVersion
|
||||
|
||||
// We use NDK 23 which has both M1 support and is the side-by-side NDK version from AGP.
|
||||
ndkVersion = "23.1.7779620"
|
||||
if (System.properties['os.arch'] == "aarch64") {
|
||||
// For M1 Users we need to use the NDK 24 which added support for aarch64
|
||||
ndkVersion = "24.0.8215888"
|
||||
} else {
|
||||
// Otherwise we default to the side-by-side NDK version from AGP.
|
||||
ndkVersion = "21.4.7075529"
|
||||
}
|
||||
}
|
||||
repositories {
|
||||
mavenCentral()
|
||||
@@ -21,8 +25,9 @@ buildscript {
|
||||
google()
|
||||
}
|
||||
dependencies {
|
||||
classpath("com.android.tools.build:gradle:7.3.1")
|
||||
classpath("com.android.tools.build:gradle:7.2.1")
|
||||
classpath("com.facebook.react:react-native-gradle-plugin")
|
||||
classpath("de.undercouch:gradle-download-task:5.0.1")
|
||||
classpath('com.google.gms:google-services:4.3.14')
|
||||
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion")
|
||||
|
||||
@@ -33,9 +38,50 @@ buildscript {
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
exclusiveContent {
|
||||
// We get React Native's Android binaries exclusively through npm,
|
||||
// from a local Maven repo inside node_modules/react-native/.
|
||||
// (The use of exclusiveContent prevents looking elsewhere like Maven Central
|
||||
// and potentially getting a wrong version.)
|
||||
filter {
|
||||
includeGroup "com.facebook.react"
|
||||
}
|
||||
forRepository {
|
||||
maven {
|
||||
url "$rootDir/../node_modules/react-native/android"
|
||||
}
|
||||
}
|
||||
}
|
||||
google()
|
||||
mavenCentral()
|
||||
mavenLocal()
|
||||
maven {
|
||||
// All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
|
||||
url("$rootDir/../node_modules/react-native/android")
|
||||
|
||||
// Replace AAR from original RN with AAR from react-native-v8
|
||||
// url("$rootDir/../node_modules/react-native-v8/dist")
|
||||
}
|
||||
maven {
|
||||
// Local Maven repo containing AARs with JSC library built for Android
|
||||
url("$rootDir/../node_modules/jsc-android/dist")
|
||||
|
||||
// prebuilt libv8android.so
|
||||
// url("$rootDir/../node_modules/v8-android/dist")
|
||||
}
|
||||
maven {
|
||||
url "https://www.jitpack.io"
|
||||
}
|
||||
maven {
|
||||
url "$rootDir/../node_modules/detox/Detox-android"
|
||||
}
|
||||
mavenCentral {
|
||||
// We don't want to fetch react-native from Maven Central as there are
|
||||
// older versions over there.
|
||||
content {
|
||||
excludeGroup "com.facebook.react"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -40,8 +40,4 @@ reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64
|
||||
# your application. You should enable this flag either if you want
|
||||
# to write custom TurboModules/Fabric components OR use libraries that
|
||||
# are providing them.
|
||||
newArchEnabled=false
|
||||
|
||||
# Use this property to enable or disable the Hermes JS engine.
|
||||
# If set to false, you will be using JSC instead.
|
||||
hermesEnabled=true
|
||||
newArchEnabled=false
|
||||
@@ -1,5 +1,13 @@
|
||||
rootProject.name = 'Mattermost'
|
||||
include ':app'
|
||||
includeBuild('../node_modules/react-native-gradle-plugin')
|
||||
if (settings.hasProperty("newArchEnabled") && settings.newArchEnabled == "true") {
|
||||
include(":ReactAndroid")
|
||||
project(":ReactAndroid").projectDir = file('../node_modules/react-native/ReactAndroid')
|
||||
include(":ReactAndroid:hermes-engine")
|
||||
project(":ReactAndroid:hermes-engine").projectDir = file('../node_modules/react-native/ReactAndroid/hermes-engine')
|
||||
}
|
||||
|
||||
include ':reactnativenotifications'
|
||||
project(':reactnativenotifications').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-notifications/lib/android/app')
|
||||
apply from: file("../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesSettingsGradle(settings)
|
||||
@@ -7,4 +15,3 @@ include ':react-native-video'
|
||||
project(':react-native-video').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-video/android-exoplayer')
|
||||
include ':watermelondb-jsi'
|
||||
project(':watermelondb-jsi').projectDir = new File(rootProject.projectDir, '../node_modules/@nozbe/watermelondb/native/android-jsi')
|
||||
includeBuild('../node_modules/react-native-gradle-plugin')
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Tutorial} from '@constants';
|
||||
import {GLOBAL_IDENTIFIERS} from '@constants/database';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {logError} from '@utils/log';
|
||||
@@ -23,20 +22,16 @@ export const storeDeviceToken = async (token: string, prepareRecordsOnly = false
|
||||
return storeGlobal(GLOBAL_IDENTIFIERS.DEVICE_TOKEN, token, prepareRecordsOnly);
|
||||
};
|
||||
|
||||
export const storeMultiServerTutorial = async (prepareRecordsOnly = false) => {
|
||||
return storeGlobal(GLOBAL_IDENTIFIERS.MULTI_SERVER_TUTORIAL, 'true', prepareRecordsOnly);
|
||||
};
|
||||
|
||||
export const storeOnboardingViewedValue = async (value = true) => {
|
||||
return storeGlobal(GLOBAL_IDENTIFIERS.ONBOARDING, value, false);
|
||||
};
|
||||
|
||||
export const storeMultiServerTutorial = async (prepareRecordsOnly = false) => {
|
||||
return storeGlobal(Tutorial.MULTI_SERVER, 'true', prepareRecordsOnly);
|
||||
};
|
||||
|
||||
export const storeProfileLongPressTutorial = async (prepareRecordsOnly = false) => {
|
||||
return storeGlobal(Tutorial.PROFILE_LONG_PRESS, 'true', prepareRecordsOnly);
|
||||
};
|
||||
|
||||
export const storeSkinEmojiSelectorTutorial = async (prepareRecordsOnly = false) => {
|
||||
return storeGlobal(Tutorial.EMOJI_SKIN_SELECTOR, 'true', prepareRecordsOnly);
|
||||
return storeGlobal(GLOBAL_IDENTIFIERS.PROFILE_LONG_PRESS_TUTORIAL, 'true', prepareRecordsOnly);
|
||||
};
|
||||
|
||||
export const storeDontAskForReview = async (prepareRecordsOnly = false) => {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Model} from '@nozbe/watermelondb';
|
||||
|
||||
import {CHANNELS_CATEGORY, DMS_CATEGORY} from '@constants/categories';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {prepareCategoryChannels, queryCategoriesByTeamIds, getCategoryById, prepareCategoriesAndCategoriesChannels} from '@queries/servers/categories';
|
||||
@@ -9,7 +11,6 @@ import {queryMyTeams} from '@queries/servers/team';
|
||||
import {isDMorGM} from '@utils/channel';
|
||||
import {logError} from '@utils/log';
|
||||
|
||||
import type {Model} from '@nozbe/watermelondb';
|
||||
import type ChannelModel from '@typings/database/models/servers/channel';
|
||||
|
||||
export const deleteCategory = async (serverUrl: string, categoryId: string) => {
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Database} from '@nozbe/watermelondb';
|
||||
import {DeviceEventEmitter} from 'react-native';
|
||||
|
||||
import {Navigation} from '@constants';
|
||||
import {SYSTEM_IDENTIFIERS} from '@constants/database';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import ServerDataOperator from '@database/operator/server_data_operator';
|
||||
import {getMyChannel} from '@queries/servers/channel';
|
||||
import {getCommonSystemValues, getTeamHistory} from '@queries/servers/system';
|
||||
import {getTeamChannelHistory} from '@queries/servers/team';
|
||||
@@ -13,9 +15,6 @@ import {dismissAllModalsAndPopToRoot, dismissAllModalsAndPopToScreen} from '@scr
|
||||
|
||||
import {switchToChannel} from './channel';
|
||||
|
||||
import type ServerDataOperator from '@database/operator/server_data_operator';
|
||||
import type {Database} from '@nozbe/watermelondb';
|
||||
|
||||
let mockIsTablet: jest.Mock;
|
||||
const now = new Date('2020-01-01').getTime();
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Model} from '@nozbe/watermelondb';
|
||||
import {DeviceEventEmitter} from 'react-native';
|
||||
|
||||
import {General, Navigation as NavigationConstants, Preferences, Screens} from '@constants';
|
||||
@@ -23,7 +24,6 @@ import {isTablet} from '@utils/helpers';
|
||||
import {logError, logInfo} from '@utils/log';
|
||||
import {displayGroupMessageName, displayUsername, getUserIdFromChannelName} from '@utils/user';
|
||||
|
||||
import type {Model} from '@nozbe/watermelondb';
|
||||
import type ChannelModel from '@typings/database/models/servers/channel';
|
||||
import type UserModel from '@typings/database/models/servers/user';
|
||||
|
||||
|
||||
@@ -18,12 +18,9 @@ export async function updateDraftFile(serverUrl: string, channelId: string, root
|
||||
return {error: 'file not found'};
|
||||
}
|
||||
|
||||
file.is_voice_recording = draft.files[i].is_voice_recording;
|
||||
|
||||
// We create a new list to make sure we re-render the draft input.
|
||||
const newFiles = [...draft.files];
|
||||
newFiles[i] = file;
|
||||
|
||||
draft.prepareUpdate((d) => {
|
||||
d.files = newFiles;
|
||||
});
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
import {fetchPostAuthors} from '@actions/remote/post';
|
||||
import {ActionType, Post} from '@constants';
|
||||
import {MM_TABLES} from '@constants/database';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {getPostById, prepareDeletePost, queryPostsById} from '@queries/servers/post';
|
||||
import {getCurrentUserId} from '@queries/servers/system';
|
||||
@@ -19,8 +18,6 @@ import type MyChannelModel from '@typings/database/models/servers/my_channel';
|
||||
import type PostModel from '@typings/database/models/servers/post';
|
||||
import type UserModel from '@typings/database/models/servers/user';
|
||||
|
||||
const {SERVER: {DRAFT, FILE, POST, POSTS_IN_THREAD, REACTION, THREAD, THREAD_PARTICIPANT, THREADS_IN_TEAM}} = MM_TABLES;
|
||||
|
||||
export const sendAddToChannelEphemeralPost = async (serverUrl: string, user: UserModel, addedUsernames: string[], messages: string[], channeId: string, postRootId = '') => {
|
||||
try {
|
||||
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
@@ -247,31 +244,3 @@ export async function getPosts(serverUrl: string, ids: string[]) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export async function deletePosts(serverUrl: string, postIds: string[]) {
|
||||
try {
|
||||
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
|
||||
const postsFormatted = `'${postIds.join("','")}'`;
|
||||
|
||||
await database.write(() => {
|
||||
return database.adapter.unsafeExecute({
|
||||
sqls: [
|
||||
[`DELETE FROM ${POST} where id IN (${postsFormatted})`, []],
|
||||
[`DELETE FROM ${REACTION} where post_id IN (${postsFormatted})`, []],
|
||||
[`DELETE FROM ${FILE} where post_id IN (${postsFormatted})`, []],
|
||||
[`DELETE FROM ${DRAFT} where root_id IN (${postsFormatted})`, []],
|
||||
|
||||
[`DELETE FROM ${POSTS_IN_THREAD} where root_id IN (${postsFormatted})`, []],
|
||||
|
||||
[`DELETE FROM ${THREAD} where id IN (${postsFormatted})`, []],
|
||||
[`DELETE FROM ${THREAD_PARTICIPANT} where thread_id IN (${postsFormatted})`, []],
|
||||
[`DELETE FROM ${THREADS_IN_TEAM} where thread_id IN (${postsFormatted})`, []],
|
||||
],
|
||||
});
|
||||
});
|
||||
return {error: false};
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +1,14 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Q} from '@nozbe/watermelondb';
|
||||
import deepEqual from 'deep-equal';
|
||||
|
||||
import {MM_TABLES, SYSTEM_IDENTIFIERS} from '@constants/database';
|
||||
import {SYSTEM_IDENTIFIERS} from '@constants/database';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {getServerCredentials} from '@init/credentials';
|
||||
import {queryAllChannelsForTeam} from '@queries/servers/channel';
|
||||
import {getConfig, getLicense, getGlobalDataRetentionPolicy, getGranularDataRetentionPolicies, getLastGlobalDataRetentionRun, getIsDataRetentionEnabled} from '@queries/servers/system';
|
||||
import {getConfig, getLicense} from '@queries/servers/system';
|
||||
import {logError} from '@utils/log';
|
||||
|
||||
import {deletePosts} from './post';
|
||||
|
||||
import type {DataRetentionPoliciesRequest} from '@actions/remote/systems';
|
||||
import type PostModel from '@typings/database/models/servers/post';
|
||||
|
||||
const {SERVER: {POST}} = MM_TABLES;
|
||||
|
||||
export async function storeConfigAndLicense(serverUrl: string, config: ClientConfig, license: ClientLicense) {
|
||||
try {
|
||||
// If we have credentials for this server then update the values in the database
|
||||
@@ -83,155 +74,6 @@ export async function storeConfig(serverUrl: string, config: ClientConfig | unde
|
||||
return [];
|
||||
}
|
||||
|
||||
export async function storeDataRetentionPolicies(serverUrl: string, data: DataRetentionPoliciesRequest, prepareRecordsOnly = false) {
|
||||
try {
|
||||
const {globalPolicy, teamPolicies, channelPolicies} = data;
|
||||
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const systems: IdValue[] = [{
|
||||
id: SYSTEM_IDENTIFIERS.DATA_RETENTION_POLICIES,
|
||||
value: globalPolicy || {},
|
||||
}, {
|
||||
id: SYSTEM_IDENTIFIERS.GRANULAR_DATA_RETENTION_POLICIES,
|
||||
value: {
|
||||
team: teamPolicies || [],
|
||||
channel: channelPolicies || [],
|
||||
},
|
||||
}];
|
||||
|
||||
return operator.handleSystem({
|
||||
systems,
|
||||
prepareRecordsOnly,
|
||||
});
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateLastDataRetentionRun(serverUrl: string, value?: number, prepareRecordsOnly = false) {
|
||||
try {
|
||||
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
|
||||
const systems: IdValue[] = [{
|
||||
id: SYSTEM_IDENTIFIERS.LAST_DATA_RETENTION_RUN,
|
||||
value: value || Date.now(),
|
||||
}];
|
||||
|
||||
return operator.handleSystem({systems, prepareRecordsOnly});
|
||||
} catch (error) {
|
||||
logError('Failed updateLastDataRetentionRun', error);
|
||||
return {error};
|
||||
}
|
||||
}
|
||||
|
||||
export async function dataRetentionCleanup(serverUrl: string) {
|
||||
try {
|
||||
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
|
||||
const isDataRetentionEnabled = await getIsDataRetentionEnabled(database);
|
||||
if (!isDataRetentionEnabled) {
|
||||
return {error: undefined};
|
||||
}
|
||||
|
||||
const lastRunAt = await getLastGlobalDataRetentionRun(database);
|
||||
const lastCleanedToday = new Date(lastRunAt).toDateString() === new Date().toDateString();
|
||||
|
||||
// Do not run if clean up is already done today
|
||||
if (lastRunAt && lastCleanedToday) {
|
||||
return {error: undefined};
|
||||
}
|
||||
|
||||
const globalPolicy = await getGlobalDataRetentionPolicy(database);
|
||||
const granularPoliciesData = await getGranularDataRetentionPolicies(database);
|
||||
|
||||
// Get global data retention cutoff
|
||||
let globalRetentionCutoff = 0;
|
||||
if (globalPolicy?.message_deletion_enabled) {
|
||||
globalRetentionCutoff = globalPolicy.message_retention_cutoff;
|
||||
}
|
||||
|
||||
// Get Granular data retention policies
|
||||
let teamPolicies: TeamDataRetentionPolicy[] = [];
|
||||
let channelPolicies: ChannelDataRetentionPolicy[] = [];
|
||||
if (granularPoliciesData) {
|
||||
teamPolicies = granularPoliciesData.team;
|
||||
channelPolicies = granularPoliciesData.channel;
|
||||
}
|
||||
|
||||
const channelsCutoffs: {[key: string]: number} = {};
|
||||
|
||||
// Get channel level cutoff from team policies
|
||||
for await (const teamPolicy of teamPolicies) {
|
||||
const {team_id, post_duration} = teamPolicy;
|
||||
const channelIds = await queryAllChannelsForTeam(database, team_id).fetchIds();
|
||||
if (channelIds.length) {
|
||||
const cutoff = getDataRetentionPolicyCutoff(post_duration);
|
||||
channelIds.forEach((channelId) => {
|
||||
channelsCutoffs[channelId] = cutoff;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Get channel level cutoff from channel policies
|
||||
channelPolicies.forEach(({channel_id, post_duration}) => {
|
||||
channelsCutoffs[channel_id] = getDataRetentionPolicyCutoff(post_duration);
|
||||
});
|
||||
|
||||
const conditions = [];
|
||||
|
||||
const channelIds = Object.keys(channelsCutoffs);
|
||||
if (channelIds.length) {
|
||||
// Fetch posts by channel level cutoff
|
||||
for (const channelId of channelIds) {
|
||||
const cutoff = channelsCutoffs[channelId];
|
||||
conditions.push(`(channel_id='${channelId}' AND create_at < ${cutoff})`);
|
||||
}
|
||||
|
||||
// Fetch posts by global cutoff which are not already fetched by channel level cutoff
|
||||
conditions.push(`(channel_id NOT IN ('${channelIds.join("','")}') AND create_at < ${globalRetentionCutoff})`);
|
||||
} else {
|
||||
conditions.push(`create_at < ${globalRetentionCutoff}`);
|
||||
}
|
||||
|
||||
const postIds = await database.get<PostModel>(POST).query(
|
||||
Q.unsafeSqlQuery(`SELECT * FROM ${POST} where ${conditions.join(' OR ')}`),
|
||||
).fetchIds();
|
||||
|
||||
if (postIds.length) {
|
||||
const batchSize = 1000;
|
||||
const deletePromises = [];
|
||||
for (let i = 0; i < postIds.length; i += batchSize) {
|
||||
const batch = postIds.slice(i, batchSize);
|
||||
deletePromises.push(
|
||||
deletePosts(serverUrl, batch),
|
||||
);
|
||||
}
|
||||
const deleteResult = await Promise.all(deletePromises);
|
||||
for (const {error} of deleteResult) {
|
||||
if (error) {
|
||||
return {error};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await updateLastDataRetentionRun(serverUrl);
|
||||
|
||||
return {error: undefined};
|
||||
} catch (error) {
|
||||
logError('An error occurred while performing data retention cleanup', error);
|
||||
return {error};
|
||||
}
|
||||
}
|
||||
|
||||
// Returns cutoff time based on the policy's post_duration
|
||||
function getDataRetentionPolicyCutoff(postDuration: number) {
|
||||
const periodDate = new Date();
|
||||
periodDate.setDate(periodDate.getDate() - postDuration);
|
||||
periodDate.setHours(0);
|
||||
periodDate.setMinutes(0);
|
||||
periodDate.setSeconds(0);
|
||||
return periodDate.getTime();
|
||||
}
|
||||
|
||||
export async function setLastServerVersionCheck(serverUrl: string, reset = false) {
|
||||
try {
|
||||
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {IntlShape} from 'react-intl';
|
||||
|
||||
import {sendEphemeralPost} from '@actions/local/post';
|
||||
import ClientError from '@client/rest/error';
|
||||
import {AppCallResponseTypes} from '@constants/apps';
|
||||
import NetworkManager from '@managers/network_manager';
|
||||
import {cleanForm, createCallRequest, makeCallErrorResponse} from '@utils/apps';
|
||||
|
||||
import type {Client} from '@client/rest';
|
||||
import type ClientError from '@client/rest/error';
|
||||
import type PostModel from '@typings/database/models/servers/post';
|
||||
import type {IntlShape} from 'react-intl';
|
||||
|
||||
export async function handleBindingClick<Res=unknown>(serverUrl: string, binding: AppBinding, context: AppContext, intl: IntlShape): Promise<{data?: AppCallResponse<Res>; error?: AppCallResponse<Res>}> {
|
||||
// Fetch form
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
/* eslint-disable max-lines */
|
||||
import {Model} from '@nozbe/watermelondb';
|
||||
import {IntlShape} from 'react-intl';
|
||||
import {DeviceEventEmitter} from 'react-native';
|
||||
|
||||
import {addChannelToDefaultCategory, storeCategories} from '@actions/local/category';
|
||||
@@ -38,9 +40,7 @@ import {addCurrentUserToTeam, fetchTeamByName, removeCurrentUserFromTeam} from '
|
||||
import {fetchProfilesInGroupChannels, fetchProfilesPerChannels, fetchUsersByIds, updateUsersNoLongerVisible} from './user';
|
||||
|
||||
import type {Client} from '@client/rest';
|
||||
import type {Model} from '@nozbe/watermelondb';
|
||||
import type ChannelModel from '@typings/database/models/servers/channel';
|
||||
import type {IntlShape} from 'react-intl';
|
||||
|
||||
export type MyChannelsRequest = {
|
||||
categories?: CategoryWithChannels[];
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {IntlShape} from 'react-intl';
|
||||
import {Alert} from 'react-native';
|
||||
|
||||
import {doAppSubmit, postEphemeralCallResponseForCommandArgs} from '@actions/remote/apps';
|
||||
import {Client} from '@client/rest';
|
||||
import {AppCommandParser} from '@components/autocomplete/slash_suggestion/app_command_parser/app_command_parser';
|
||||
import {AppCallResponseTypes} from '@constants/apps';
|
||||
import DatabaseManager from '@database/manager';
|
||||
@@ -16,9 +18,6 @@ import {showAppForm} from '@screens/navigation';
|
||||
import {handleDeepLink, matchDeepLink} from '@utils/deep_link';
|
||||
import {tryOpenURL} from '@utils/url';
|
||||
|
||||
import type {Client} from '@client/rest';
|
||||
import type {IntlShape} from 'react-intl';
|
||||
|
||||
export const executeCommand = async (serverUrl: string, intl: IntlShape, message: string, channelId: string, rootId?: string): Promise<{data?: CommandResponse; error?: string | {message: string}}> => {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
|
||||
@@ -2,14 +2,13 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {forceLogoutIfNecessary} from '@actions/remote/session';
|
||||
import {Client} from '@client/rest';
|
||||
import {Emoji, General} from '@constants';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {debounce} from '@helpers/api/general';
|
||||
import NetworkManager from '@managers/network_manager';
|
||||
import {queryCustomEmojisByName} from '@queries/servers/custom_emoji';
|
||||
|
||||
import type {Client} from '@client/rest';
|
||||
|
||||
export const fetchCustomEmojis = async (serverUrl: string, page = 0, perPage = General.PAGE_SIZE_DEFAULT, sort = Emoji.SORT_BY_NAME) => {
|
||||
let client: Client;
|
||||
try {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {dataRetentionCleanup, setLastServerVersionCheck} from '@actions/local/systems';
|
||||
import {setLastServerVersionCheck} from '@actions/local/systems';
|
||||
import {fetchConfigAndLicense} from '@actions/remote/systems';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {prepareCommonSystemValues, getCurrentTeamId, getWebSocketLastDisconnected, getCurrentChannelId, getConfig, getLicense} from '@queries/servers/system';
|
||||
@@ -27,9 +27,6 @@ export async function appEntry(serverUrl: string, since = 0, isUpgrade = false)
|
||||
}
|
||||
}
|
||||
|
||||
// Run data retention cleanup
|
||||
await dataRetentionCleanup(serverUrl);
|
||||
|
||||
// clear lastUnreadChannelId
|
||||
const removeLastUnreadChannelId = await prepareCommonSystemValues(operator, {lastUnreadChannelId: ''});
|
||||
if (removeLastUnreadChannelId) {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {dataRetentionCleanup} from '@actions/local/systems';
|
||||
import {Database, Model} from '@nozbe/watermelondb';
|
||||
|
||||
import {fetchMissingDirectChannelsInfo, fetchMyChannelsForTeam, handleKickFromChannel, MyChannelsRequest} from '@actions/remote/channel';
|
||||
import {fetchGroupsForMember} from '@actions/remote/groups';
|
||||
import {fetchPostsForUnreadChannels} from '@actions/remote/post';
|
||||
@@ -36,7 +37,6 @@ import {logDebug} from '@utils/log';
|
||||
import {processIsCRTEnabled} from '@utils/thread';
|
||||
|
||||
import type ClientError from '@client/rest/error';
|
||||
import type {Database, Model} from '@nozbe/watermelondb';
|
||||
|
||||
export type AppEntryData = {
|
||||
initialTeamId: string;
|
||||
@@ -378,9 +378,7 @@ export const syncOtherServers = async (serverUrl: string) => {
|
||||
for (const server of servers) {
|
||||
if (server.url !== serverUrl && server.lastActiveAt > 0) {
|
||||
registerDeviceToken(server.url);
|
||||
syncAllChannelMembersAndThreads(server.url).then(() => {
|
||||
dataRetentionCleanup(server.url);
|
||||
});
|
||||
syncAllChannelMembersAndThreads(server.url);
|
||||
autoUpdateTimezone(server.url);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Database} from '@nozbe/watermelondb';
|
||||
|
||||
import {storeConfigAndLicense} from '@actions/local/systems';
|
||||
import {MyChannelsRequest} from '@actions/remote/channel';
|
||||
import {fetchGroupsForMember} from '@actions/remote/groups';
|
||||
import {fetchPostsForUnreadChannels} from '@actions/remote/post';
|
||||
import {fetchDataRetentionPolicy} from '@actions/remote/systems';
|
||||
import {MyTeamsRequest, updateCanJoinTeams} from '@actions/remote/team';
|
||||
import {syncTeamThreads} from '@actions/remote/thread';
|
||||
import {autoUpdateTimezone, updateAllUsersSince} from '@actions/remote/user';
|
||||
@@ -16,16 +18,14 @@ import {selectDefaultTeam} from '@helpers/api/team';
|
||||
import {queryAllChannels, queryAllChannelsForTeam} from '@queries/servers/channel';
|
||||
import {prepareModels, truncateCrtRelatedTables} from '@queries/servers/entry';
|
||||
import {getHasCRTChanged} from '@queries/servers/preference';
|
||||
import {getConfig, getIsDataRetentionEnabled} from '@queries/servers/system';
|
||||
import {getConfig} from '@queries/servers/system';
|
||||
import {filterAndTransformRoles, getMemberChannelsFromGQLQuery, getMemberTeamsFromGQLQuery, gqlToClientChannelMembership, gqlToClientPreference, gqlToClientSidebarCategory, gqlToClientTeamMembership, gqlToClientUser} from '@utils/graphql';
|
||||
import {logDebug} from '@utils/log';
|
||||
import {processIsCRTEnabled} from '@utils/thread';
|
||||
|
||||
import {teamsToRemove, FETCH_UNREADS_TIMEOUT, entryRest, EntryResponse, entryInitialChannelId, restDeferredAppEntryActions, getRemoveTeamIds} from './common';
|
||||
|
||||
import type {MyChannelsRequest} from '@actions/remote/channel';
|
||||
import type ClientError from '@client/rest/error';
|
||||
import type {Database} from '@nozbe/watermelondb';
|
||||
import type ChannelModel from '@typings/database/models/servers/channel';
|
||||
|
||||
export async function deferredAppEntryGraphQLActions(
|
||||
@@ -265,12 +265,6 @@ export const entry = async (serverUrl: string, teamId?: string, channelId?: stri
|
||||
result = entryRest(serverUrl, teamId, channelId, since);
|
||||
}
|
||||
|
||||
// Fetch data retention policies
|
||||
const isDataRetentionEnabled = await getIsDataRetentionEnabled(database);
|
||||
if (isDataRetentionEnabled) {
|
||||
fetchDataRetentionPolicy(serverUrl);
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,25 +1,20 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {ClientResponse, ClientResponseError} from '@mattermost/react-native-network-client';
|
||||
|
||||
import {Client} from '@client/rest';
|
||||
import ClientError from '@client/rest/error';
|
||||
import {DOWNLOAD_TIMEOUT} from '@constants/network';
|
||||
import NetworkManager from '@managers/network_manager';
|
||||
|
||||
import {forceLogoutIfNecessary} from './session';
|
||||
|
||||
import type {Client} from '@client/rest';
|
||||
import type ClientError from '@client/rest/error';
|
||||
import type {ClientResponse, ClientResponseError} from '@mattermost/react-native-network-client';
|
||||
|
||||
export const downloadFile = (serverUrl: string, fileId: string, desitnation: string) => { // Let it throw and handle it accordingly
|
||||
const client = NetworkManager.getClient(serverUrl);
|
||||
return client.apiClient.download(client.getFileRoute(fileId), desitnation.replace('file://', ''), {timeoutInterval: DOWNLOAD_TIMEOUT});
|
||||
};
|
||||
|
||||
export const downloadProfileImage = (serverUrl: string, userId: string, lastPictureUpdate: number, destination: string) => { // Let it throw and handle it accordingly
|
||||
const client = NetworkManager.getClient(serverUrl);
|
||||
return client.apiClient.download(client.getProfilePictureUrl(userId, lastPictureUpdate), destination.replace('file://', ''), {timeoutInterval: DOWNLOAD_TIMEOUT});
|
||||
};
|
||||
|
||||
export const uploadFile = (
|
||||
serverUrl: string,
|
||||
file: FileInfo,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Client} from '@client/rest';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import NetworkManager from '@managers/network_manager';
|
||||
import {getChannelById} from '@queries/servers/channel';
|
||||
@@ -8,8 +9,6 @@ import {getTeamById} from '@queries/servers/team';
|
||||
|
||||
import {forceLogoutIfNecessary} from './session';
|
||||
|
||||
import type {Client} from '@client/rest';
|
||||
|
||||
export const fetchGroup = async (serverUrl: string, id: string, fetchOnly = false) => {
|
||||
try {
|
||||
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
|
||||
@@ -444,7 +444,7 @@ export async function fetchPostsBefore(serverUrl: string, channelId: string, pos
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
|
||||
if (activeServerUrl === serverUrl) {
|
||||
DeviceEventEmitter.emit(Events.LOADING_CHANNEL_POSTS, false);
|
||||
DeviceEventEmitter.emit(Events.LOADING_CHANNEL_POSTS, true);
|
||||
}
|
||||
return {error};
|
||||
}
|
||||
|
||||
@@ -178,19 +178,3 @@ export const setDirectChannelVisible = async (serverUrl: string, channelId: stri
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
|
||||
export const savePreferredSkinTone = async (serverUrl: string, skinCode: string) => {
|
||||
try {
|
||||
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const userId = await getCurrentUserId(database);
|
||||
const pref: PreferenceType = {
|
||||
user_id: userId,
|
||||
category: Preferences.CATEGORY_EMOJI,
|
||||
name: Preferences.EMOJI_SKINTONE,
|
||||
value: skinCode,
|
||||
};
|
||||
return savePreference(serverUrl, [pref]);
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Model} from '@nozbe/watermelondb';
|
||||
|
||||
import {addRecentReaction} from '@actions/local/reactions';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import NetworkManager from '@managers/network_manager';
|
||||
@@ -12,7 +14,6 @@ import {getEmojiFirstAlias} from '@utils/emoji/helpers';
|
||||
import {forceLogoutIfNecessary} from './session';
|
||||
|
||||
import type {Client} from '@client/rest';
|
||||
import type {Model} from '@nozbe/watermelondb';
|
||||
import type PostModel from '@typings/database/models/servers/post';
|
||||
|
||||
export async function addReaction(serverUrl: string, postId: string, emojiName: string) {
|
||||
|
||||
@@ -21,6 +21,7 @@ import {scheduleExpiredNotification} from '@utils/notification';
|
||||
import {getCSRFFromCookie} from '@utils/security';
|
||||
|
||||
import {loginEntry} from './entry';
|
||||
import {fetchDataRetentionPolicy} from './systems';
|
||||
|
||||
import type ClientError from '@client/rest/error';
|
||||
import type {LoginArgs} from '@typings/database/database';
|
||||
@@ -41,6 +42,11 @@ export const completeLogin = async (serverUrl: string) => {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Data retention
|
||||
if (config?.DataRetentionEnableMessageDeletion === 'true' && license?.IsLicensed === 'true' && license?.DataRetention === 'true') {
|
||||
fetchDataRetentionPolicy(serverUrl);
|
||||
}
|
||||
|
||||
await DatabaseManager.setActiveServerDatabase(serverUrl);
|
||||
|
||||
const systems: IdValue[] = [];
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {storeConfigAndLicense, storeDataRetentionPolicies} from '@actions/local/systems';
|
||||
import {storeConfigAndLicense} from '@actions/local/systems';
|
||||
import {forceLogoutIfNecessary} from '@actions/remote/session';
|
||||
import {SYSTEM_IDENTIFIERS} from '@constants/database';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import NetworkManager from '@managers/network_manager';
|
||||
import {getCurrentUserId} from '@queries/servers/system';
|
||||
import {logError} from '@utils/log';
|
||||
|
||||
import type ClientError from '@client/rest/error';
|
||||
|
||||
@@ -15,47 +16,7 @@ export type ConfigAndLicenseRequest = {
|
||||
error?: unknown;
|
||||
}
|
||||
|
||||
export type DataRetentionPoliciesRequest = {
|
||||
globalPolicy?: GlobalDataRetentionPolicy;
|
||||
teamPolicies?: TeamDataRetentionPolicy[];
|
||||
channelPolicies?: ChannelDataRetentionPolicy[];
|
||||
error?: unknown;
|
||||
}
|
||||
|
||||
export const fetchDataRetentionPolicy = async (serverUrl: string, fetchOnly = false): Promise<DataRetentionPoliciesRequest> => {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
|
||||
try {
|
||||
const {data: globalPolicy, error: globalPolicyError} = await fetchGlobalDataRetentionPolicy(serverUrl);
|
||||
const {data: teamPolicies, error: teamPoliciesError} = await fetchAllGranularDataRetentionPolicies(serverUrl);
|
||||
const {data: channelPolicies, error: channelPoliciesError} = await fetchAllGranularDataRetentionPolicies(serverUrl, true);
|
||||
|
||||
const hasError = globalPolicyError || teamPoliciesError || channelPoliciesError;
|
||||
if (hasError) {
|
||||
return hasError;
|
||||
}
|
||||
|
||||
const data = {
|
||||
globalPolicy,
|
||||
teamPolicies: teamPolicies as TeamDataRetentionPolicy[],
|
||||
channelPolicies: channelPolicies as ChannelDataRetentionPolicy[],
|
||||
};
|
||||
|
||||
if (!fetchOnly) {
|
||||
await storeDataRetentionPolicies(serverUrl, data);
|
||||
}
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(serverUrl, error as ClientError);
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
|
||||
export const fetchGlobalDataRetentionPolicy = async (serverUrl: string): Promise<{data?: GlobalDataRetentionPolicy; error?: unknown}> => {
|
||||
export const fetchDataRetentionPolicy = async (serverUrl: string) => {
|
||||
let client;
|
||||
try {
|
||||
client = NetworkManager.getClient(serverUrl);
|
||||
@@ -63,47 +24,28 @@ export const fetchGlobalDataRetentionPolicy = async (serverUrl: string): Promise
|
||||
return {error};
|
||||
}
|
||||
|
||||
let data = {};
|
||||
try {
|
||||
const data = await client.getGlobalDataRetentionPolicy();
|
||||
return {data};
|
||||
data = await client.getDataRetentionPolicy();
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(serverUrl, error as ClientError);
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
|
||||
export const fetchAllGranularDataRetentionPolicies = async (
|
||||
serverUrl: string,
|
||||
isChannel = false,
|
||||
page = 0,
|
||||
policies: Array<TeamDataRetentionPolicy | ChannelDataRetentionPolicy> = [],
|
||||
): Promise<{data?: Array<TeamDataRetentionPolicy | ChannelDataRetentionPolicy>; error?: unknown}> => {
|
||||
let client;
|
||||
try {
|
||||
client = NetworkManager.getClient(serverUrl);
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
if (operator) {
|
||||
const systems: IdValue[] = [{
|
||||
id: SYSTEM_IDENTIFIERS.DATA_RETENTION_POLICIES,
|
||||
value: JSON.stringify(data),
|
||||
}];
|
||||
|
||||
operator.handleSystem({systems, prepareRecordsOnly: false}).
|
||||
catch((error) => {
|
||||
logError('An error occurred while saving data retention policies', error);
|
||||
});
|
||||
}
|
||||
|
||||
const {database} = operator;
|
||||
|
||||
const currentUserId = await getCurrentUserId(database);
|
||||
let data;
|
||||
if (isChannel) {
|
||||
data = await client.getChannelDataRetentionPolicies(currentUserId, page);
|
||||
} else {
|
||||
data = await client.getTeamDataRetentionPolicies(currentUserId, page);
|
||||
}
|
||||
policies.push(...data.policies);
|
||||
if (policies.length < data.total_count) {
|
||||
await fetchAllGranularDataRetentionPolicies(serverUrl, isChannel, page + 1, policies);
|
||||
}
|
||||
return {data: policies};
|
||||
return data;
|
||||
};
|
||||
|
||||
export const fetchConfigAndLicense = async (serverUrl: string, fetchOnly = false): Promise<ConfigAndLicenseRequest> => {
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Model} from '@nozbe/watermelondb';
|
||||
import {DeviceEventEmitter} from 'react-native';
|
||||
|
||||
import {removeUserFromTeam as localRemoveUserFromTeam} from '@actions/local/team';
|
||||
import {Client} from '@client/rest';
|
||||
import {PER_PAGE_DEFAULT} from '@client/rest/constants';
|
||||
import {Events} from '@constants';
|
||||
import DatabaseManager from '@database/manager';
|
||||
@@ -25,9 +27,7 @@ import {fetchPostsForChannel, fetchPostsForUnreadChannels} from './post';
|
||||
import {fetchRolesIfNeeded} from './role';
|
||||
import {forceLogoutIfNecessary} from './session';
|
||||
|
||||
import type {Client} from '@client/rest';
|
||||
import type ClientError from '@client/rest/error';
|
||||
import type {Model} from '@nozbe/watermelondb';
|
||||
|
||||
export type MyTeamsRequest = {
|
||||
teams?: Team[];
|
||||
@@ -114,54 +114,6 @@ export async function addUserToTeam(serverUrl: string, teamId: string, userId: s
|
||||
}
|
||||
}
|
||||
|
||||
export async function addUsersToTeam(serverUrl: string, teamId: string, userIds: string[], fetchOnly = false) {
|
||||
try {
|
||||
const client = NetworkManager.getClient(serverUrl);
|
||||
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
EphemeralStore.startAddingToTeam(teamId);
|
||||
|
||||
const members = await client.addUsersToTeamGracefully(teamId, userIds);
|
||||
|
||||
if (!fetchOnly) {
|
||||
const teamMemberships: TeamMembership[] = [];
|
||||
const roles = [];
|
||||
|
||||
for (const {member} of members) {
|
||||
teamMemberships.push(member);
|
||||
roles.push(...member.roles.split(' '));
|
||||
}
|
||||
|
||||
fetchRolesIfNeeded(serverUrl, Array.from(new Set(roles)));
|
||||
|
||||
if (operator) {
|
||||
await operator.handleTeamMemberships({teamMemberships, prepareRecordsOnly: true});
|
||||
}
|
||||
}
|
||||
|
||||
EphemeralStore.finishAddingToTeam(teamId);
|
||||
return {members};
|
||||
} catch (error) {
|
||||
if (EphemeralStore.isAddingToTeam(teamId)) {
|
||||
EphemeralStore.finishAddingToTeam(teamId);
|
||||
}
|
||||
|
||||
forceLogoutIfNecessary(serverUrl, error as ClientError);
|
||||
return {error};
|
||||
}
|
||||
}
|
||||
|
||||
export async function sendEmailInvitesToTeam(serverUrl: string, teamId: string, emails: string[]) {
|
||||
try {
|
||||
const client = NetworkManager.getClient(serverUrl);
|
||||
const members = await client.sendEmailInvitesToTeamGracefully(teamId, emails);
|
||||
|
||||
return {members};
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(serverUrl, error as ClientError);
|
||||
return {error};
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchMyTeams(serverUrl: string, fetchOnly = false): Promise<MyTeamsRequest> {
|
||||
let client;
|
||||
try {
|
||||
@@ -476,28 +428,3 @@ export async function handleKickFromTeam(serverUrl: string, teamId: string) {
|
||||
logDebug('Failed to kick user from team', error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getTeamMembersByIds(serverUrl: string, teamId: string, userIds: string[], fetchOnly?: boolean) {
|
||||
try {
|
||||
const client = NetworkManager.getClient(serverUrl);
|
||||
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const members = await client.getTeamMembersByIds(teamId, userIds);
|
||||
|
||||
if (!fetchOnly) {
|
||||
const roles = [];
|
||||
|
||||
for (const {roles: memberRoles} of members) {
|
||||
roles.push(...memberRoles.split(' '));
|
||||
}
|
||||
|
||||
fetchRolesIfNeeded(serverUrl, Array.from(new Set(roles)));
|
||||
|
||||
await operator.handleTeamMemberships({teamMemberships: members, prepareRecordsOnly: true});
|
||||
}
|
||||
|
||||
return {members};
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(serverUrl, error as ClientError);
|
||||
return {error};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import Model from '@nozbe/watermelondb/Model';
|
||||
|
||||
import {markTeamThreadsAsRead, markThreadAsViewed, processReceivedThreads, switchToThread, updateTeamThreadsSync, updateThread} from '@actions/local/thread';
|
||||
import {fetchPostThread} from '@actions/remote/post';
|
||||
import {General} from '@constants';
|
||||
@@ -17,7 +19,6 @@ import {getThreadsListEdges} from '@utils/thread';
|
||||
import {forceLogoutIfNecessary} from './session';
|
||||
|
||||
import type {Client} from '@client/rest';
|
||||
import type Model from '@nozbe/watermelondb/Model';
|
||||
|
||||
type FetchThreadsOptions = {
|
||||
before?: string;
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
/* eslint-disable max-lines */
|
||||
|
||||
import {Model} from '@nozbe/watermelondb';
|
||||
import {chunk} from 'lodash';
|
||||
|
||||
import {updateChannelsDisplayName} from '@actions/local/channel';
|
||||
@@ -25,7 +26,6 @@ import {forceLogoutIfNecessary} from './session';
|
||||
|
||||
import type {Client} from '@client/rest';
|
||||
import type ClientError from '@client/rest/error';
|
||||
import type {Model} from '@nozbe/watermelondb';
|
||||
import type UserModel from '@typings/database/models/servers/user';
|
||||
|
||||
export type MyUserRequest = {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Model} from '@nozbe/watermelondb';
|
||||
|
||||
import {addChannelToDefaultCategory} from '@actions/local/category';
|
||||
import {
|
||||
markChannelAsViewed, removeCurrentUserFromChannel, setChannelDeleteAt,
|
||||
@@ -20,8 +22,6 @@ import {getCurrentUser, getTeammateNameDisplay, getUserById} from '@queries/serv
|
||||
import EphemeralStore from '@store/ephemeral_store';
|
||||
import {logDebug} from '@utils/log';
|
||||
|
||||
import type {Model} from '@nozbe/watermelondb';
|
||||
|
||||
// Received when current user created a channel in a different client
|
||||
export async function handleChannelCreatedEvent(serverUrl: string, msg: any) {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Model} from '@nozbe/watermelondb';
|
||||
import {DeviceEventEmitter} from 'react-native';
|
||||
|
||||
import {storeMyChannelsForTeam, markChannelAsUnread, markChannelAsViewed, updateLastPostAt} from '@actions/local/channel';
|
||||
@@ -19,11 +20,12 @@ import NavigationStore from '@store/navigation_store';
|
||||
import {isTablet} from '@utils/helpers';
|
||||
import {isFromWebhook, isSystemMessage, shouldIgnorePost} from '@utils/post';
|
||||
|
||||
import type {Model} from '@nozbe/watermelondb';
|
||||
import type MyChannelModel from '@typings/database/models/servers/my_channel';
|
||||
|
||||
function preparedMyChannelHack(myChannel: MyChannelModel) {
|
||||
// @ts-expect-error hack accessing _preparedState
|
||||
if (!myChannel._preparedState) {
|
||||
// @ts-expect-error hack setting _preparedState
|
||||
myChannel._preparedState = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Model} from '@nozbe/watermelondb';
|
||||
|
||||
import {removeUserFromTeam} from '@actions/local/team';
|
||||
import {fetchMyChannelsForTeam} from '@actions/remote/channel';
|
||||
import {fetchRoles} from '@actions/remote/role';
|
||||
import {fetchMyTeam, handleKickFromTeam, updateCanJoinTeams} from '@actions/remote/team';
|
||||
import {updateUsersNoLongerVisible} from '@actions/remote/user';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import ServerDataOperator from '@database/operator/server_data_operator';
|
||||
import NetworkManager from '@managers/network_manager';
|
||||
import {prepareCategoriesAndCategoriesChannels} from '@queries/servers/categories';
|
||||
import {prepareMyChannelsForTeam} from '@queries/servers/channel';
|
||||
@@ -16,9 +19,6 @@ import EphemeralStore from '@store/ephemeral_store';
|
||||
import {setTeamLoading} from '@store/team_load_store';
|
||||
import {logDebug} from '@utils/log';
|
||||
|
||||
import type ServerDataOperator from '@database/operator/server_data_operator';
|
||||
import type {Model} from '@nozbe/watermelondb';
|
||||
|
||||
export async function handleTeamArchived(serverUrl: string, msg: WebSocketMessage) {
|
||||
try {
|
||||
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Model} from '@nozbe/watermelondb';
|
||||
import {DeviceEventEmitter} from 'react-native';
|
||||
|
||||
import {updateChannelsDisplayName} from '@actions/local/channel';
|
||||
@@ -15,8 +16,6 @@ import {getConfig, getLicense} from '@queries/servers/system';
|
||||
import {getCurrentUser} from '@queries/servers/user';
|
||||
import {displayUsername} from '@utils/user';
|
||||
|
||||
import type {Model} from '@nozbe/watermelondb';
|
||||
|
||||
export async function handleUserUpdatedEvent(serverUrl: string, msg: WebSocketMessage) {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import {Client} from '@client/rest';
|
||||
import {MEMBERS_PER_PAGE} from '@constants/graphql';
|
||||
import NetworkManager from '@managers/network_manager';
|
||||
|
||||
import QueryNames from './constants';
|
||||
|
||||
import type {Client} from '@client/rest';
|
||||
|
||||
const doGQLQuery = async (serverUrl: string, query: string, variables: {[name: string]: any}, operationName: string) => {
|
||||
let client: Client;
|
||||
try {
|
||||
|
||||
@@ -177,14 +177,10 @@ export default class ClientBase {
|
||||
return `${this.getEmojisRoute()}/${emojiId}`;
|
||||
}
|
||||
|
||||
getGlobalDataRetentionRoute() {
|
||||
getDataRetentionRoute() {
|
||||
return `${this.urlVersion}/data_retention`;
|
||||
}
|
||||
|
||||
getGranularDataRetentionRoute(userId: string) {
|
||||
return `${this.getUserRoute(userId)}/data_retention`;
|
||||
}
|
||||
|
||||
getRolesRoute() {
|
||||
return `${this.urlVersion}/roles`;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {toMilliseconds} from '@utils/datetime';
|
||||
import {ClientResponse, ClientResponseError, ProgressPromise, UploadRequestOptions} from '@mattermost/react-native-network-client';
|
||||
|
||||
import type {ClientResponse, ClientResponseError, ProgressPromise, UploadRequestOptions} from '@mattermost/react-native-network-client';
|
||||
import {toMilliseconds} from '@utils/datetime';
|
||||
|
||||
export interface ClientFilesMix {
|
||||
getFileUrl: (fileId: string, timestamp: number) => string;
|
||||
|
||||
@@ -3,14 +3,8 @@
|
||||
|
||||
import {buildQueryString} from '@utils/helpers';
|
||||
|
||||
import {PER_PAGE_DEFAULT} from './constants';
|
||||
import ClientError from './error';
|
||||
|
||||
type PoliciesResponse<T> = {
|
||||
policies: T[];
|
||||
total_count: number;
|
||||
}
|
||||
|
||||
export interface ClientGeneralMix {
|
||||
getOpenGraphMetadata: (url: string) => Promise<any>;
|
||||
ping: (deviceId?: string, timeoutInterval?: number) => Promise<any>;
|
||||
@@ -18,9 +12,7 @@ export interface ClientGeneralMix {
|
||||
getClientConfigOld: () => Promise<ClientConfig>;
|
||||
getClientLicenseOld: () => Promise<ClientLicense>;
|
||||
getTimezones: () => Promise<string[]>;
|
||||
getGlobalDataRetentionPolicy: () => Promise<GlobalDataRetentionPolicy>;
|
||||
getTeamDataRetentionPolicies: (userId: string, page?: number, perPage?: number) => Promise<PoliciesResponse<TeamDataRetentionPolicy>>;
|
||||
getChannelDataRetentionPolicies: (userId: string, page?: number, perPage?: number) => Promise<PoliciesResponse<ChannelDataRetentionPolicy>>;
|
||||
getDataRetentionPolicy: () => Promise<any>;
|
||||
getRolesByNames: (rolesNames: string[]) => Promise<Role[]>;
|
||||
getRedirectLocation: (urlParam: string) => Promise<Record<string, string>>;
|
||||
}
|
||||
@@ -82,23 +74,9 @@ const ClientGeneral = (superclass: any) => class extends superclass {
|
||||
);
|
||||
};
|
||||
|
||||
getGlobalDataRetentionPolicy = () => {
|
||||
getDataRetentionPolicy = () => {
|
||||
return this.doFetch(
|
||||
`${this.getGlobalDataRetentionRoute()}/policy`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
getTeamDataRetentionPolicies = (userId: string, page = 0, perPage = PER_PAGE_DEFAULT) => {
|
||||
return this.doFetch(
|
||||
`${this.getGranularDataRetentionRoute(userId)}/team_policies${buildQueryString({page, per_page: perPage})}`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
getChannelDataRetentionPolicies = (userId: string, page = 0, perPage = PER_PAGE_DEFAULT) => {
|
||||
return this.doFetch(
|
||||
`${this.getGranularDataRetentionRoute(userId)}/channel_policies${buildQueryString({page, per_page: perPage})}`,
|
||||
`${this.getDataRetentionRoute()}/policy`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
@@ -18,10 +18,7 @@ export interface ClientTeamsMix {
|
||||
getMyTeamMembers: () => Promise<TeamMembership[]>;
|
||||
getTeamMembers: (teamId: string, page?: number, perPage?: number) => Promise<TeamMembership[]>;
|
||||
getTeamMember: (teamId: string, userId: string) => Promise<TeamMembership>;
|
||||
getTeamMembersByIds: (teamId: string, userIds: string[]) => Promise<TeamMembership[]>;
|
||||
addToTeam: (teamId: string, userId: string) => Promise<TeamMembership>;
|
||||
addUsersToTeamGracefully: (teamId: string, userIds: string[]) => Promise<TeamMemberWithError[]>;
|
||||
sendEmailInvitesToTeamGracefully: (teamId: string, emails: string[]) => Promise<TeamInviteWithError[]>;
|
||||
joinTeam: (inviteId: string) => Promise<TeamMembership>;
|
||||
removeFromTeam: (teamId: string, userId: string) => Promise<any>;
|
||||
getTeamStats: (teamId: string) => Promise<any>;
|
||||
@@ -123,13 +120,6 @@ const ClientTeams = (superclass: any) => class extends superclass {
|
||||
);
|
||||
};
|
||||
|
||||
getTeamMembersByIds = (teamId: string, userIds: string[]) => {
|
||||
return this.doFetch(
|
||||
`${this.getTeamMembersRoute(teamId)}/ids`,
|
||||
{method: 'post', body: userIds},
|
||||
);
|
||||
};
|
||||
|
||||
addToTeam = async (teamId: string, userId: string) => {
|
||||
this.analytics.trackAPI('api_teams_invite_members', {team_id: teamId});
|
||||
|
||||
@@ -140,27 +130,6 @@ const ClientTeams = (superclass: any) => class extends superclass {
|
||||
);
|
||||
};
|
||||
|
||||
addUsersToTeamGracefully = (teamId: string, userIds: string[]) => {
|
||||
this.analytics.trackAPI('api_teams_batch_add_members', {team_id: teamId, count: userIds.length});
|
||||
|
||||
const members: Array<{team_id: string; user_id: string}> = [];
|
||||
userIds.forEach((id) => members.push({team_id: teamId, user_id: id}));
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getTeamMembersRoute(teamId)}/batch?graceful=true`,
|
||||
{method: 'post', body: members},
|
||||
);
|
||||
};
|
||||
|
||||
sendEmailInvitesToTeamGracefully = (teamId: string, emails: string[]) => {
|
||||
this.analytics.trackAPI('api_teams_invite_members', {team_id: teamId});
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getTeamRoute(teamId)}/invite/email?graceful=true`,
|
||||
{method: 'post', body: emails},
|
||||
);
|
||||
};
|
||||
|
||||
joinTeam = async (inviteId: string) => {
|
||||
const query = buildQueryString({invite_id: inviteId});
|
||||
return this.doFetch(
|
||||
|
||||
@@ -31,9 +31,8 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
elevation: 3,
|
||||
},
|
||||
shadow: {
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
shadowColor: '#000',
|
||||
shadowOpacity: 1,
|
||||
shadowOpacity: 0.12,
|
||||
shadowRadius: 6,
|
||||
shadowOffset: {
|
||||
width: 0,
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Database} from '@nozbe/watermelondb';
|
||||
import {IntlShape} from 'react-intl';
|
||||
|
||||
import {doAppFetchForm, doAppLookup} from '@actions/remote/apps';
|
||||
import {fetchChannelById, fetchChannelByName, searchChannels} from '@actions/remote/channel';
|
||||
import {fetchUsersByIds, fetchUsersByUsernames, searchUsers} from '@actions/remote/user';
|
||||
@@ -14,10 +17,8 @@ import {createCallRequest, filterEmptyOptions} from '@utils/apps';
|
||||
|
||||
import {getChannelSuggestions, getUserSuggestions, inTextMentionSuggestions} from './mentions';
|
||||
|
||||
import type {Database} from '@nozbe/watermelondb';
|
||||
import type ChannelModel from '@typings/database/models/servers/channel';
|
||||
import type UserModel from '@typings/database/models/servers/user';
|
||||
import type {IntlShape} from 'react-intl';
|
||||
|
||||
/* eslint-disable max-lines */
|
||||
|
||||
@@ -545,7 +546,7 @@ export class ParsedCommand {
|
||||
this.incomplete += c;
|
||||
this.i++;
|
||||
if (escaped) {
|
||||
//TODO: handle \n, \t, other escaped chars https://mattermost.atlassian.net/browse/MM-43476
|
||||
//TODO: handle \n, \t, other escaped chars
|
||||
escaped = false;
|
||||
}
|
||||
break;
|
||||
@@ -734,7 +735,7 @@ export class ParsedCommand {
|
||||
this.incomplete += c;
|
||||
this.i++;
|
||||
if (escaped) {
|
||||
//TODO: handle \n, \t, other escaped chars https://mattermost.atlassian.net/browse/MM-43476
|
||||
//TODO: handle \n, \t, other escaped chars
|
||||
escaped = false;
|
||||
}
|
||||
break;
|
||||
@@ -1078,7 +1079,7 @@ export class AppCommandParser {
|
||||
}
|
||||
|
||||
// Add "Execute Current Command" suggestion
|
||||
// TODO get full text from SuggestionBox https://mattermost.atlassian.net/browse/MM-43477
|
||||
// TODO get full text from SuggestionBox
|
||||
const executableStates: string[] = [
|
||||
ParseState.EndCommand,
|
||||
ParseState.CommandSeparator,
|
||||
|
||||
@@ -19,7 +19,7 @@ import IntegrationsManager from '@managers/integrations_manager';
|
||||
import {AppCommandParser} from './app_command_parser/app_command_parser';
|
||||
import SlashSuggestionItem from './slash_suggestion_item';
|
||||
|
||||
// TODO: Remove when all below commands have been implemented https://mattermost.atlassian.net/browse/MM-43478
|
||||
// TODO: Remove when all below commands have been implemented
|
||||
const COMMANDS_TO_IMPLEMENT_LATER = ['collapse', 'expand', 'logout'];
|
||||
const NON_MOBILE_COMMANDS = ['shortcuts', 'search', 'settings'];
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {MessageDescriptor} from '@formatjs/intl/src/types';
|
||||
import React from 'react';
|
||||
import {StyleProp, TextStyle, View, ViewStyle} from 'react-native';
|
||||
|
||||
@@ -8,8 +9,6 @@ import FormattedText from '@components/formatted_text';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
import type {MessageDescriptor} from '@formatjs/intl/src/types';
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
container: {
|
||||
|
||||
@@ -3,13 +3,12 @@
|
||||
|
||||
import React, {useCallback} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {StyleProp, ViewStyle} from 'react-native';
|
||||
|
||||
import OptionBox from '@components/option_box';
|
||||
import {Screens} from '@constants';
|
||||
import {dismissBottomSheet, goToScreen, showModal} from '@screens/navigation';
|
||||
|
||||
import type {StyleProp, ViewStyle} from 'react-native';
|
||||
|
||||
type Props = {
|
||||
channelId: string;
|
||||
containerStyle?: StyleProp<ViewStyle>;
|
||||
|
||||
@@ -3,14 +3,13 @@
|
||||
|
||||
import React, {useCallback} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {StyleProp, ViewStyle} from 'react-native';
|
||||
|
||||
import {toggleFavoriteChannel} from '@actions/remote/category';
|
||||
import OptionBox from '@components/option_box';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {dismissBottomSheet} from '@screens/navigation';
|
||||
|
||||
import type {StyleProp, ViewStyle} from 'react-native';
|
||||
|
||||
type Props = {
|
||||
channelId: string;
|
||||
containerStyle?: StyleProp<ViewStyle>;
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
import React, {useCallback} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {StyleProp, ViewStyle} from 'react-native';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import OptionBox from '@components/option_box';
|
||||
@@ -11,8 +12,6 @@ import {Screens} from '@constants';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {dismissBottomSheet, showModal} from '@screens/navigation';
|
||||
|
||||
import type {StyleProp, ViewStyle} from 'react-native';
|
||||
|
||||
type Props = {
|
||||
channelId: string;
|
||||
containerStyle?: StyleProp<ViewStyle>;
|
||||
|
||||
@@ -3,14 +3,13 @@
|
||||
|
||||
import React, {useCallback} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {StyleProp, ViewStyle} from 'react-native';
|
||||
|
||||
import {toggleMuteChannel} from '@actions/remote/channel';
|
||||
import OptionBox from '@components/option_box';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {dismissBottomSheet} from '@screens/navigation';
|
||||
|
||||
import type {StyleProp, ViewStyle} from 'react-native';
|
||||
|
||||
type Props = {
|
||||
channelId: string;
|
||||
containerStyle?: StyleProp<ViewStyle>;
|
||||
|
||||
@@ -3,13 +3,12 @@
|
||||
|
||||
import React, {useCallback} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {StyleProp, ViewStyle} from 'react-native';
|
||||
|
||||
import OptionBox from '@components/option_box';
|
||||
import {Screens} from '@constants';
|
||||
import {dismissBottomSheet, goToScreen, showModal} from '@screens/navigation';
|
||||
|
||||
import type {StyleProp, ViewStyle} from 'react-native';
|
||||
|
||||
type Props = {
|
||||
channelId: string;
|
||||
containerStyle?: StyleProp<ViewStyle>;
|
||||
|
||||
@@ -2,23 +2,6 @@
|
||||
|
||||
exports[`components/channel_list/categories/body/channel_item should match snapshot 1`] = `
|
||||
<View
|
||||
accessibilityState={
|
||||
{
|
||||
"busy": undefined,
|
||||
"checked": undefined,
|
||||
"disabled": undefined,
|
||||
"expanded": undefined,
|
||||
"selected": undefined,
|
||||
}
|
||||
}
|
||||
accessibilityValue={
|
||||
{
|
||||
"max": undefined,
|
||||
"min": undefined,
|
||||
"now": undefined,
|
||||
"text": undefined,
|
||||
}
|
||||
}
|
||||
accessible={true}
|
||||
collapsable={false}
|
||||
focusable={true}
|
||||
@@ -132,23 +115,6 @@ exports[`components/channel_list/categories/body/channel_item should match snaps
|
||||
|
||||
exports[`components/channel_list/categories/body/channel_item should match snapshot when it has a call 1`] = `
|
||||
<View
|
||||
accessibilityState={
|
||||
{
|
||||
"busy": undefined,
|
||||
"checked": undefined,
|
||||
"disabled": undefined,
|
||||
"expanded": undefined,
|
||||
"selected": undefined,
|
||||
}
|
||||
}
|
||||
accessibilityValue={
|
||||
{
|
||||
"max": undefined,
|
||||
"min": undefined,
|
||||
"now": undefined,
|
||||
"text": undefined,
|
||||
}
|
||||
}
|
||||
accessible={true}
|
||||
collapsable={false}
|
||||
focusable={true}
|
||||
@@ -291,23 +257,6 @@ exports[`components/channel_list/categories/body/channel_item should match snaps
|
||||
|
||||
exports[`components/channel_list/categories/body/channel_item should match snapshot when it has a draft 1`] = `
|
||||
<View
|
||||
accessibilityState={
|
||||
{
|
||||
"busy": undefined,
|
||||
"checked": undefined,
|
||||
"disabled": undefined,
|
||||
"expanded": undefined,
|
||||
"selected": undefined,
|
||||
}
|
||||
}
|
||||
accessibilityValue={
|
||||
{
|
||||
"max": undefined,
|
||||
"min": undefined,
|
||||
"now": undefined,
|
||||
"text": undefined,
|
||||
}
|
||||
}
|
||||
accessible={true}
|
||||
collapsable={false}
|
||||
focusable={true}
|
||||
|
||||
@@ -9,23 +9,6 @@ exports[`components/channel_list_row should be selected 1`] = `
|
||||
}
|
||||
>
|
||||
<View
|
||||
accessibilityState={
|
||||
{
|
||||
"busy": undefined,
|
||||
"checked": undefined,
|
||||
"disabled": undefined,
|
||||
"expanded": undefined,
|
||||
"selected": undefined,
|
||||
}
|
||||
}
|
||||
accessibilityValue={
|
||||
{
|
||||
"max": undefined,
|
||||
"min": undefined,
|
||||
"now": undefined,
|
||||
"text": undefined,
|
||||
}
|
||||
}
|
||||
accessible={true}
|
||||
collapsable={false}
|
||||
focusable={true}
|
||||
@@ -120,23 +103,6 @@ exports[`components/channel_list_row should match snapshot with delete_at filled
|
||||
}
|
||||
>
|
||||
<View
|
||||
accessibilityState={
|
||||
{
|
||||
"busy": undefined,
|
||||
"checked": undefined,
|
||||
"disabled": undefined,
|
||||
"expanded": undefined,
|
||||
"selected": undefined,
|
||||
}
|
||||
}
|
||||
accessibilityValue={
|
||||
{
|
||||
"max": undefined,
|
||||
"min": undefined,
|
||||
"now": undefined,
|
||||
"text": undefined,
|
||||
}
|
||||
}
|
||||
accessible={true}
|
||||
collapsable={false}
|
||||
focusable={true}
|
||||
@@ -209,23 +175,6 @@ exports[`components/channel_list_row should match snapshot with open channel ico
|
||||
}
|
||||
>
|
||||
<View
|
||||
accessibilityState={
|
||||
{
|
||||
"busy": undefined,
|
||||
"checked": undefined,
|
||||
"disabled": undefined,
|
||||
"expanded": undefined,
|
||||
"selected": undefined,
|
||||
}
|
||||
}
|
||||
accessibilityValue={
|
||||
{
|
||||
"max": undefined,
|
||||
"min": undefined,
|
||||
"now": undefined,
|
||||
"text": undefined,
|
||||
}
|
||||
}
|
||||
accessible={true}
|
||||
collapsable={false}
|
||||
focusable={true}
|
||||
@@ -298,23 +247,6 @@ exports[`components/channel_list_row should match snapshot with private channel
|
||||
}
|
||||
>
|
||||
<View
|
||||
accessibilityState={
|
||||
{
|
||||
"busy": undefined,
|
||||
"checked": undefined,
|
||||
"disabled": undefined,
|
||||
"expanded": undefined,
|
||||
"selected": undefined,
|
||||
}
|
||||
}
|
||||
accessibilityValue={
|
||||
{
|
||||
"max": undefined,
|
||||
"min": undefined,
|
||||
"now": undefined,
|
||||
"text": undefined,
|
||||
}
|
||||
}
|
||||
accessible={true}
|
||||
collapsable={false}
|
||||
focusable={true}
|
||||
@@ -387,23 +319,6 @@ exports[`components/channel_list_row should match snapshot with purpose filled i
|
||||
}
|
||||
>
|
||||
<View
|
||||
accessibilityState={
|
||||
{
|
||||
"busy": undefined,
|
||||
"checked": undefined,
|
||||
"disabled": undefined,
|
||||
"expanded": undefined,
|
||||
"selected": undefined,
|
||||
}
|
||||
}
|
||||
accessibilityValue={
|
||||
{
|
||||
"max": undefined,
|
||||
"min": undefined,
|
||||
"now": undefined,
|
||||
"text": undefined,
|
||||
}
|
||||
}
|
||||
accessible={true}
|
||||
collapsable={false}
|
||||
focusable={true}
|
||||
@@ -491,23 +406,6 @@ exports[`components/channel_list_row should match snapshot with shared filled in
|
||||
}
|
||||
>
|
||||
<View
|
||||
accessibilityState={
|
||||
{
|
||||
"busy": undefined,
|
||||
"checked": undefined,
|
||||
"disabled": undefined,
|
||||
"expanded": undefined,
|
||||
"selected": undefined,
|
||||
}
|
||||
}
|
||||
accessibilityValue={
|
||||
{
|
||||
"max": undefined,
|
||||
"min": undefined,
|
||||
"now": undefined,
|
||||
"text": undefined,
|
||||
}
|
||||
}
|
||||
accessible={true}
|
||||
collapsable={false}
|
||||
focusable={true}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import Database from '@nozbe/watermelondb/Database';
|
||||
import React from 'react';
|
||||
|
||||
import {renderWithEverything} from '@test/intl-test-helper';
|
||||
@@ -8,8 +9,6 @@ import TestHelper from '@test/test_helper';
|
||||
|
||||
import ChannelListRow from '.';
|
||||
|
||||
import type Database from '@nozbe/watermelondb/Database';
|
||||
|
||||
describe('components/channel_list_row', () => {
|
||||
let database: Database;
|
||||
const channel: Channel = {
|
||||
|
||||
@@ -5,6 +5,7 @@ import Clipboard from '@react-native-clipboard/clipboard';
|
||||
import React, {useCallback} from 'react';
|
||||
|
||||
import {BaseOption} from '@components/common_post_options';
|
||||
import {Screens} from '@constants';
|
||||
import {SNACK_BAR_TYPE} from '@constants/snack_bar';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {t} from '@i18n';
|
||||
@@ -12,11 +13,10 @@ import {dismissBottomSheet} from '@screens/navigation';
|
||||
import {showSnackBar} from '@utils/snack_bar';
|
||||
|
||||
import type PostModel from '@typings/database/models/servers/post';
|
||||
import type {AvailableScreens} from '@typings/screens/navigation';
|
||||
|
||||
type Props = {
|
||||
bottomSheetId: AvailableScreens;
|
||||
sourceScreen: AvailableScreens;
|
||||
bottomSheetId: typeof Screens[keyof typeof Screens];
|
||||
sourceScreen: typeof Screens[keyof typeof Screens];
|
||||
post: PostModel;
|
||||
teamName: string;
|
||||
}
|
||||
|
||||
@@ -5,15 +5,15 @@ import React, {useCallback} from 'react';
|
||||
|
||||
import {updateThreadFollowing} from '@actions/remote/thread';
|
||||
import {BaseOption} from '@components/common_post_options';
|
||||
import {Screens} from '@constants';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {t} from '@i18n';
|
||||
import {dismissBottomSheet} from '@screens/navigation';
|
||||
|
||||
import type ThreadModel from '@typings/database/models/servers/thread';
|
||||
import type {AvailableScreens} from '@typings/screens/navigation';
|
||||
|
||||
type FollowThreadOptionProps = {
|
||||
bottomSheetId: AvailableScreens;
|
||||
bottomSheetId: typeof Screens[keyof typeof Screens];
|
||||
thread: ThreadModel;
|
||||
teamId?: string;
|
||||
};
|
||||
|
||||
@@ -5,16 +5,16 @@ import React, {useCallback} from 'react';
|
||||
|
||||
import {fetchAndSwitchToThread} from '@actions/remote/thread';
|
||||
import {BaseOption} from '@components/common_post_options';
|
||||
import {Screens} from '@constants';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {t} from '@i18n';
|
||||
import {dismissBottomSheet} from '@screens/navigation';
|
||||
|
||||
import type PostModel from '@typings/database/models/servers/post';
|
||||
import type {AvailableScreens} from '@typings/screens/navigation';
|
||||
|
||||
type Props = {
|
||||
post: PostModel;
|
||||
bottomSheetId: AvailableScreens;
|
||||
bottomSheetId: typeof Screens[keyof typeof Screens];
|
||||
}
|
||||
const ReplyOption = ({post, bottomSheetId}: Props) => {
|
||||
const serverUrl = useServerUrl();
|
||||
|
||||
@@ -5,14 +5,13 @@ import React, {useCallback} from 'react';
|
||||
|
||||
import {deleteSavedPost, savePostPreference} from '@actions/remote/preference';
|
||||
import {BaseOption} from '@components/common_post_options';
|
||||
import {Screens} from '@constants';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {t} from '@i18n';
|
||||
import {dismissBottomSheet} from '@screens/navigation';
|
||||
|
||||
import type {AvailableScreens} from '@typings/screens/navigation';
|
||||
|
||||
type CopyTextProps = {
|
||||
bottomSheetId: AvailableScreens;
|
||||
bottomSheetId: typeof Screens[keyof typeof Screens];
|
||||
isSaved: boolean;
|
||||
postId: string;
|
||||
}
|
||||
|
||||
@@ -2,23 +2,6 @@
|
||||
|
||||
exports[`components/custom_status/clear_button should match snapshot 1`] = `
|
||||
<View
|
||||
accessibilityState={
|
||||
{
|
||||
"busy": undefined,
|
||||
"checked": undefined,
|
||||
"disabled": undefined,
|
||||
"expanded": undefined,
|
||||
"selected": undefined,
|
||||
}
|
||||
}
|
||||
accessibilityValue={
|
||||
{
|
||||
"max": undefined,
|
||||
"min": undefined,
|
||||
"now": undefined,
|
||||
"text": undefined,
|
||||
}
|
||||
}
|
||||
accessible={true}
|
||||
collapsable={false}
|
||||
focusable={true}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import Database from '@nozbe/watermelondb/Database';
|
||||
import React from 'react';
|
||||
|
||||
import CustomStatusEmoji from '@components/custom_status/custom_status_emoji';
|
||||
@@ -8,8 +9,6 @@ import {CustomStatusDurationEnum} from '@constants/custom_status';
|
||||
import {renderWithEverything} from '@test/intl-test-helper';
|
||||
import TestHelper from '@test/test_helper';
|
||||
|
||||
import type Database from '@nozbe/watermelondb/Database';
|
||||
|
||||
describe('components/custom_status/custom_status_emoji', () => {
|
||||
let database: Database | undefined;
|
||||
beforeAll(async () => {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import React, {useMemo} from 'react';
|
||||
|
||||
import type {EmojiComponent, EmojiProps} from '@typings/components/emoji';
|
||||
import {EmojiComponent, EmojiProps} from '@typings/components/emoji';
|
||||
|
||||
let emojiComponent: EmojiComponent;
|
||||
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {BottomSheetFlatList} from '@gorhom/bottom-sheet';
|
||||
import Fuse from 'fuse.js';
|
||||
import React, {useCallback, useMemo} from 'react';
|
||||
import {FlatList, ListRenderItemInfo, StyleSheet, View} from 'react-native';
|
||||
|
||||
import NoResultsWithTerm from '@components/no_results_with_term';
|
||||
import {useIsTablet} from '@hooks/device';
|
||||
import {getEmojis, searchEmojis} from '@utils/emoji/helpers';
|
||||
|
||||
import EmojiItem from './emoji_item';
|
||||
@@ -16,6 +14,7 @@ import type CustomEmojiModel from '@typings/database/models/servers/custom_emoji
|
||||
|
||||
type Props = {
|
||||
customEmojis: CustomEmojiModel[];
|
||||
keyboardHeight: number;
|
||||
skinTone: string;
|
||||
searchTerm: string;
|
||||
onEmojiPress: (emojiName: string) => void;
|
||||
@@ -29,9 +28,9 @@ const style = StyleSheet.create({
|
||||
},
|
||||
});
|
||||
|
||||
const EmojiFiltered = ({customEmojis, skinTone, searchTerm, onEmojiPress}: Props) => {
|
||||
const isTablet = useIsTablet();
|
||||
const EmojiFiltered = ({customEmojis, keyboardHeight, skinTone, searchTerm, onEmojiPress}: Props) => {
|
||||
const emojis = useMemo(() => getEmojis(skinTone, customEmojis), [skinTone, customEmojis]);
|
||||
const flatListStyle = useMemo(() => ({flexGrow: 1, paddingBottom: keyboardHeight}), [keyboardHeight]);
|
||||
|
||||
const fuse = useMemo(() => {
|
||||
const options = {findAllMatches: true, ignoreLocation: true, includeMatches: true, shouldSort: false, includeScore: true};
|
||||
@@ -46,8 +45,6 @@ const EmojiFiltered = ({customEmojis, skinTone, searchTerm, onEmojiPress}: Props
|
||||
return searchEmojis(fuse, searchTerm);
|
||||
}, [fuse, searchTerm]);
|
||||
|
||||
const List = useMemo(() => (isTablet ? FlatList : BottomSheetFlatList), [isTablet]);
|
||||
|
||||
const keyExtractor = useCallback((item: string) => item, []);
|
||||
|
||||
const renderEmpty = useCallback(() => {
|
||||
@@ -68,7 +65,8 @@ const EmojiFiltered = ({customEmojis, skinTone, searchTerm, onEmojiPress}: Props
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<List
|
||||
<FlatList
|
||||
contentContainerStyle={flatListStyle}
|
||||
data={data}
|
||||
initialNumToRender={30}
|
||||
keyboardDismissMode='interactive'
|
||||
@@ -1,29 +1,47 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
|
||||
import withObservables from '@nozbe/with-observables';
|
||||
import React, {useCallback, useState} from 'react';
|
||||
import {StyleSheet, View} from 'react-native';
|
||||
import {LayoutChangeEvent, Platform, StyleSheet, View} from 'react-native';
|
||||
import {Edge, SafeAreaView} from 'react-native-safe-area-context';
|
||||
import {of as of$} from 'rxjs';
|
||||
import {switchMap} from 'rxjs/operators';
|
||||
|
||||
import {searchCustomEmojis} from '@actions/remote/custom_emoji';
|
||||
import SearchBar from '@components/search';
|
||||
import {Preferences} from '@constants';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {debounce} from '@helpers/api/general';
|
||||
import {useKeyboardHeight} from '@hooks/device';
|
||||
import {queryAllCustomEmojis} from '@queries/servers/custom_emoji';
|
||||
import {queryPreferencesByCategoryAndName} from '@queries/servers/preference';
|
||||
import {observeConfigBooleanValue, observeRecentReactions} from '@queries/servers/system';
|
||||
import {getKeyboardAppearanceFromTheme} from '@utils/theme';
|
||||
|
||||
import EmojiFiltered from './filtered';
|
||||
import PickerHeader from './header';
|
||||
import EmojiSections from './sections';
|
||||
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
import type CustomEmojiModel from '@typings/database/models/servers/custom_emoji';
|
||||
|
||||
export const SCROLLVIEW_NATIVE_ID = 'emojiSelector';
|
||||
const edges: Edge[] = ['bottom', 'left', 'right'];
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
flex: {
|
||||
flex: 1,
|
||||
},
|
||||
container: {
|
||||
flex: 1,
|
||||
marginHorizontal: 12,
|
||||
},
|
||||
searchBar: {
|
||||
paddingBottom: 5,
|
||||
paddingVertical: 5,
|
||||
marginLeft: 12,
|
||||
marginRight: Platform.select({ios: 4, default: 12}),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -32,21 +50,22 @@ type Props = {
|
||||
customEmojisEnabled: boolean;
|
||||
onEmojiPress: (emoji: string) => void;
|
||||
recentEmojis: string[];
|
||||
skinTone: string;
|
||||
testID?: string;
|
||||
}
|
||||
|
||||
const Picker = ({customEmojis, customEmojisEnabled, onEmojiPress, recentEmojis, testID = ''}: Props) => {
|
||||
const EmojiPicker = ({customEmojis, customEmojisEnabled, onEmojiPress, recentEmojis, skinTone, testID = ''}: Props) => {
|
||||
const theme = useTheme();
|
||||
const serverUrl = useServerUrl();
|
||||
const keyboardHeight = useKeyboardHeight();
|
||||
const [width, setWidth] = useState(0);
|
||||
const [searchTerm, setSearchTerm] = useState<string|undefined>();
|
||||
|
||||
const onLayout = useCallback(({nativeEvent}: LayoutChangeEvent) => setWidth(nativeEvent.layout.width), []);
|
||||
const onCancelSearch = useCallback(() => setSearchTerm(undefined), []);
|
||||
|
||||
const onChangeSearchTerm = useCallback((text: string) => {
|
||||
setSearchTerm(text);
|
||||
searchCustom(text);
|
||||
}, []);
|
||||
|
||||
const searchCustom = debounce((text: string) => {
|
||||
if (text && text.length > 1) {
|
||||
searchCustomEmojis(serverUrl, text);
|
||||
@@ -58,6 +77,8 @@ const Picker = ({customEmojis, customEmojisEnabled, onEmojiPress, recentEmojis,
|
||||
EmojiList = (
|
||||
<EmojiFiltered
|
||||
customEmojis={customEmojis}
|
||||
keyboardHeight={keyboardHeight}
|
||||
skinTone={skinTone}
|
||||
searchTerm={searchTerm}
|
||||
onEmojiPress={onEmojiPress}
|
||||
/>
|
||||
@@ -69,17 +90,20 @@ const Picker = ({customEmojis, customEmojisEnabled, onEmojiPress, recentEmojis,
|
||||
customEmojisEnabled={customEmojisEnabled}
|
||||
onEmojiPress={onEmojiPress}
|
||||
recentEmojis={recentEmojis}
|
||||
skinTone={skinTone}
|
||||
width={width}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View
|
||||
<SafeAreaView
|
||||
style={styles.flex}
|
||||
edges={edges}
|
||||
testID={`${testID}.screen`}
|
||||
>
|
||||
<View style={styles.searchBar}>
|
||||
<PickerHeader
|
||||
<SearchBar
|
||||
autoCapitalize='none'
|
||||
keyboardAppearance={getKeyboardAppearanceFromTheme(theme)}
|
||||
onCancel={onCancelSearch}
|
||||
@@ -88,9 +112,28 @@ const Picker = ({customEmojis, customEmojisEnabled, onEmojiPress, recentEmojis,
|
||||
value={searchTerm}
|
||||
/>
|
||||
</View>
|
||||
{EmojiList}
|
||||
</View>
|
||||
<View
|
||||
style={styles.container}
|
||||
onLayout={onLayout}
|
||||
>
|
||||
{Boolean(width) &&
|
||||
<>
|
||||
{EmojiList}
|
||||
</>
|
||||
}
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
);
|
||||
};
|
||||
|
||||
export default Picker;
|
||||
const enhanced = withObservables([], ({database}: WithDatabaseArgs) => ({
|
||||
customEmojisEnabled: observeConfigBooleanValue(database, 'EnableCustomEmoji'),
|
||||
customEmojis: queryAllCustomEmojis(database).observe(),
|
||||
recentEmojis: observeRecentReactions(database),
|
||||
skinTone: queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_EMOJI, Preferences.EMOJI_SKINTONE).
|
||||
observeWithColumns(['value']).pipe(
|
||||
switchMap((prefs) => of$(prefs?.[0]?.value ?? 'default')),
|
||||
),
|
||||
}));
|
||||
|
||||
export default withDatabase(enhanced(EmojiPicker));
|
||||
@@ -18,31 +18,28 @@ type Props = {
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
container: {
|
||||
width: 32,
|
||||
height: 32,
|
||||
alignItems: 'center',
|
||||
flex: 1,
|
||||
height: 35,
|
||||
justifyContent: 'center',
|
||||
zIndex: 10,
|
||||
},
|
||||
icon: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.56),
|
||||
},
|
||||
selectedContainer: {
|
||||
backgroundColor: changeOpacity(theme.buttonBg, 0.08),
|
||||
borderRadius: 4,
|
||||
color: changeOpacity(theme.centerChannelColor, 0.4),
|
||||
},
|
||||
selected: {
|
||||
color: theme.buttonBg,
|
||||
color: theme.centerChannelColor,
|
||||
},
|
||||
}));
|
||||
|
||||
const EmojiCategoryBarIcon = ({currentIndex, icon, index, scrollToIndex, theme}: Props) => {
|
||||
const SectionIcon = ({currentIndex, icon, index, scrollToIndex, theme}: Props) => {
|
||||
const style = getStyleSheet(theme);
|
||||
const onPress = useCallback(preventDoubleTap(() => scrollToIndex(index)), []);
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
onPress={onPress}
|
||||
style={[style.container, currentIndex === index ? style.selectedContainer : undefined]}
|
||||
style={style.container}
|
||||
>
|
||||
<CompassIcon
|
||||
name={icon}
|
||||
@@ -53,4 +50,4 @@ const EmojiCategoryBarIcon = ({currentIndex, icon, index, scrollToIndex, theme}:
|
||||
);
|
||||
};
|
||||
|
||||
export default EmojiCategoryBarIcon;
|
||||
export default SectionIcon;
|
||||
76
app/components/emoji_picker/sections/icons_bar/index.tsx
Normal file
76
app/components/emoji_picker/sections/icons_bar/index.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {View} from 'react-native';
|
||||
import {KeyboardTrackingView} from 'react-native-keyboard-tracking-view';
|
||||
|
||||
import {useTheme} from '@context/theme';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
import SectionIcon from './icon';
|
||||
|
||||
export const SCROLLVIEW_NATIVE_ID = 'emojiSelector';
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
container: {
|
||||
bottom: 10,
|
||||
height: 35,
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
},
|
||||
background: {
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
},
|
||||
pane: {
|
||||
flexDirection: 'row',
|
||||
borderRadius: 10,
|
||||
paddingHorizontal: 10,
|
||||
width: '100%',
|
||||
borderColor: changeOpacity(theme.centerChannelColor, 0.3),
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.1),
|
||||
borderWidth: 1,
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
}));
|
||||
|
||||
export type SectionIconType = {
|
||||
key: string;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
type Props = {
|
||||
currentIndex: number;
|
||||
sections: SectionIconType[];
|
||||
scrollToIndex: (index: number) => void;
|
||||
}
|
||||
|
||||
const EmojiSectionBar = ({currentIndex, sections, scrollToIndex}: Props) => {
|
||||
const theme = useTheme();
|
||||
const styles = getStyleSheet(theme);
|
||||
return (
|
||||
<KeyboardTrackingView
|
||||
scrollViewNativeID={SCROLLVIEW_NATIVE_ID}
|
||||
normalList={true}
|
||||
style={styles.container}
|
||||
testID='emoji_picker.emoji_sections.section_bar'
|
||||
>
|
||||
<View style={styles.background}>
|
||||
<View style={styles.pane}>
|
||||
{sections.map((section, index) => (
|
||||
<SectionIcon
|
||||
currentIndex={currentIndex}
|
||||
key={section.key}
|
||||
icon={section.icon}
|
||||
index={index}
|
||||
scrollToIndex={scrollToIndex}
|
||||
theme={theme}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
</KeyboardTrackingView>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmojiSectionBar;
|
||||
@@ -1,37 +1,31 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {BottomSheetSectionList} from '@gorhom/bottom-sheet';
|
||||
import {chunk} from 'lodash';
|
||||
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
|
||||
import React, {useCallback, useMemo, useRef, useState} from 'react';
|
||||
import {ListRenderItemInfo, NativeScrollEvent, NativeSyntheticEvent, SectionList, SectionListData, StyleSheet, View} from 'react-native';
|
||||
import sectionListGetItemLayout from 'react-native-section-list-get-item-layout';
|
||||
|
||||
import {fetchCustomEmojis} from '@actions/remote/custom_emoji';
|
||||
import TouchableEmoji from '@components/touchable_emoji';
|
||||
import {EMOJIS_PER_PAGE} from '@constants/emoji';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useIsTablet} from '@hooks/device';
|
||||
import {setEmojiCategoryBarIcons, setEmojiCategoryBarSection, useEmojiCategoryBar} from '@hooks/emoji_category_bar';
|
||||
import {CategoryNames, EmojiIndicesByCategory, CategoryTranslations, CategoryMessage} from '@utils/emoji';
|
||||
import {fillEmoji} from '@utils/emoji/helpers';
|
||||
|
||||
import EmojiCategoryBar from '../emoji_category_bar';
|
||||
|
||||
import EmojiSectionBar, {SCROLLVIEW_NATIVE_ID, SectionIconType} from './icons_bar';
|
||||
import SectionFooter from './section_footer';
|
||||
import SectionHeader, {SECTION_HEADER_HEIGHT} from './section_header';
|
||||
import TouchableEmoji from './touchable_emoji';
|
||||
|
||||
import type CustomEmojiModel from '@typings/database/models/servers/custom_emoji';
|
||||
|
||||
const EMOJI_SIZE = 34;
|
||||
const EMOJIS_PER_ROW = 7;
|
||||
const EMOJIS_PER_ROW_TABLET = 9;
|
||||
const EMOJI_ROW_MARGIN = 12;
|
||||
export const EMOJI_SIZE = 30;
|
||||
export const EMOJI_GUTTER = 8;
|
||||
|
||||
const ICONS: Record<string, string> = {
|
||||
recent: 'clock-outline',
|
||||
'smileys-emotion': 'emoticon-happy-outline',
|
||||
'people-body': 'account-outline',
|
||||
'people-body': 'eye-outline',
|
||||
'animals-nature': 'leaf-outline',
|
||||
'food-drink': 'food-apple',
|
||||
'travel-places': 'airplane-variant',
|
||||
@@ -43,27 +37,20 @@ const ICONS: Record<string, string> = {
|
||||
};
|
||||
|
||||
const categoryToI18n: Record<string, CategoryTranslation> = {};
|
||||
let emojiSectionsByOffset: number[] = [];
|
||||
|
||||
const getItemLayout = sectionListGetItemLayout({
|
||||
getItemHeight: () => EMOJI_SIZE + EMOJI_ROW_MARGIN,
|
||||
getItemHeight: () => (EMOJI_SIZE + (EMOJI_GUTTER * 2)),
|
||||
getSectionHeaderHeight: () => SECTION_HEADER_HEIGHT,
|
||||
sectionOffsetsCallback: (offsetsById) => {
|
||||
emojiSectionsByOffset = offsetsById;
|
||||
},
|
||||
});
|
||||
|
||||
const styles = StyleSheet.create(({
|
||||
flex: {flex: 1},
|
||||
contentContainerStyle: {paddingBottom: 50},
|
||||
row: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
marginBottom: EMOJI_ROW_MARGIN,
|
||||
marginBottom: EMOJI_GUTTER,
|
||||
},
|
||||
emoji: {
|
||||
height: EMOJI_SIZE,
|
||||
width: EMOJI_SIZE,
|
||||
height: EMOJI_SIZE + EMOJI_GUTTER,
|
||||
marginHorizontal: 7,
|
||||
width: EMOJI_SIZE + EMOJI_GUTTER,
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -72,6 +59,8 @@ type Props = {
|
||||
customEmojisEnabled: boolean;
|
||||
onEmojiPress: (emoji: string) => void;
|
||||
recentEmojis: string[];
|
||||
skinTone: string;
|
||||
width: number;
|
||||
}
|
||||
|
||||
CategoryNames.forEach((name: string) => {
|
||||
@@ -84,34 +73,27 @@ CategoryNames.forEach((name: string) => {
|
||||
}
|
||||
});
|
||||
|
||||
const emptyEmoji: EmojiAlias = {
|
||||
name: '',
|
||||
short_name: '',
|
||||
aliases: [],
|
||||
};
|
||||
|
||||
const EmojiSections = ({customEmojis, customEmojisEnabled, onEmojiPress, recentEmojis}: Props) => {
|
||||
const EmojiSections = ({customEmojis, customEmojisEnabled, onEmojiPress, recentEmojis, skinTone, width}: Props) => {
|
||||
const serverUrl = useServerUrl();
|
||||
const isTablet = useIsTablet();
|
||||
const {currentIndex, selectedIndex} = useEmojiCategoryBar();
|
||||
const list = useRef<SectionList<EmojiSection>>(null);
|
||||
const categoryIndex = useRef(currentIndex);
|
||||
const [sectionIndex, setSectionIndex] = useState(0);
|
||||
const [customEmojiPage, setCustomEmojiPage] = useState(0);
|
||||
const [fetchingCustomEmojis, setFetchingCustomEmojis] = useState(false);
|
||||
const [loadedAllCustomEmojis, setLoadedAllCustomEmojis] = useState(false);
|
||||
const offset = useRef(0);
|
||||
const manualScroll = useRef(false);
|
||||
|
||||
const sections: EmojiSection[] = useMemo(() => {
|
||||
const emojisPerRow = isTablet ? EMOJIS_PER_ROW_TABLET : EMOJIS_PER_ROW;
|
||||
if (!width) {
|
||||
return [];
|
||||
}
|
||||
const chunkSize = Math.floor(width / (EMOJI_SIZE + EMOJI_GUTTER));
|
||||
|
||||
return CategoryNames.map((category) => {
|
||||
const emojiIndices = EmojiIndicesByCategory.get('default')?.get(category);
|
||||
const emojiIndices = EmojiIndicesByCategory.get(skinTone)?.get(category);
|
||||
|
||||
let data: EmojiAlias[][];
|
||||
switch (category) {
|
||||
case 'custom': {
|
||||
const builtInCustom = emojiIndices.map(fillEmoji.bind(null, 'custom'));
|
||||
const builtInCustom = emojiIndices.map(fillEmoji);
|
||||
|
||||
// eslint-disable-next-line max-nested-callbacks
|
||||
const custom = customEmojisEnabled ? customEmojis.map((ce) => ({
|
||||
@@ -120,7 +102,7 @@ const EmojiSections = ({customEmojis, customEmojisEnabled, onEmojiPress, recentE
|
||||
short_name: '',
|
||||
})) : [];
|
||||
|
||||
data = chunk<EmojiAlias>(builtInCustom.concat(custom), emojisPerRow);
|
||||
data = chunk<EmojiAlias>(builtInCustom.concat(custom), chunkSize);
|
||||
break;
|
||||
}
|
||||
case 'recent':
|
||||
@@ -129,34 +111,36 @@ const EmojiSections = ({customEmojis, customEmojisEnabled, onEmojiPress, recentE
|
||||
aliases: [],
|
||||
name: emoji,
|
||||
short_name: '',
|
||||
})), EMOJIS_PER_ROW);
|
||||
})), chunkSize);
|
||||
break;
|
||||
default:
|
||||
data = chunk(emojiIndices.map(fillEmoji.bind(null, category)), emojisPerRow);
|
||||
data = chunk(emojiIndices.map(fillEmoji), chunkSize);
|
||||
break;
|
||||
}
|
||||
|
||||
for (const d of data) {
|
||||
if (d.length < emojisPerRow) {
|
||||
d.push(
|
||||
...(new Array(emojisPerRow - d.length).fill(emptyEmoji)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...categoryToI18n[category],
|
||||
data,
|
||||
key: category,
|
||||
};
|
||||
}).filter((s: EmojiSection) => s.data.length);
|
||||
}, [customEmojis, customEmojisEnabled, isTablet]);
|
||||
}, [skinTone, customEmojis, customEmojisEnabled, width]);
|
||||
|
||||
useEffect(() => {
|
||||
setEmojiCategoryBarIcons(sections.map((s) => ({
|
||||
const sectionIcons: SectionIconType[] = useMemo(() => {
|
||||
return sections.map((s) => ({
|
||||
key: s.key,
|
||||
icon: s.icon,
|
||||
})));
|
||||
}));
|
||||
}, [sections]);
|
||||
|
||||
const emojiSectionsByOffset = useMemo(() => {
|
||||
let lastOffset = 0;
|
||||
return sections.map((s) => {
|
||||
const start = lastOffset;
|
||||
const nextOffset = s.data.length * (EMOJI_SIZE + (EMOJI_GUTTER * 2));
|
||||
lastOffset += nextOffset;
|
||||
return start;
|
||||
});
|
||||
}, [sections]);
|
||||
|
||||
const onLoadMoreCustomEmojis = useCallback(async () => {
|
||||
@@ -176,31 +160,24 @@ const EmojiSections = ({customEmojis, customEmojisEnabled, onEmojiPress, recentE
|
||||
|
||||
const onScroll = useCallback((e: NativeSyntheticEvent<NativeScrollEvent>) => {
|
||||
const {contentOffset} = e.nativeEvent;
|
||||
const direction = contentOffset.y > offset.current ? 'up' : 'down';
|
||||
offset.current = contentOffset.y;
|
||||
let nextIndex = emojiSectionsByOffset.findIndex(
|
||||
(offset) => contentOffset.y <= offset,
|
||||
);
|
||||
|
||||
if (manualScroll.current) {
|
||||
return;
|
||||
if (nextIndex === -1) {
|
||||
nextIndex = emojiSectionsByOffset.length - 1;
|
||||
} else if (nextIndex !== 0) {
|
||||
nextIndex -= 1;
|
||||
}
|
||||
|
||||
const nextIndex = contentOffset.y >= emojiSectionsByOffset[categoryIndex.current + 1] - SECTION_HEADER_HEIGHT ? categoryIndex.current + 1 : categoryIndex.current;
|
||||
const prevIndex = Math.max(0, contentOffset.y <= emojiSectionsByOffset[categoryIndex.current] - SECTION_HEADER_HEIGHT ? categoryIndex.current - 1 : categoryIndex.current);
|
||||
if (nextIndex > categoryIndex.current && direction === 'up') {
|
||||
categoryIndex.current = nextIndex;
|
||||
setEmojiCategoryBarSection(nextIndex);
|
||||
} else if (prevIndex < categoryIndex.current && direction === 'down') {
|
||||
categoryIndex.current = prevIndex;
|
||||
setEmojiCategoryBarSection(prevIndex);
|
||||
if (nextIndex !== sectionIndex) {
|
||||
setSectionIndex(nextIndex);
|
||||
}
|
||||
}, []);
|
||||
}, [emojiSectionsByOffset, sectionIndex]);
|
||||
|
||||
const scrollToIndex = (index: number) => {
|
||||
manualScroll.current = true;
|
||||
list.current?.scrollToLocation({sectionIndex: index, itemIndex: 0, animated: false, viewOffset: 0});
|
||||
setEmojiCategoryBarSection(index);
|
||||
setTimeout(() => {
|
||||
manualScroll.current = false;
|
||||
}, 350);
|
||||
setSectionIndex(index);
|
||||
};
|
||||
|
||||
const renderSectionHeader = useCallback(({section}: {section: SectionListData<EmojiAlias[], EmojiSection>}) => {
|
||||
@@ -216,22 +193,14 @@ const EmojiSections = ({customEmojis, customEmojisEnabled, onEmojiPress, recentE
|
||||
const renderItem = useCallback(({item}: ListRenderItemInfo<EmojiAlias[]>) => {
|
||||
return (
|
||||
<View style={styles.row}>
|
||||
{item.map((emoji: EmojiAlias, index: number) => {
|
||||
if (!emoji.name && !emoji.short_name) {
|
||||
return (
|
||||
<View
|
||||
key={`empty-${index.toString()}`}
|
||||
style={styles.emoji}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
{item.map((emoji: EmojiAlias) => {
|
||||
return (
|
||||
<TouchableEmoji
|
||||
key={emoji.name}
|
||||
name={emoji.name}
|
||||
onEmojiPress={onEmojiPress}
|
||||
category={emoji.category}
|
||||
size={EMOJI_SIZE}
|
||||
style={styles.emoji}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
@@ -239,23 +208,16 @@ const EmojiSections = ({customEmojis, customEmojisEnabled, onEmojiPress, recentE
|
||||
);
|
||||
}, []);
|
||||
|
||||
const List = useMemo(() => (isTablet ? SectionList : BottomSheetSectionList), [isTablet]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedIndex != null) {
|
||||
scrollToIndex(selectedIndex);
|
||||
}
|
||||
}, [selectedIndex]);
|
||||
|
||||
return (
|
||||
<View style={styles.flex}>
|
||||
<List
|
||||
|
||||
// @ts-expect-error bottom sheet definition
|
||||
<>
|
||||
<SectionList
|
||||
getItemLayout={getItemLayout}
|
||||
initialNumToRender={20}
|
||||
keyboardDismissMode='interactive'
|
||||
keyboardShouldPersistTaps='always'
|
||||
ListFooterComponent={renderFooter}
|
||||
maxToRenderPerBatch={20}
|
||||
nativeID={SCROLLVIEW_NATIVE_ID}
|
||||
onEndReached={onLoadMoreCustomEmojis}
|
||||
onEndReachedThreshold={2}
|
||||
onScroll={onScroll}
|
||||
@@ -263,15 +225,16 @@ const EmojiSections = ({customEmojis, customEmojisEnabled, onEmojiPress, recentE
|
||||
renderItem={renderItem}
|
||||
renderSectionHeader={renderSectionHeader}
|
||||
sections={sections}
|
||||
contentContainerStyle={styles.contentContainerStyle}
|
||||
stickySectionHeadersEnabled={true}
|
||||
showsVerticalScrollIndicator={false}
|
||||
contentContainerStyle={{paddingBottom: 50}}
|
||||
windowSize={100}
|
||||
testID='emoji_picker.emoji_sections.section_list'
|
||||
/>
|
||||
{isTablet &&
|
||||
<EmojiCategoryBar/>
|
||||
}
|
||||
</View>
|
||||
<EmojiSectionBar
|
||||
currentIndex={sectionIndex}
|
||||
scrollToIndex={scrollToIndex}
|
||||
sections={sectionIcons}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -7,7 +7,6 @@ import {View} from 'react-native';
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
type Props = {
|
||||
section: EmojiSection;
|
||||
@@ -24,8 +23,8 @@ const getStyleSheetFromTheme = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
},
|
||||
sectionTitle: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.2),
|
||||
textTransform: 'uppercase',
|
||||
...typography('Heading', 75, 'SemiBold'),
|
||||
fontSize: 15,
|
||||
fontWeight: '700',
|
||||
},
|
||||
};
|
||||
});
|
||||
35
app/components/emoji_picker/sections/touchable_emoji.tsx
Normal file
35
app/components/emoji_picker/sections/touchable_emoji.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback} from 'react';
|
||||
import {StyleProp, ViewStyle} from 'react-native';
|
||||
|
||||
import Emoji from '@components/emoji';
|
||||
import TouchableWithFeedback from '@components/touchable_with_feedback';
|
||||
import {preventDoubleTap} from '@utils/tap';
|
||||
|
||||
type Props = {
|
||||
name: string;
|
||||
onEmojiPress: (emoji: string) => void;
|
||||
size?: number;
|
||||
style: StyleProp<ViewStyle>;
|
||||
}
|
||||
|
||||
const TouchableEmoji = ({name, onEmojiPress, size = 30, style}: Props) => {
|
||||
const onPress = useCallback(preventDoubleTap(() => onEmojiPress(name)), []);
|
||||
|
||||
return (
|
||||
<TouchableWithFeedback
|
||||
onPress={onPress}
|
||||
style={style}
|
||||
type={'opacity'}
|
||||
>
|
||||
<Emoji
|
||||
emojiName={name}
|
||||
size={size}
|
||||
/>
|
||||
</TouchableWithFeedback>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(TouchableEmoji);
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {ClientResponse, ProgressPromise} from '@mattermost/react-native-network-client';
|
||||
import React, {forwardRef, useImperativeHandle, useRef, useState} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {Platform, StatusBar, StatusBarStyle, StyleSheet, TouchableOpacity, View} from 'react-native';
|
||||
@@ -20,7 +21,6 @@ import {emptyFunction} from '@utils/general';
|
||||
import FileIcon from './file_icon';
|
||||
|
||||
import type {Client} from '@client/rest';
|
||||
import type {ClientResponse, ProgressPromise} from '@mattermost/react-native-network-client';
|
||||
|
||||
export type DocumentFileRef = {
|
||||
handlePreviewPress: () => void;
|
||||
|
||||
@@ -104,8 +104,7 @@ const ImageFile = ({
|
||||
const props: ProgressiveImageProps = {};
|
||||
|
||||
if (file.localPath) {
|
||||
const prefix = file.localPath.startsWith('file://') ? '' : 'file://';
|
||||
props.defaultSource = {uri: prefix + file.localPath};
|
||||
props.defaultSource = {uri: file.localPath};
|
||||
} else if (file.id) {
|
||||
if (file.mini_preview && file.mime_type) {
|
||||
props.thumbnailUri = `data:${file.mime_type};base64,${file.mini_preview}`;
|
||||
|
||||
@@ -1,113 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useMemo} from 'react';
|
||||
import {StyleSheet, Text, useWindowDimensions, View} from 'react-native';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import PlayBack from '@components/files/voice_recording_file/playback';
|
||||
import {MIC_SIZE, VOICE_MESSAGE_CARD_RATIO} from '@constants/view';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
//i18n
|
||||
const VOICE_MESSAGE = 'Voice message';
|
||||
const UPLOADING_TEXT = 'Uploading..(0%)';
|
||||
|
||||
type Props = {
|
||||
file: FileInfo;
|
||||
uploading: boolean;
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
container: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
borderWidth: StyleSheet.hairlineWidth,
|
||||
borderColor: changeOpacity(theme.centerChannelColor, 0.56),
|
||||
shadowColor: '#000',
|
||||
shadowOpacity: 0.08,
|
||||
shadowRadius: 6,
|
||||
shadowOffset: {
|
||||
width: 0,
|
||||
height: 3,
|
||||
},
|
||||
alignItems: 'center',
|
||||
},
|
||||
centerContainer: {
|
||||
marginLeft: 12,
|
||||
},
|
||||
title: {
|
||||
color: theme.centerChannelColor,
|
||||
...typography('Heading', 200),
|
||||
},
|
||||
uploading: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.56),
|
||||
...typography('Body', 75),
|
||||
},
|
||||
close: {
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.56),
|
||||
},
|
||||
mic: {
|
||||
borderRadius: MIC_SIZE / 2,
|
||||
backgroundColor: changeOpacity(theme.buttonBg, 0.12),
|
||||
height: MIC_SIZE,
|
||||
width: MIC_SIZE,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginLeft: 12,
|
||||
},
|
||||
playBackContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const VoiceRecordingFile = ({file, uploading}: Props) => {
|
||||
const theme = useTheme();
|
||||
const styles = getStyleSheet(theme);
|
||||
const dimensions = useWindowDimensions();
|
||||
const isVoiceMessage = file.is_voice_recording;
|
||||
|
||||
const voiceStyle = useMemo(() => {
|
||||
return {
|
||||
width: dimensions.width * VOICE_MESSAGE_CARD_RATIO,
|
||||
};
|
||||
}, [dimensions.width]);
|
||||
|
||||
const getUploadingView = useCallback(() => {
|
||||
return (
|
||||
<>
|
||||
<View
|
||||
style={styles.mic}
|
||||
>
|
||||
<CompassIcon
|
||||
name='microphone'
|
||||
size={24}
|
||||
color={theme.buttonBg}
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.centerContainer}>
|
||||
<Text style={styles.title}>{VOICE_MESSAGE}</Text>
|
||||
<Text style={styles.uploading}>{UPLOADING_TEXT}</Text>
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
}, [uploading]);
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
styles.container,
|
||||
isVoiceMessage && voiceStyle,
|
||||
]}
|
||||
>
|
||||
{uploading ? getUploadingView() : <PlayBack/>}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default VoiceRecordingFile;
|
||||
@@ -1,61 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useState} from 'react';
|
||||
import {TouchableOpacity, View} from 'react-native';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import SoundWave from '@components/post_draft/draft_input/voice_input/sound_wave';
|
||||
import TimeElapsed from '@components/post_draft/draft_input/voice_input/time_elapsed';
|
||||
import {MIC_SIZE} from '@constants/view';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {preventDoubleTap} from '@utils/tap';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
mic: {
|
||||
borderRadius: MIC_SIZE / 2,
|
||||
backgroundColor: changeOpacity(theme.buttonBg, 0.12),
|
||||
height: MIC_SIZE,
|
||||
width: MIC_SIZE,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginLeft: 12,
|
||||
},
|
||||
playBackContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
};
|
||||
});
|
||||
const PlayBack = () => {
|
||||
const theme = useTheme();
|
||||
const styles = getStyleSheet(theme);
|
||||
const [playing, setPlaying] = useState(false);
|
||||
|
||||
const play = preventDoubleTap(() => {
|
||||
return setPlaying((p) => !p);
|
||||
});
|
||||
|
||||
return (
|
||||
<View
|
||||
style={styles.playBackContainer}
|
||||
>
|
||||
<TouchableOpacity
|
||||
style={styles.mic}
|
||||
onPress={play}
|
||||
>
|
||||
<CompassIcon
|
||||
color={theme.buttonBg}
|
||||
name='play'
|
||||
size={24}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
<SoundWave animating={playing}/>
|
||||
<TimeElapsed/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default PlayBack;
|
||||
@@ -1,30 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import * as React from 'react';
|
||||
import Svg, {Path} from 'react-native-svg';
|
||||
|
||||
function AlertSvgComponent() {
|
||||
return (
|
||||
<Svg
|
||||
width={55}
|
||||
height={55}
|
||||
viewBox='0 0 55 55'
|
||||
fill='none'
|
||||
>
|
||||
<Path
|
||||
d='M4.43715 54.2949C1.46653 54.2949 0.12055 52.1536 1.44612 49.5365L25.4558 2.37353C26.8154 -0.236815 28.9566 -0.236815 30.289 2.37353L54.292 49.5365C55.6515 52.1468 54.292 54.2949 51.3009 54.2949H4.43715Z'
|
||||
fill='#FFBC1F'
|
||||
/>
|
||||
<Path
|
||||
d='M24.1032 19.8165L26.5708 36.3963C26.5946 36.7253 26.7422 37.0331 26.9837 37.2578C27.2252 37.4824 27.5428 37.6073 27.8726 37.6073C28.2025 37.6073 28.5201 37.4824 28.7616 37.2578C29.0031 37.0331 29.1506 36.7253 29.1744 36.3963L31.642 19.8165C32.0907 13.3518 23.6478 13.3518 24.1032 19.8165Z'
|
||||
fill='#2D3039'
|
||||
/>
|
||||
<Path
|
||||
d='M27.8688 39.3942C28.6161 39.3955 29.3461 39.6183 29.9668 40.0344C30.5874 40.4506 31.0708 41.0413 31.3559 41.7321C31.6409 42.4228 31.7147 43.1825 31.5681 43.9153C31.4215 44.648 31.061 45.3208 30.5322 45.8487C30.0033 46.3766 29.3299 46.7359 28.5969 46.8812C27.8639 47.0265 27.1043 46.9512 26.4141 46.6649C25.7239 46.3787 25.134 45.8943 24.719 45.2729C24.304 44.6515 24.0825 43.921 24.0825 43.1737C24.0825 42.6768 24.1804 42.1848 24.3708 41.7258C24.5612 41.2668 24.8402 40.8498 25.1919 40.4988C25.5436 40.1477 25.9611 39.8695 26.4204 39.6799C26.8798 39.4904 27.3719 39.3933 27.8688 39.3942Z'
|
||||
fill='#2D3039'
|
||||
/>
|
||||
</Svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default AlertSvgComponent;
|
||||
@@ -1,34 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import * as React from 'react';
|
||||
import Svg, {G, Path, Defs, ClipPath, Rect} from 'react-native-svg';
|
||||
|
||||
function ErrorSvgComponent() {
|
||||
return (
|
||||
<Svg
|
||||
width={46}
|
||||
height={45}
|
||||
viewBox='0 0 46 45'
|
||||
fill='none'
|
||||
>
|
||||
<G clipPath='url(#clip0_1304_35713)'>
|
||||
<Path
|
||||
d='M45.2126 6.63077L38.8691 0.287231L23.0033 16.153L7.13065 0.287231L0.787109 6.63077L16.6529 22.5035L0.787109 38.3692L7.13065 44.7128L23.0033 28.847L38.8691 44.7128L45.2126 38.3692L29.3469 22.5035L45.2126 6.63077Z'
|
||||
fill='#D24B4E'
|
||||
/>
|
||||
</G>
|
||||
<Defs>
|
||||
<ClipPath id='clip0_1304_35713'>
|
||||
<Rect
|
||||
width='44.4255'
|
||||
height='44.4255'
|
||||
fill='white'
|
||||
transform='translate(0.787109 0.287231)'
|
||||
/>
|
||||
</ClipPath>
|
||||
</Defs>
|
||||
</Svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default ErrorSvgComponent;
|
||||
@@ -1,22 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import * as React from 'react';
|
||||
import Svg, {Path} from 'react-native-svg';
|
||||
|
||||
function SuccessSvgComponent() {
|
||||
return (
|
||||
<Svg
|
||||
width={46}
|
||||
height={47}
|
||||
viewBox='0 0 46 47'
|
||||
fill='none'
|
||||
>
|
||||
<Path
|
||||
d='M41.1767 0.776611L13.0005 31.7625L4.82284 25.5642H0.276367L13.0005 46.2234L45.7232 0.776611H41.1767Z'
|
||||
fill='#3DB887'
|
||||
/>
|
||||
</Svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default SuccessSvgComponent;
|
||||
@@ -73,23 +73,6 @@ exports[`Loading Error should match snapshot 1`] = `
|
||||
Error description
|
||||
</Text>
|
||||
<View
|
||||
accessibilityState={
|
||||
{
|
||||
"busy": undefined,
|
||||
"checked": undefined,
|
||||
"disabled": undefined,
|
||||
"expanded": undefined,
|
||||
"selected": undefined,
|
||||
}
|
||||
}
|
||||
accessibilityValue={
|
||||
{
|
||||
"max": undefined,
|
||||
"min": undefined,
|
||||
"now": undefined,
|
||||
"text": undefined,
|
||||
}
|
||||
}
|
||||
accessible={true}
|
||||
collapsable={false}
|
||||
focusable={true}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {useManagedConfig} from '@mattermost/react-native-emm';
|
||||
import {Database} from '@nozbe/watermelondb';
|
||||
import Clipboard from '@react-native-clipboard/clipboard';
|
||||
import React, {useCallback, useEffect, useMemo} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
@@ -20,7 +21,6 @@ import {bottomSheet, dismissBottomSheet, openAsBottomSheet} from '@screens/navig
|
||||
import {bottomSheetSnapPoint} from '@utils/helpers';
|
||||
import {displayUsername, getUsersByUsername} from '@utils/user';
|
||||
|
||||
import type {Database} from '@nozbe/watermelondb';
|
||||
import type GroupModelType from '@typings/database/models/servers/group';
|
||||
import type GroupMembershipModel from '@typings/database/models/servers/group_membership';
|
||||
import type UserModelType from '@typings/database/models/servers/user';
|
||||
@@ -34,7 +34,7 @@ type AtMentionProps = {
|
||||
location: string;
|
||||
mentionKeys?: Array<{key: string }>;
|
||||
mentionName: string;
|
||||
mentionStyle: StyleProp<TextStyle>;
|
||||
mentionStyle: TextStyle;
|
||||
onPostPress?: (e: GestureResponderEvent) => void;
|
||||
teammateNameDisplay: string;
|
||||
textStyle?: StyleProp<TextStyle>;
|
||||
@@ -203,7 +203,7 @@ const AtMention = ({
|
||||
}
|
||||
}, [managedConfig, intl, theme, bottom]);
|
||||
|
||||
const mentionTextStyle: StyleProp<TextStyle> = [];
|
||||
const mentionTextStyle = [];
|
||||
|
||||
let backgroundColor;
|
||||
let canPress = false;
|
||||
|
||||
@@ -5,7 +5,7 @@ import {useManagedConfig} from '@mattermost/react-native-emm';
|
||||
import {Parser, Node} from 'commonmark';
|
||||
import Renderer from 'commonmark-react-renderer';
|
||||
import React, {ReactElement, useMemo, useRef} from 'react';
|
||||
import {Dimensions, GestureResponderEvent, Platform, StyleProp, StyleSheet, Text, TextStyle, View, ViewStyle} from 'react-native';
|
||||
import {Dimensions, GestureResponderEvent, Platform, StyleProp, Text, TextStyle, View, ViewStyle} from 'react-native';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import Emoji from '@components/emoji';
|
||||
@@ -143,15 +143,13 @@ const Markdown = ({
|
||||
if (disableAtMentions) {
|
||||
return renderText({context, literal: `@${mentionName}`});
|
||||
}
|
||||
const computedStyles = StyleSheet.flatten(computeTextStyle(textStyles, baseTextStyle, context));
|
||||
const {fontFamily, fontSize, fontWeight} = computedStyles;
|
||||
|
||||
return (
|
||||
<AtMention
|
||||
channelId={channelId}
|
||||
disableAtChannelMentionHighlight={disableAtChannelMentionHighlight}
|
||||
mentionStyle={[textStyles.mention, {fontSize, fontWeight, fontFamily}]}
|
||||
textStyle={[computedStyles, style.atMentionOpacity]}
|
||||
mentionStyle={textStyles.mention}
|
||||
textStyle={[computeTextStyle(textStyles, baseTextStyle, context), style.atMentionOpacity]}
|
||||
isSearchResult={isSearchResult}
|
||||
location={location}
|
||||
mentionName={mentionName}
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
|
||||
import withObservables from '@nozbe/with-observables';
|
||||
|
||||
import {observeVoiceMessagesEnabled} from '@queries/servers/system';
|
||||
|
||||
import DraftInput from './draft_input';
|
||||
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
|
||||
const enhanced = withObservables([], ({database}: WithDatabaseArgs) => {
|
||||
return {
|
||||
voiceMessageEnabled: observeVoiceMessagesEnabled(database),
|
||||
};
|
||||
});
|
||||
|
||||
export default withDatabase(enhanced(DraftInput));
|
||||
@@ -1,30 +1,26 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useRef, useState} from 'react';
|
||||
import {PasteInputRef} from '@mattermost/react-native-paste-input';
|
||||
import React, {useCallback, useRef} from 'react';
|
||||
import {LayoutChangeEvent, Platform, ScrollView, View} from 'react-native';
|
||||
import {Edge, SafeAreaView} from 'react-native-safe-area-context';
|
||||
|
||||
import QuickActions from '@components/post_draft/quick_actions';
|
||||
import PostPriorityLabel from '@components/post_priority/post_priority_label';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
import RecordAction from '../record_action';
|
||||
import PostInput from '../post_input';
|
||||
import QuickActions from '../quick_actions';
|
||||
import SendAction from '../send_action';
|
||||
import Typing from '../typing';
|
||||
|
||||
import MessageInput from './message_input';
|
||||
import VoiceInput from './voice_input';
|
||||
|
||||
import type {PasteInputRef} from '@mattermost/react-native-paste-input';
|
||||
import Uploads from '../uploads';
|
||||
|
||||
type Props = {
|
||||
testID?: string;
|
||||
channelId: string;
|
||||
rootId?: string;
|
||||
currentUserId: string;
|
||||
voiceMessageEnabled: boolean;
|
||||
canShowPostPriority?: boolean;
|
||||
|
||||
// Post Props
|
||||
@@ -54,6 +50,16 @@ const SAFE_AREA_VIEW_EDGES: Edge[] = ['left', 'right'];
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
actionsContainer: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
paddingBottom: Platform.select({
|
||||
ios: 1,
|
||||
android: 2,
|
||||
}),
|
||||
},
|
||||
inputContainer: {
|
||||
flex: 1,
|
||||
flexDirection: 'column',
|
||||
@@ -77,21 +83,6 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
borderTopLeftRadius: 12,
|
||||
borderTopRightRadius: 12,
|
||||
},
|
||||
actionsContainer: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
paddingBottom: Platform.select({
|
||||
ios: 1,
|
||||
android: 2,
|
||||
}),
|
||||
},
|
||||
sendVoiceMessage: {
|
||||
position: 'absolute',
|
||||
right: -5,
|
||||
top: 16,
|
||||
},
|
||||
postPriorityLabel: {
|
||||
marginLeft: 12,
|
||||
marginTop: Platform.select({
|
||||
@@ -103,83 +94,42 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
});
|
||||
|
||||
export default function DraftInput({
|
||||
addFiles,
|
||||
canSend,
|
||||
testID,
|
||||
channelId,
|
||||
currentUserId,
|
||||
cursorPosition,
|
||||
canShowPostPriority,
|
||||
files,
|
||||
maxMessageLength,
|
||||
rootId = '',
|
||||
sendMessage,
|
||||
testID,
|
||||
updateCursorPosition,
|
||||
updatePostInputTop,
|
||||
updateValue,
|
||||
uploadFileError,
|
||||
value,
|
||||
voiceMessageEnabled,
|
||||
uploadFileError,
|
||||
sendMessage,
|
||||
canSend,
|
||||
updateValue,
|
||||
addFiles,
|
||||
updateCursorPosition,
|
||||
cursorPosition,
|
||||
updatePostInputTop,
|
||||
postPriority,
|
||||
updatePostPriority,
|
||||
setIsFocused,
|
||||
}: Props) {
|
||||
const [recording, setRecording] = useState(false);
|
||||
const theme = useTheme();
|
||||
const style = getStyleSheet(theme);
|
||||
|
||||
const handleLayout = useCallback((e: LayoutChangeEvent) => {
|
||||
updatePostInputTop(e.nativeEvent.layout.height);
|
||||
}, []);
|
||||
|
||||
const onPresRecording = useCallback(() => {
|
||||
setRecording(true);
|
||||
}, []);
|
||||
|
||||
const onCloseRecording = useCallback(() => {
|
||||
setRecording(false);
|
||||
}, []);
|
||||
|
||||
const isHandlingVoice = files[0]?.is_voice_recording || recording;
|
||||
const inputRef = useRef<PasteInputRef>();
|
||||
const focus = useCallback(() => {
|
||||
inputRef.current?.focus();
|
||||
}, []);
|
||||
|
||||
// Render
|
||||
const postInputTestID = `${testID}.post.input`;
|
||||
const quickActionsTestID = `${testID}.quick_actions`;
|
||||
const sendActionTestID = `${testID}.send_action`;
|
||||
const recordActionTestID = `${testID}.record_action`;
|
||||
|
||||
const getActionButton = useCallback(() => {
|
||||
if (value.length === 0 && files.length === 0 && voiceMessageEnabled) {
|
||||
return (
|
||||
<RecordAction
|
||||
onPress={onPresRecording}
|
||||
testID={recordActionTestID}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SendAction
|
||||
disabled={!canSend}
|
||||
sendMessage={sendMessage}
|
||||
testID={sendActionTestID}
|
||||
containerStyle={isHandlingVoice && style.sendVoiceMessage}
|
||||
/>
|
||||
);
|
||||
}, [
|
||||
canSend,
|
||||
files.length,
|
||||
onCloseRecording,
|
||||
onPresRecording,
|
||||
sendMessage,
|
||||
testID,
|
||||
value.length,
|
||||
voiceMessageEnabled,
|
||||
isHandlingVoice,
|
||||
]);
|
||||
const style = getStyleSheet(theme);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -193,63 +143,61 @@ export default function DraftInput({
|
||||
style={style.inputWrapper}
|
||||
testID={testID}
|
||||
>
|
||||
|
||||
<ScrollView
|
||||
contentContainerStyle={style.inputContentContainer}
|
||||
disableScrollViewPanResponder={true}
|
||||
keyboardShouldPersistTaps={'always'}
|
||||
overScrollMode={'never'}
|
||||
pinchGestureEnabled={false}
|
||||
scrollEnabled={false}
|
||||
showsHorizontalScrollIndicator={false}
|
||||
showsVerticalScrollIndicator={false}
|
||||
style={style.inputContainer}
|
||||
contentContainerStyle={style.inputContentContainer}
|
||||
keyboardShouldPersistTaps={'always'}
|
||||
scrollEnabled={false}
|
||||
showsVerticalScrollIndicator={false}
|
||||
showsHorizontalScrollIndicator={false}
|
||||
pinchGestureEnabled={false}
|
||||
overScrollMode={'never'}
|
||||
disableScrollViewPanResponder={true}
|
||||
>
|
||||
{Boolean(postPriority?.priority) && (
|
||||
<View style={style.postPriorityLabel}>
|
||||
<PostPriorityLabel label={postPriority!.priority}/>
|
||||
</View>
|
||||
)}
|
||||
{recording && (
|
||||
<VoiceInput
|
||||
addFiles={addFiles}
|
||||
onClose={onCloseRecording}
|
||||
setRecording={setRecording}
|
||||
/>
|
||||
)}
|
||||
{!recording &&
|
||||
<MessageInput
|
||||
addFiles={addFiles}
|
||||
channelId={channelId}
|
||||
currentUserId={currentUserId}
|
||||
cursorPosition={cursorPosition}
|
||||
files={files}
|
||||
inputRef={inputRef}
|
||||
maxMessageLength={maxMessageLength}
|
||||
rootId={rootId}
|
||||
sendMessage={sendMessage}
|
||||
setIsFocused={setIsFocused}
|
||||
testID={testID}
|
||||
updateCursorPosition={updateCursorPosition}
|
||||
updateValue={updateValue}
|
||||
uploadFileError={uploadFileError}
|
||||
value={value}
|
||||
/>
|
||||
}
|
||||
<PostInput
|
||||
testID={postInputTestID}
|
||||
channelId={channelId}
|
||||
maxMessageLength={maxMessageLength}
|
||||
rootId={rootId}
|
||||
cursorPosition={cursorPosition}
|
||||
updateCursorPosition={updateCursorPosition}
|
||||
updateValue={updateValue}
|
||||
value={value}
|
||||
addFiles={addFiles}
|
||||
sendMessage={sendMessage}
|
||||
inputRef={inputRef}
|
||||
setIsFocused={setIsFocused}
|
||||
/>
|
||||
<Uploads
|
||||
currentUserId={currentUserId}
|
||||
files={files}
|
||||
uploadFileError={uploadFileError}
|
||||
channelId={channelId}
|
||||
rootId={rootId}
|
||||
/>
|
||||
<View style={style.actionsContainer}>
|
||||
{!isHandlingVoice &&
|
||||
<QuickActions
|
||||
addFiles={addFiles}
|
||||
canShowPostPriority={canShowPostPriority}
|
||||
fileCount={files.length}
|
||||
postPriority={postPriority}
|
||||
testID={quickActionsTestID}
|
||||
updatePostPriority={updatePostPriority}
|
||||
updateValue={updateValue}
|
||||
value={value}
|
||||
focus={focus}
|
||||
/>
|
||||
}
|
||||
{!isHandlingVoice && getActionButton()}
|
||||
<QuickActions
|
||||
testID={quickActionsTestID}
|
||||
fileCount={files.length}
|
||||
addFiles={addFiles}
|
||||
updateValue={updateValue}
|
||||
value={value}
|
||||
postPriority={postPriority}
|
||||
updatePostPriority={updatePostPriority}
|
||||
canShowPostPriority={canShowPostPriority}
|
||||
focus={focus}
|
||||
/>
|
||||
<SendAction
|
||||
testID={sendActionTestID}
|
||||
disabled={!canSend}
|
||||
sendMessage={sendMessage}
|
||||
/>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
@@ -1,83 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import PostInput from '../post_input';
|
||||
import Uploads from '../uploads';
|
||||
|
||||
import type {PasteInputRef} from '@mattermost/react-native-paste-input';
|
||||
|
||||
type Props = {
|
||||
testID?: string;
|
||||
channelId: string;
|
||||
rootId?: string;
|
||||
currentUserId: string;
|
||||
|
||||
// Cursor Position Handler
|
||||
updateCursorPosition: (pos: number) => void;
|
||||
cursorPosition: number;
|
||||
|
||||
// Send Handler
|
||||
sendMessage: () => void;
|
||||
maxMessageLength: number;
|
||||
|
||||
// Draft Handler
|
||||
addFiles: (files: FileInfo[]) => void;
|
||||
files: FileInfo[];
|
||||
inputRef: React.MutableRefObject<PasteInputRef | undefined>;
|
||||
setIsFocused: (isFocused: boolean) => void;
|
||||
uploadFileError: React.ReactNode;
|
||||
updateValue: (value: string) => void;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export default function MessageInput({
|
||||
addFiles,
|
||||
channelId,
|
||||
currentUserId,
|
||||
cursorPosition,
|
||||
files,
|
||||
inputRef,
|
||||
maxMessageLength,
|
||||
rootId = '',
|
||||
sendMessage,
|
||||
setIsFocused,
|
||||
testID,
|
||||
updateCursorPosition,
|
||||
updateValue,
|
||||
uploadFileError,
|
||||
value,
|
||||
}: Props) {
|
||||
// Render
|
||||
const postInputTestID = `${testID}.post.input`;
|
||||
const isHandlingVoice = files[0]?.is_voice_recording;
|
||||
|
||||
return (
|
||||
<>
|
||||
{!isHandlingVoice && (
|
||||
<PostInput
|
||||
addFiles={addFiles}
|
||||
channelId={channelId}
|
||||
cursorPosition={cursorPosition}
|
||||
inputRef={inputRef}
|
||||
maxMessageLength={maxMessageLength}
|
||||
rootId={rootId}
|
||||
sendMessage={sendMessage}
|
||||
setIsFocused={setIsFocused}
|
||||
testID={postInputTestID}
|
||||
updateCursorPosition={updateCursorPosition}
|
||||
updateValue={updateValue}
|
||||
value={value}
|
||||
/>
|
||||
)}
|
||||
<Uploads
|
||||
currentUserId={currentUserId}
|
||||
files={files}
|
||||
uploadFileError={uploadFileError}
|
||||
channelId={channelId}
|
||||
rootId={rootId}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user