forked from Ivasoft/mattermost-mobile
Compare commits
20 Commits
release-1.
...
changeme
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f325cd8168 | ||
|
|
9cfae7ab3e | ||
|
|
992adad5a1 | ||
|
|
e73d9cb105 | ||
|
|
db6109920e | ||
|
|
c0ad0bf12a | ||
|
|
7538ad2c6f | ||
|
|
546487aa08 | ||
|
|
9c2611bf4b | ||
|
|
2135101a01 | ||
|
|
828d357de0 | ||
|
|
b4709846cc | ||
|
|
6ddb9be41f | ||
|
|
6a1b8a481b | ||
|
|
c94716353c | ||
|
|
38bddbf9fd | ||
|
|
e57f144543 | ||
|
|
3319f7ccd0 | ||
|
|
95ff84f24a | ||
|
|
49f86826e1 |
@@ -57,7 +57,7 @@
|
||||
"newlines-between": "always",
|
||||
"pathGroups": [
|
||||
{
|
||||
"pattern": "@(@react-native-async-storage|@react-native-community|@react-native-cookies|@react-navigation|@rudderstack|@sentry|@testing-library)/**",
|
||||
"pattern": "@(@react-native-community|@react-native-cookies|@react-navigation|@rudderstack|@sentry|@testing-library|@storybook)/**",
|
||||
"group": "external",
|
||||
"position": "before"
|
||||
},
|
||||
|
||||
@@ -25,8 +25,6 @@ emoji=true
|
||||
|
||||
exact_by_default=true
|
||||
|
||||
format.bracket_spacing=false
|
||||
|
||||
module.file_ext=.js
|
||||
module.file_ext=.json
|
||||
module.file_ext=.ios.js
|
||||
@@ -63,4 +61,4 @@ untyped-import
|
||||
untyped-type-import
|
||||
|
||||
[version]
|
||||
^0.162.0
|
||||
^0.149.0
|
||||
|
||||
1
.github/PULL_REQUEST_TEMPLATE.md
vendored
1
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -26,7 +26,6 @@ Place an '[x]' (no spaces) in all applicable fields. Please remove unrelated fie
|
||||
- [ ] Added or updated unit tests (required for all new features)
|
||||
- [ ] Has UI changes
|
||||
- [ ] Includes text changes and localization file updates
|
||||
- [ ] Have tested against the 5 core themes to ensure consistency between them.
|
||||
|
||||
#### Device Information
|
||||
This PR was tested on: <!-- Device name(s), OS version(s) -->
|
||||
|
||||
55
.github/workflows/scorecards-analysis.yml
vendored
55
.github/workflows/scorecards-analysis.yml
vendored
@@ -1,55 +0,0 @@
|
||||
name: Scorecards supply-chain security
|
||||
on:
|
||||
# Only the default branch is supported.
|
||||
branch_protection_rule:
|
||||
schedule:
|
||||
- cron: '22 14 * * 1'
|
||||
push:
|
||||
branches: [ master ]
|
||||
|
||||
# Declare default permissions as read only.
|
||||
permissions: read-all
|
||||
|
||||
jobs:
|
||||
analysis:
|
||||
name: Scorecards analysis
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
# Needed to upload the results to code-scanning dashboard.
|
||||
security-events: write
|
||||
actions: read
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- name: "Checkout code"
|
||||
uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579 # v2.4.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: "Run analysis"
|
||||
uses: ossf/scorecard-action@c8416b0b2bf627c349ca92fc8e3de51a64b005cf # v1.0.2
|
||||
with:
|
||||
results_file: results.sarif
|
||||
results_format: sarif
|
||||
# Read-only PAT token. To create it,
|
||||
# follow the steps in https://github.com/ossf/scorecard-action#pat-token-creation.
|
||||
repo_token: ${{ secrets.SCORECARD_READ_TOKEN }}
|
||||
# Publish the results to enable scorecard badges. For more details, see
|
||||
# https://github.com/ossf/scorecard-action#publishing-results.
|
||||
# For private repositories, `publish_results` will automatically be set to `false`,
|
||||
# regardless of the value entered here.
|
||||
publish_results: true
|
||||
|
||||
# Upload the results as artifacts (optional).
|
||||
- name: "Upload artifact"
|
||||
uses: actions/upload-artifact@82c141cc518b40d92cc801eee768e7aafc9c2fa2 # v2.3.1
|
||||
with:
|
||||
name: SARIF file
|
||||
path: results.sarif
|
||||
retention-days: 5
|
||||
|
||||
# Upload the results to GitHub's code scanning dashboard.
|
||||
- name: "Upload to code-scanning"
|
||||
uses: github/codeql-action/upload-sarif@5f532563584d71fdef14ee64d17bafb34f751ce5 # v1.0.26
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -30,7 +30,6 @@ DerivedData
|
||||
*.hmap
|
||||
*.ipa
|
||||
*.apk
|
||||
*.aab
|
||||
*.xcuserstate
|
||||
project.xcworkspace
|
||||
ios/Pods
|
||||
|
||||
250
NOTICE.txt
250
NOTICE.txt
@@ -7,58 +7,39 @@ NOTICES:
|
||||
This document includes a list of open source components used in Mattermost Mobile, including those that have been modified.
|
||||
|
||||
--------
|
||||
## @babel/runtime
|
||||
|
||||
## @mattermost/react-native-paste-input
|
||||
This product contains 'runtime' by Sebastian McKenzie.
|
||||
|
||||
This product contains '@mattermost/react-native-paste-input' by Mattermost.
|
||||
|
||||
React Native TextInput component have functionality to capture text input from a user by using the soft and hardware keyboards but lacks the ability to restrict copy & paste options as well as allwing pasting different files formats copied from other apps, like images & videos from the Photos gallery app.
|
||||
babel's modular runtime helpers
|
||||
|
||||
* HOMEPAGE:
|
||||
* https://github.com/mattermost/react-native-paste-input
|
||||
* https://github.com/babel/babel/tree/master/packages/babel-runtime
|
||||
|
||||
* LICENSE: MIT
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020 Elias Nahum
|
||||
Copyright (c) 2014-present Sebastian McKenzie and other contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
---
|
||||
|
||||
## msgpack/msgpack
|
||||
|
||||
This product contains 'msgpack/msgpack' by MessagePack.
|
||||
|
||||
MessagePack is an efficient binary serialization format. It's like JSON. but fast and small.
|
||||
|
||||
* HOMEPAGE:
|
||||
* https://github.com/msgpack/msgpack
|
||||
|
||||
* LICENSE: ISC
|
||||
|
||||
Copyright 2019 The MessagePack Community.
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
---
|
||||
|
||||
@@ -655,41 +636,6 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
|
||||
---
|
||||
|
||||
## buffer
|
||||
|
||||
This product contains a modified version of 'buffer' by Feross Aboukhadijeh.
|
||||
|
||||
The buffer module from node.js, for the browser.
|
||||
|
||||
* HOMEPAGE:
|
||||
* https://github.com/feross/buffer
|
||||
|
||||
* LICENSE: MIT
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) Feross Aboukhadijeh, and other contributors.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
---
|
||||
|
||||
## commonmark
|
||||
|
||||
This product contains a modified version of 'commonmark' by John MacFarlane.
|
||||
@@ -1387,41 +1333,6 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
---
|
||||
|
||||
## pako
|
||||
|
||||
This product contains 'pako' by Nodeca.
|
||||
|
||||
zlib port to javascript, very fast!
|
||||
|
||||
* HOMEPAGE:
|
||||
* https://github.com/nodeca/pako
|
||||
|
||||
* LICENSE: MIT
|
||||
|
||||
(The MIT License)
|
||||
|
||||
Copyright (C) 2014-2017 by Vitaly Puzrin and Andrei Tuputcyn
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
---
|
||||
|
||||
## prop-types
|
||||
|
||||
This product contains 'prop-types' by Facebook.
|
||||
@@ -2054,33 +1965,6 @@ SOFTWARE.
|
||||
|
||||
---
|
||||
|
||||
## react-native-incall-manager
|
||||
|
||||
This product contains a modified version of 'react-native-incall-manager' by React Native WebRTC.
|
||||
|
||||
Handling media-routes/sensors/events during a audio/video chat on React Native
|
||||
|
||||
* HOMEPAGE:
|
||||
* https://github.com/react-native-webrtc/react-native-incall-manager
|
||||
|
||||
* LICENSE: ISC License
|
||||
|
||||
Copyright (c) 2016, zxcpoiu
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
---
|
||||
|
||||
## react-native-keyboard-aware-scroll-view
|
||||
|
||||
This product contains a modified version of 'react-native-keyboard-aware-scroll-view' by APSL.
|
||||
@@ -2849,39 +2733,6 @@ SOFTWARE.
|
||||
|
||||
---
|
||||
|
||||
## react-native-webrtc
|
||||
|
||||
This product contains a modified version of 'react-native-webrtc'.
|
||||
|
||||
* HOMEPAGE:
|
||||
* https://github.com/react-native-webrtc/react-native-webrtc
|
||||
|
||||
* LICENSE: MIT
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 Howard Yang
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
---
|
||||
|
||||
## react-native-webview
|
||||
|
||||
This product contains a modified version of 'react-native-webview' by Jamon Holmgren.
|
||||
@@ -2987,65 +2838,6 @@ SOFTWARE.
|
||||
|
||||
---
|
||||
|
||||
## readable-stream
|
||||
|
||||
This product contains 'readable-stream' by Node.js.
|
||||
|
||||
Node.js core streams for userland
|
||||
|
||||
* HOMEPAGE:
|
||||
* https://github.com/nodejs/readable-stream
|
||||
|
||||
* LICENSE: Node.js is licensed for use as follows:
|
||||
|
||||
"""
|
||||
Copyright Node.js contributors. All rights reserved.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to
|
||||
deal in the Software without restriction, including without limitation the
|
||||
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
sell copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
This license applies to parts of Node.js originating from the
|
||||
https://github.com/joyent/node repository:
|
||||
|
||||
"""
|
||||
Copyright Joyent, Inc. and other Node contributors. All rights reserved.
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to
|
||||
deal in the Software without restriction, including without limitation the
|
||||
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
sell copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
---
|
||||
|
||||
## redux
|
||||
|
||||
This product contains 'redux' by Redux.
|
||||
|
||||
@@ -113,17 +113,12 @@ def jscFlavor = 'org.webkit:android-jsc-intl:+'
|
||||
/**
|
||||
* 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
|
||||
* This should be set on project.ext.react and mirrored here. If it is not set
|
||||
* on project.ext.react, JavaScript will not be compiled to Hermes Bytecode
|
||||
* and the benefits of using Hermes will therefore be sharply reduced.
|
||||
*/
|
||||
def enableHermes = project.ext.react.get("enableHermes", false);
|
||||
|
||||
/**
|
||||
* Architectures to build native code for in debug.
|
||||
*/
|
||||
def nativeArchitectures = project.getProperties().get("reactNativeDebugArchitectures")
|
||||
|
||||
android {
|
||||
compileSdkVersion rootProject.ext.compileSdkVersion
|
||||
|
||||
@@ -131,8 +126,9 @@ android {
|
||||
applicationId "com.mattermost.rnbeta"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 386
|
||||
versionName "1.49.1"
|
||||
missingDimensionStrategy "RNNotifications.reactNativeVersion", "reactNative60"
|
||||
versionCode 374
|
||||
versionName "1.47.0"
|
||||
multiDexEnabled = true
|
||||
testBuildType System.getProperty('testBuildType', 'debug')
|
||||
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
||||
@@ -164,11 +160,6 @@ android {
|
||||
debug {
|
||||
minifyEnabled enableProguardInReleaseBuilds
|
||||
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
|
||||
if (nativeArchitectures) {
|
||||
ndk {
|
||||
abiFilters nativeArchitectures.split(',')
|
||||
}
|
||||
}
|
||||
}
|
||||
unsigned.initWith(buildTypes.release)
|
||||
unsigned {
|
||||
@@ -199,11 +190,6 @@ android {
|
||||
pickFirst '**/*.so'
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
checkReleaseBuilds false
|
||||
abortOnError false
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
repositories {
|
||||
@@ -239,7 +225,7 @@ dependencies {
|
||||
|
||||
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.0.0"
|
||||
debugImplementation("com.facebook.flipper:flipper:${FLIPPER_VERSION}") {
|
||||
exclude group:'com.facebook.fbjni'
|
||||
exclude group:'com.facebook.fbjni'
|
||||
}
|
||||
debugImplementation("com.facebook.flipper:flipper-network-plugin:${FLIPPER_VERSION}") {
|
||||
exclude group:'com.facebook.flipper'
|
||||
|
||||
@@ -9,10 +9,6 @@
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH" />
|
||||
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
|
||||
<application
|
||||
android:name=".MainApplication"
|
||||
@@ -82,13 +78,5 @@
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="com.google.android.youtube.api.service.START" />
|
||||
</intent>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<data android:mimeType="*/*" />
|
||||
</intent>
|
||||
</queries>
|
||||
|
||||
</manifest>
|
||||
|
||||
Binary file not shown.
@@ -8,11 +8,13 @@ import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import com.swmansion.reanimated.ReanimatedJSIModulePackage;
|
||||
import com.ammarahmed.mmkv.RNMMKVModule;
|
||||
|
||||
public class CustomMMKVJSIModulePackage extends ReanimatedJSIModulePackage {
|
||||
@Override
|
||||
public List<JSIModuleSpec> getJSIModules(ReactApplicationContext reactApplicationContext, JavaScriptContextHolder jsContext) {
|
||||
super.getJSIModules(reactApplicationContext, jsContext);
|
||||
reactApplicationContext.getNativeModule(RNMMKVModule.class).installLib(jsContext, reactApplicationContext.getFilesDir().getAbsolutePath() + "/mmkv");
|
||||
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import androidx.annotation.Nullable;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.core.app.NotificationManagerCompat;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
@@ -29,6 +30,7 @@ import static com.wix.reactnativenotifications.Defs.NOTIFICATION_RECEIVED_EVENT_
|
||||
|
||||
import com.mattermost.react_native_interface.ResolvePromise;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONObject;
|
||||
|
||||
public class CustomPushNotification extends PushNotification {
|
||||
@@ -59,7 +61,7 @@ public class CustomPushNotification extends PushNotification {
|
||||
editor.apply();
|
||||
}
|
||||
|
||||
Map<String, Map<String, JSONObject>> inputMap = new HashMap<>();
|
||||
Map<String, List<Integer>> inputMap = new HashMap<>();
|
||||
saveNotificationsMap(context, inputMap);
|
||||
}
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
@@ -67,70 +69,55 @@ public class CustomPushNotification extends PushNotification {
|
||||
}
|
||||
}
|
||||
|
||||
public static void cancelNotification(Context context, String channelId, String rootId, Integer notificationId, Boolean isCRTEnabled) {
|
||||
public static void cancelNotification(Context context, String channelId, Integer notificationId) {
|
||||
if (!android.text.TextUtils.isEmpty(channelId)) {
|
||||
final String notificationIdStr = notificationId.toString();
|
||||
final Boolean isThreadNotification = isCRTEnabled && !android.text.TextUtils.isEmpty(rootId);
|
||||
final String groupId = isThreadNotification ? rootId : channelId;
|
||||
Map<String, Map<String, JSONObject>> notificationsInChannel = loadNotificationsMap(context);
|
||||
Map<String, JSONObject> notifications = notificationsInChannel.get(groupId);
|
||||
Map<String, List<Integer>> notificationsInChannel = loadNotificationsMap(context);
|
||||
List<Integer> notifications = notificationsInChannel.get(channelId);
|
||||
if (notifications == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final NotificationManager notificationManager = context.getSystemService(NotificationManager.class);
|
||||
notificationManager.cancel(notificationId);
|
||||
notifications.remove(notificationIdStr);
|
||||
notifications.remove(notificationId);
|
||||
final StatusBarNotification[] statusNotifications = notificationManager.getActiveNotifications();
|
||||
boolean hasMore = false;
|
||||
for (final StatusBarNotification status : statusNotifications) {
|
||||
Bundle bundle = status.getNotification().extras;
|
||||
if (isThreadNotification) {
|
||||
hasMore = bundle.getString("root_id").equals(rootId);
|
||||
} else if (isCRTEnabled) {
|
||||
hasMore = !bundle.getString("root_id").equals(rootId);
|
||||
} else {
|
||||
hasMore = bundle.getString("channel_id").equals(channelId);
|
||||
}
|
||||
if (hasMore) {
|
||||
if (status.getNotification().extras.getString("channel_id").equals(channelId)) {
|
||||
hasMore = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasMore) {
|
||||
notificationsInChannel.remove(groupId);
|
||||
} else {
|
||||
notificationsInChannel.put(groupId, notifications);
|
||||
notificationsInChannel.remove(channelId);
|
||||
}
|
||||
|
||||
saveNotificationsMap(context, notificationsInChannel);
|
||||
}
|
||||
}
|
||||
|
||||
public static void clearChannelNotifications(Context context, String channelId, String rootId, Boolean isCRTEnabled) {
|
||||
public static void clearChannelNotifications(Context context, String channelId) {
|
||||
if (!android.text.TextUtils.isEmpty(channelId)) {
|
||||
final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
|
||||
|
||||
// rootId is available only when CRT is enabled & clearing the thread
|
||||
final boolean isClearThread = isCRTEnabled && !android.text.TextUtils.isEmpty(rootId);
|
||||
|
||||
Map<String, Map<String, JSONObject>> notificationsInChannel = loadNotificationsMap(context);
|
||||
String groupId = isClearThread ? rootId : channelId;
|
||||
Map<String, JSONObject> notifications = notificationsInChannel.get(groupId);
|
||||
|
||||
Map<String, List<Integer>> notificationsInChannel = loadNotificationsMap(context);
|
||||
List<Integer> notifications = notificationsInChannel.get(channelId);
|
||||
if (notifications == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
notificationsInChannel.remove(groupId);
|
||||
notificationsInChannel.remove(channelId);
|
||||
saveNotificationsMap(context, notificationsInChannel);
|
||||
notifications.forEach(
|
||||
(notificationIdStr, post) -> notificationManager.cancel(Integer.valueOf(notificationIdStr))
|
||||
);
|
||||
|
||||
for (final Integer notificationId : notifications) {
|
||||
final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
|
||||
notificationManager.cancel(notificationId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void clearAllNotifications(Context context) {
|
||||
if (context != null) {
|
||||
Map<String, Map<String, JSONObject>> notificationsInChannel = loadNotificationsMap(context);
|
||||
Map<String, List<Integer>> notificationsInChannel = loadNotificationsMap(context);
|
||||
notificationsInChannel.clear();
|
||||
saveNotificationsMap(context, notificationsInChannel);
|
||||
final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
|
||||
@@ -145,8 +132,6 @@ public class CustomPushNotification extends PushNotification {
|
||||
final String ackId = initialData.getString("ack_id");
|
||||
final String postId = initialData.getString("post_id");
|
||||
final String channelId = initialData.getString("channel_id");
|
||||
final String rootId = initialData.getString("root_id");
|
||||
final boolean isCRTEnabled = initialData.getString("is_crt_enabled") != null && initialData.getString("is_crt_enabled").equals("true");
|
||||
final boolean isIdLoaded = initialData.getString("id_loaded") != null && initialData.getString("id_loaded").equals("true");
|
||||
int notificationId = CustomPushNotificationHelper.MESSAGE_NOTIFICATION_ID;
|
||||
if (postId != null) {
|
||||
@@ -180,41 +165,24 @@ public class CustomPushNotification extends PushNotification {
|
||||
|
||||
if (type.equals(PUSH_TYPE_MESSAGE)) {
|
||||
if (channelId != null) {
|
||||
try {
|
||||
|
||||
JSONObject post = new JSONObject();
|
||||
if (!android.text.TextUtils.isEmpty(rootId)) {
|
||||
post.put("root_id", rootId);
|
||||
}
|
||||
if (!android.text.TextUtils.isEmpty(postId)) {
|
||||
post.put("post_id", postId);
|
||||
}
|
||||
|
||||
final Boolean isThreadNotification = isCRTEnabled && post.has("root_id");
|
||||
final String groupId = isThreadNotification ? rootId : channelId;
|
||||
|
||||
Map<String, Map<String, JSONObject>> notificationsInChannel = loadNotificationsMap(mContext);
|
||||
Map<String, JSONObject> notifications = notificationsInChannel.get(groupId);
|
||||
if (notifications == null) {
|
||||
notifications = Collections.synchronizedMap(new HashMap<String, JSONObject>());
|
||||
}
|
||||
|
||||
if (notifications.size() > 0) {
|
||||
createSummary = false;
|
||||
}
|
||||
|
||||
notifications.put(String.valueOf(notificationId), post);
|
||||
|
||||
if (createSummary) {
|
||||
// Add the summary notification id as well
|
||||
notifications.put(String.valueOf(notificationId + 1), new JSONObject());
|
||||
}
|
||||
|
||||
notificationsInChannel.put(groupId, notifications);
|
||||
saveNotificationsMap(mContext, notificationsInChannel);
|
||||
} catch(Exception e) {
|
||||
e.printStackTrace();
|
||||
Map<String, List<Integer>> notificationsInChannel = loadNotificationsMap(mContext);
|
||||
List<Integer> list = notificationsInChannel.get(channelId);
|
||||
if (list == null) {
|
||||
list = Collections.synchronizedList(new ArrayList(0));
|
||||
}
|
||||
|
||||
list.add(0, notificationId);
|
||||
if (list.size() > 1) {
|
||||
createSummary = false;
|
||||
}
|
||||
|
||||
if (createSummary) {
|
||||
// Add the summary notification id as well
|
||||
list.add(0, notificationId + 1);
|
||||
}
|
||||
|
||||
notificationsInChannel.put(channelId, list);
|
||||
saveNotificationsMap(mContext, notificationsInChannel);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,7 +190,7 @@ public class CustomPushNotification extends PushNotification {
|
||||
}
|
||||
break;
|
||||
case PUSH_TYPE_CLEAR:
|
||||
clearChannelNotifications(mContext, channelId, rootId, isCRTEnabled);
|
||||
clearChannelNotifications(mContext, channelId);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -237,11 +205,19 @@ public class CustomPushNotification extends PushNotification {
|
||||
|
||||
Bundle data = mNotificationProps.asBundle();
|
||||
final String channelId = data.getString("channel_id");
|
||||
final String rootId = data.getString("root_id");
|
||||
final Boolean isCRTEnabled = data.getBoolean("is_crt_enabled");
|
||||
final String postId = data.getString("post_id");
|
||||
Integer notificationId = CustomPushNotificationHelper.MESSAGE_NOTIFICATION_ID;
|
||||
|
||||
if (postId != null) {
|
||||
notificationId = postId.hashCode();
|
||||
}
|
||||
|
||||
if (channelId != null) {
|
||||
clearChannelNotifications(mContext, channelId, rootId, isCRTEnabled);
|
||||
Map<String, List<Integer>> notificationsInChannel = loadNotificationsMap(mContext);
|
||||
List<Integer> notifications = notificationsInChannel.get(channelId);
|
||||
notifications.remove(notificationId);
|
||||
saveNotificationsMap(mContext, notificationsInChannel);
|
||||
clearChannelNotifications(mContext, channelId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -274,7 +250,7 @@ public class CustomPushNotification extends PushNotification {
|
||||
mJsIOHelper.sendEventToJS(NOTIFICATION_RECEIVED_EVENT_NAME, mNotificationProps.asBundle(), mAppLifecycleFacade.getRunningReactContext());
|
||||
}
|
||||
|
||||
private static void saveNotificationsMap(Context context, Map<String, Map<String, JSONObject>> inputMap) {
|
||||
private static void saveNotificationsMap(Context context, Map<String, List<Integer>> inputMap) {
|
||||
SharedPreferences pSharedPref = context.getSharedPreferences(PUSH_NOTIFICATIONS, Context.MODE_PRIVATE);
|
||||
if (pSharedPref != null && context != null) {
|
||||
JSONObject json = new JSONObject(inputMap);
|
||||
@@ -286,41 +262,23 @@ public class CustomPushNotification extends PushNotification {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Map Structure
|
||||
*
|
||||
* {
|
||||
* channel_id1 | thread_id1: {
|
||||
* notification_id1: {
|
||||
* post_id: 'p1',
|
||||
* root_id: 'r1',
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
*
|
||||
*/
|
||||
private static Map<String, Map<String, JSONObject>> loadNotificationsMap(Context context) {
|
||||
Map<String, Map<String, JSONObject>> outputMap = new HashMap<>();
|
||||
private static Map<String, List<Integer>> loadNotificationsMap(Context context) {
|
||||
Map<String, List<Integer>> outputMap = new HashMap<>();
|
||||
if (context != null) {
|
||||
SharedPreferences pSharedPref = context.getSharedPreferences(PUSH_NOTIFICATIONS, Context.MODE_PRIVATE);
|
||||
try {
|
||||
if (pSharedPref != null) {
|
||||
String jsonString = pSharedPref.getString(NOTIFICATIONS_IN_CHANNEL, (new JSONObject()).toString());
|
||||
JSONObject json = new JSONObject(jsonString);
|
||||
|
||||
// Can be a channel_id or thread_id
|
||||
Iterator<String> groupIdsItr = json.keys();
|
||||
while (groupIdsItr.hasNext()) {
|
||||
String groupId = groupIdsItr.next();
|
||||
JSONObject notificationsJSONObj = json.getJSONObject(groupId);
|
||||
Map<String, JSONObject> notifications = new HashMap<>();
|
||||
Iterator<String> notificationIdKeys = notificationsJSONObj.keys();
|
||||
while(notificationIdKeys.hasNext()) {
|
||||
String notificationId = notificationIdKeys.next();
|
||||
JSONObject post = notificationsJSONObj.getJSONObject(notificationId);
|
||||
notifications.put(notificationId, post);
|
||||
Iterator<String> keysItr = json.keys();
|
||||
while (keysItr.hasNext()) {
|
||||
String key = keysItr.next();
|
||||
JSONArray array = json.getJSONArray(key);
|
||||
List<Integer> values = new ArrayList<>();
|
||||
for (int i = 0; i < array.length(); ++i) {
|
||||
values.add(array.getInt(i));
|
||||
}
|
||||
outputMap.put(groupId, notifications);
|
||||
outputMap.put(key, values);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
|
||||
@@ -98,16 +98,6 @@ public class CustomPushNotificationHelper {
|
||||
userInfoBundle = new Bundle();
|
||||
}
|
||||
|
||||
String postId = bundle.getString("post_id");
|
||||
if (postId != null) {
|
||||
userInfoBundle.putString("post_id", postId);
|
||||
}
|
||||
|
||||
String rootId = bundle.getString("root_id");
|
||||
if (rootId != null) {
|
||||
userInfoBundle.putString("root_id", rootId);
|
||||
}
|
||||
|
||||
String channelId = bundle.getString("channel_id");
|
||||
if (channelId != null) {
|
||||
userInfoBundle.putString("channel_id", channelId);
|
||||
@@ -155,17 +145,13 @@ public class CustomPushNotificationHelper {
|
||||
|
||||
String channelId = bundle.getString("channel_id");
|
||||
String postId = bundle.getString("post_id");
|
||||
String rootId = bundle.getString("root_id");
|
||||
int notificationId = postId != null ? postId.hashCode() : MESSAGE_NOTIFICATION_ID;
|
||||
NotificationPreferences notificationPreferences = NotificationPreferences.getInstance(context);
|
||||
|
||||
Boolean is_crt_enabled = bundle.getString("is_crt_enabled") != null && bundle.getString("is_crt_enabled").equals("true");
|
||||
String groupId = is_crt_enabled && !android.text.TextUtils.isEmpty(rootId) ? rootId : channelId;
|
||||
|
||||
addNotificationExtras(notification, bundle);
|
||||
setNotificationIcons(context, notification, bundle);
|
||||
setNotificationMessagingStyle(context, notification, bundle);
|
||||
setNotificationGroup(notification, groupId, createSummary);
|
||||
setNotificationGroup(notification, channelId, createSummary);
|
||||
setNotificationBadgeType(notification);
|
||||
setNotificationSound(notification, notificationPreferences);
|
||||
setNotificationVibrate(notification, notificationPreferences);
|
||||
|
||||
@@ -19,9 +19,6 @@ public class NotificationDismissService extends IntentService {
|
||||
final Bundle bundle = NotificationIntentAdapter.extractPendingNotificationDataFromIntent(intent);
|
||||
final String channelId = bundle.getString("channel_id");
|
||||
final String postId = bundle.getString("post_id");
|
||||
final String rootId = bundle.getString("root_id");
|
||||
final Boolean isCRTEnabled = bundle.getString("is_crt_enabled") != null && bundle.getString("is_crt_enabled").equals("true");
|
||||
|
||||
int notificationId = CustomPushNotificationHelper.MESSAGE_NOTIFICATION_ID;
|
||||
if (postId != null) {
|
||||
notificationId = postId.hashCode();
|
||||
@@ -29,7 +26,7 @@ public class NotificationDismissService extends IntentService {
|
||||
notificationId = channelId.hashCode();
|
||||
}
|
||||
|
||||
CustomPushNotification.cancelNotification(context, channelId, rootId, notificationId, isCRTEnabled);
|
||||
CustomPushNotification.cancelNotification(context, channelId, notificationId);
|
||||
Log.i("ReactNative", "Dismiss notification");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,10 +118,6 @@ public class NotificationPreferencesModule extends ReactContextBaseJavaModule {
|
||||
WritableMap map = Arguments.createMap();
|
||||
Notification n = sbn.getNotification();
|
||||
Bundle bundle = n.extras;
|
||||
String postId = bundle.getString("post_id");
|
||||
map.putString("post_id", postId);
|
||||
String rootId = bundle.getString("root_id");
|
||||
map.putString("root_id", rootId);
|
||||
String channelId = bundle.getString("channel_id");
|
||||
map.putString("channel_id", channelId);
|
||||
result.pushMap(map);
|
||||
@@ -130,9 +126,8 @@ public class NotificationPreferencesModule extends ReactContextBaseJavaModule {
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void removeDeliveredNotifications(String channelId, String rootId, Boolean isCRTEnabled) {
|
||||
public void removeDeliveredNotifications(String channelId) {
|
||||
final Context context = mApplication.getApplicationContext();
|
||||
CustomPushNotification.clearChannelNotifications(context, channelId, rootId, isCRTEnabled);
|
||||
CustomPushNotification.clearChannelNotifications(context, channelId);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright (C) 2014 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<inset xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:insetLeft="@dimen/abc_edit_text_inset_horizontal_material"
|
||||
android:insetRight="@dimen/abc_edit_text_inset_horizontal_material"
|
||||
android:insetTop="@dimen/abc_edit_text_inset_top_material"
|
||||
android:insetBottom="@dimen/abc_edit_text_inset_bottom_material">
|
||||
|
||||
<selector>
|
||||
<!--
|
||||
This file is a copy of abc_edit_text_material (https://bit.ly/3k8fX7I).
|
||||
The item below with state_pressed="false" and state_focused="false" causes a NullPointerException.
|
||||
NullPointerException:tempt to invoke virtual method 'android.graphics.drawable.Drawable android.graphics.drawable.Drawable$ConstantState.newDrawable(android.content.res.Resources)'
|
||||
|
||||
<item android:state_pressed="false" android:state_focused="false" android:drawable="@drawable/abc_textfield_default_mtrl_alpha"/>
|
||||
|
||||
For more info, see https://bit.ly/3CdLStv (react-native/pull/29452) and https://bit.ly/3nxOMoR.
|
||||
-->
|
||||
<item android:state_enabled="false" android:drawable="@drawable/abc_textfield_default_mtrl_alpha"/>
|
||||
<item android:drawable="@drawable/abc_textfield_activated_mtrl_alpha"/>
|
||||
</selector>
|
||||
|
||||
</inset>
|
||||
@@ -4,7 +4,7 @@
|
||||
<style name="AppTheme" parent="Theme.AppCompat.DayNight.NoActionBar">
|
||||
<!-- Customize your theme here. -->
|
||||
<item name="android:windowBackground">@color/splashscreen_bg</item>
|
||||
<item name="android:editTextBackground">@drawable/rn_edit_text_material</item>
|
||||
|
||||
</style>
|
||||
|
||||
</resources>
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<paths xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<files-path name="files" path="." />
|
||||
<external-files-path name="external_files" path="." />
|
||||
<external-path name="external" path="." />
|
||||
<cache-path name="cache" path="." />
|
||||
<root-path name="root" path="." />
|
||||
</paths>
|
||||
@@ -10,17 +10,18 @@ buildscript {
|
||||
kotlinVersion = "1.5.30"
|
||||
firebaseVersion = "21.0.0"
|
||||
RNNKotlinVersion = kotlinVersion
|
||||
ndkVersion = "21.4.7075529"
|
||||
ndkVersion = "20.1.5948944"
|
||||
|
||||
}
|
||||
repositories {
|
||||
jcenter()
|
||||
google()
|
||||
mavenCentral()
|
||||
mavenLocal()
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:4.2.2'
|
||||
classpath 'com.google.gms:google-services:4.3.10'
|
||||
classpath 'com.android.tools.build:gradle:4.2.1'
|
||||
classpath 'com.google.gms:google-services:4.2.0'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
|
||||
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
@@ -31,9 +32,9 @@ buildscript {
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
mavenLocal()
|
||||
mavenCentral()
|
||||
mavenLocal()
|
||||
jcenter()
|
||||
maven {
|
||||
// All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
|
||||
url("$rootDir/../node_modules/react-native/android")
|
||||
@@ -48,16 +49,12 @@ allprojects {
|
||||
// prebuilt libv8android.so
|
||||
// url("$rootDir/../node_modules/v8-android/dist")
|
||||
}
|
||||
mavenCentral {
|
||||
// We don't want to fetch react-native from Maven Central as there are
|
||||
// older versions over there.
|
||||
content {
|
||||
excludeGroup "com.facebook.react"
|
||||
}
|
||||
}
|
||||
maven {
|
||||
url "https://www.jitpack.io"
|
||||
}
|
||||
maven {
|
||||
url ("https://dl.bintray.com/rudderstack/rudderstack")
|
||||
}
|
||||
maven {
|
||||
url "$rootDir/../node_modules/detox/Detox-android"
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
# Specifies the JVM arguments used for the daemon process.
|
||||
# The setting is particularly useful for tweaking memory settings.
|
||||
# Default value: -Xmx1024m -XX:MaxPermSize=256m
|
||||
# Default value: -Xmx10248m -XX:MaxPermSize=256m
|
||||
# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
|
||||
org.gradle.jvmargs=-Xmx2048M
|
||||
|
||||
@@ -30,4 +30,4 @@ android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
|
||||
# Version of flipper SDK to use with React Native
|
||||
FLIPPER_VERSION=0.99.0
|
||||
FLIPPER_VERSION=0.93.0
|
||||
|
||||
@@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.9-all.zip
|
||||
|
||||
@@ -19,19 +19,19 @@ import {getPreferenceKey} from '@mm-redux/utils/preference_utils';
|
||||
import {isDirectChannelVisible, isGroupChannelVisible} from '@utils/channels';
|
||||
import {buildPreference} from '@utils/preferences';
|
||||
|
||||
export async function loadSidebarDirectMessagesProfiles(state: GlobalState, channels: Channel[], channelMembers: ChannelMembership[]) {
|
||||
export async function loadSidebarDirectMessagesProfiles(state: GlobalState, channels: Array<Channel>, channelMembers: Array<ChannelMembership>) {
|
||||
const currentUserId = getCurrentUserId(state);
|
||||
const usersInChannel: RelationOneToMany<Channel, UserProfile> = getUserIdsInChannels(state);
|
||||
const directChannels = Object.values(channels).filter((c) => c.type === General.DM_CHANNEL || c.type === General.GM_CHANNEL);
|
||||
const prefs: PreferenceType[] = [];
|
||||
const prefs: Array<PreferenceType> = [];
|
||||
const promises: Array<Promise<ActionResult>> = []; //only fetch profiles that we don't have and the Direct channel should be visible
|
||||
const actions = [];
|
||||
const userIds: string[] = [];
|
||||
const userIds: Array<string> = [];
|
||||
|
||||
// Prepare preferences and start fetching profiles to batch them
|
||||
directChannels.forEach((c) => {
|
||||
const profileIds = Array.from(usersInChannel[c.id] || []);
|
||||
const profilesInChannel: string[] = profileIds.filter((u: string) => u !== currentUserId);
|
||||
const profilesInChannel: Array<string> = profileIds.filter((u: string) => u !== currentUserId);
|
||||
userIds.push(...profilesInChannel);
|
||||
|
||||
switch (c.type) {
|
||||
@@ -118,7 +118,7 @@ export async function fetchMyChannelMember(channelId: string) {
|
||||
}
|
||||
}
|
||||
|
||||
export function markChannelAsUnread(state: GlobalState, teamId: string, channelId: string, mentions: string[], isRoot = false): GenericAction[] {
|
||||
export function markChannelAsUnread(state: GlobalState, teamId: string, channelId: string, mentions: Array<string>, isRoot = false): Array<GenericAction> {
|
||||
const {myMembers} = state.entities.channels;
|
||||
const {currentUserId} = state.entities.users;
|
||||
|
||||
@@ -214,8 +214,8 @@ export async function makeGroupMessageVisibleIfNecessary(state: GlobalState, cha
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchChannelAndMyMember(channelId: string): Promise<GenericAction[]> {
|
||||
const actions: GenericAction[] = [];
|
||||
export async function fetchChannelAndMyMember(channelId: string): Promise<Array<GenericAction>> {
|
||||
const actions: Array<GenericAction> = [];
|
||||
|
||||
try {
|
||||
const [channel, member] = await Promise.all([
|
||||
@@ -246,9 +246,9 @@ export async function fetchChannelAndMyMember(channelId: string): Promise<Generi
|
||||
return actions;
|
||||
}
|
||||
|
||||
export async function getAddedDmUsersIfNecessary(state: GlobalState, preferences: PreferenceType[]): Promise<GenericAction[]> {
|
||||
export async function getAddedDmUsersIfNecessary(state: GlobalState, preferences: PreferenceType[]): Promise<Array<GenericAction>> {
|
||||
const userIds: string[] = [];
|
||||
const actions: GenericAction[] = [];
|
||||
const actions: Array<GenericAction> = [];
|
||||
|
||||
for (const preference of preferences) {
|
||||
if (preference.category === Preferences.CATEGORY_DIRECT_CHANNEL_SHOW && preference.value === 'true') {
|
||||
@@ -319,7 +319,7 @@ export function lastChannelIdForTeam(state: GlobalState, teamId: string): string
|
||||
return firstChannel;
|
||||
}
|
||||
|
||||
function fetchDirectMessageProfileIfNeeded(state: GlobalState, channel: Channel, channelMembers: ChannelMembership[], profilesInChannel: string[]) {
|
||||
function fetchDirectMessageProfileIfNeeded(state: GlobalState, channel: Channel, channelMembers: Array<ChannelMembership>, profilesInChannel: Array<string>) {
|
||||
const currentUserId = getCurrentUserId(state);
|
||||
const myPreferences = getMyPreferences(state);
|
||||
const users = getUsers(state);
|
||||
@@ -351,7 +351,7 @@ function fetchDirectMessageProfileIfNeeded(state: GlobalState, channel: Channel,
|
||||
return {preferences};
|
||||
}
|
||||
|
||||
function fetchGroupMessageProfilesIfNeeded(state: GlobalState, channel: Channel, channelMembers: ChannelMembership[], profilesInChannel: string[]) {
|
||||
function fetchGroupMessageProfilesIfNeeded(state: GlobalState, channel: Channel, channelMembers: Array<ChannelMembership>, profilesInChannel: Array<string>) {
|
||||
const currentUserId = getCurrentUserId(state);
|
||||
const myPreferences = getMyPreferences(state);
|
||||
const config = getConfig(state);
|
||||
|
||||
@@ -390,7 +390,7 @@ export function markAsViewedAndReadBatch(state, channelId, prevChannelId = '', m
|
||||
type: ChannelTypes.SET_UNREAD_MSG_COUNT,
|
||||
data: {
|
||||
channelId,
|
||||
count: isCollapsedThreadsEnabled(state) ? unreadMessageCountRoot : unreadMessageCount,
|
||||
count: unreadMessageCount,
|
||||
},
|
||||
}, {
|
||||
type: ChannelTypes.DECREMENT_UNREAD_MSG_COUNT,
|
||||
|
||||
@@ -19,11 +19,6 @@ export function setCustomStatus(customStatus: UserCustomStatus): ActionFunc {
|
||||
user.props.customStatus = JSON.stringify(customStatus);
|
||||
dispatch({type: UserTypes.RECEIVED_ME, data: user});
|
||||
|
||||
// Server does not like empty 'expires_at' string.
|
||||
if (!customStatus.expires_at) {
|
||||
delete customStatus.expires_at;
|
||||
}
|
||||
|
||||
try {
|
||||
await Client4.updateCustomStatus(customStatus);
|
||||
} catch (error) {
|
||||
|
||||
@@ -3,13 +3,10 @@
|
||||
|
||||
import {intlShape} from 'react-intl';
|
||||
import {Keyboard} from 'react-native';
|
||||
import {Navigation} from 'react-native-navigation';
|
||||
|
||||
import {showModalOverCurrentContext} from '@actions/navigation';
|
||||
import {dismissAllModals, showModalOverCurrentContext} from '@actions/navigation';
|
||||
import {loadChannelsByTeamName} from '@actions/views/channel';
|
||||
import {getPost as fetchPost, selectFocusedPostId} from '@mm-redux/actions/posts';
|
||||
import {getPost} from '@mm-redux/selectors/entities/posts';
|
||||
import {isCollapsedThreadsEnabled} from '@mm-redux/selectors/entities/preferences';
|
||||
import {selectFocusedPostId} from '@mm-redux/actions/posts';
|
||||
import {getCurrentTeam} from '@mm-redux/selectors/entities/teams';
|
||||
import {permalinkBadTeam} from '@utils/general';
|
||||
import {changeOpacity} from '@utils/theme';
|
||||
@@ -20,50 +17,26 @@ let showingPermalink = false;
|
||||
|
||||
export function showPermalink(intl: typeof intlShape, teamName: string, postId: string, openAsPermalink = true) {
|
||||
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
|
||||
const state = getState();
|
||||
|
||||
let name = teamName;
|
||||
if (!name) {
|
||||
name = getCurrentTeam(state).name;
|
||||
name = getCurrentTeam(getState()).name;
|
||||
}
|
||||
|
||||
const loadTeam = await dispatch(loadChannelsByTeamName(name, permalinkBadTeam.bind(null, intl)));
|
||||
|
||||
let isThreadPost;
|
||||
|
||||
const collapsedThreadsEnabled = isCollapsedThreadsEnabled(state);
|
||||
if (collapsedThreadsEnabled) {
|
||||
let post = getPost(state, postId);
|
||||
if (!post) {
|
||||
const {data} = await dispatch(fetchPost(postId));
|
||||
if (data) {
|
||||
post = data;
|
||||
}
|
||||
}
|
||||
if (post) {
|
||||
isThreadPost = Boolean(post.root_id);
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
if (!loadTeam.error) {
|
||||
Keyboard.dismiss();
|
||||
dispatch(selectFocusedPostId(postId));
|
||||
if (showingPermalink) {
|
||||
await dismissAllModals();
|
||||
}
|
||||
|
||||
const screen = 'Permalink';
|
||||
const passProps = {
|
||||
isPermalink: openAsPermalink,
|
||||
isThreadPost,
|
||||
focusedPostId: postId,
|
||||
teamName,
|
||||
};
|
||||
|
||||
if (showingPermalink) {
|
||||
Navigation.updateProps(screen, passProps);
|
||||
return {};
|
||||
}
|
||||
|
||||
const options = {
|
||||
layout: {
|
||||
componentBackgroundColor: changeOpacity('#000', 0.2),
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import deepEqual from 'deep-equal';
|
||||
import {batchActions} from 'redux-batched-actions';
|
||||
|
||||
import {Client4} from '@client/rest';
|
||||
@@ -32,10 +31,7 @@ export function startDataCleanup() {
|
||||
|
||||
export function loadConfigAndLicense() {
|
||||
return async (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const {currentUserId} = state.entities.users;
|
||||
const {general} = state.entities;
|
||||
const actions = [];
|
||||
const {currentUserId} = getState().entities.users;
|
||||
|
||||
try {
|
||||
const [config, license] = await Promise.all([
|
||||
@@ -43,31 +39,23 @@ export function loadConfigAndLicense() {
|
||||
Client4.getClientLicenseOld(),
|
||||
]);
|
||||
|
||||
if (!deepEqual(general.config, config)) {
|
||||
actions.push({
|
||||
type: GeneralTypes.CLIENT_CONFIG_RECEIVED,
|
||||
data: config,
|
||||
});
|
||||
}
|
||||
const actions = [{
|
||||
type: GeneralTypes.CLIENT_CONFIG_RECEIVED,
|
||||
data: config,
|
||||
}, {
|
||||
type: GeneralTypes.CLIENT_LICENSE_RECEIVED,
|
||||
data: license,
|
||||
}];
|
||||
|
||||
if (!deepEqual(general.license, license)) {
|
||||
actions.push({
|
||||
type: GeneralTypes.CLIENT_LICENSE_RECEIVED,
|
||||
data: license,
|
||||
});
|
||||
|
||||
if (currentUserId) {
|
||||
if (license?.IsLicensed === 'true' && license?.DataRetention === 'true') {
|
||||
dispatch(getDataRetentionPolicy());
|
||||
} else {
|
||||
actions.push({type: GeneralTypes.RECEIVED_DATA_RETENTION_POLICY, data: {}});
|
||||
}
|
||||
if (currentUserId) {
|
||||
if (license?.IsLicensed === 'true' && license?.DataRetention === 'true') {
|
||||
dispatch(getDataRetentionPolicy());
|
||||
} else {
|
||||
actions.push({type: GeneralTypes.RECEIVED_DATA_RETENTION_POLICY, data: {}});
|
||||
}
|
||||
}
|
||||
|
||||
if (actions.length) {
|
||||
dispatch(batchActions(actions, 'BATCH_LOAD_CONFIG_AND_LICENSE'));
|
||||
}
|
||||
dispatch(batchActions(actions, 'BATCH_LOAD_CONFIG_AND_LICENSE'));
|
||||
|
||||
return {config, license};
|
||||
} catch (error) {
|
||||
@@ -76,7 +64,7 @@ export function loadConfigAndLicense() {
|
||||
};
|
||||
}
|
||||
|
||||
export function loadFromPushNotification(notification, isInitialNotification, skipChannelSwitch = false) {
|
||||
export function loadFromPushNotification(notification, isInitialNotification) {
|
||||
return async (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const {payload} = notification;
|
||||
@@ -108,14 +96,12 @@ export function loadFromPushNotification(notification, isInitialNotification, sk
|
||||
await Promise.all(loading);
|
||||
}
|
||||
|
||||
if (!skipChannelSwitch) {
|
||||
dispatch(handleSelectTeamAndChannel(teamId, channelId));
|
||||
dispatch(selectPost(''));
|
||||
dispatch(handleSelectTeamAndChannel(teamId, channelId));
|
||||
dispatch(selectPost(''));
|
||||
|
||||
const {root_id: rootId} = notification.payload || {};
|
||||
if (isCollapsedThreadsEnabled(state) && rootId) {
|
||||
dispatch(selectPost(rootId));
|
||||
}
|
||||
const {root_id: rootId} = notification.payload || {};
|
||||
if (isCollapsedThreadsEnabled(state) && rootId) {
|
||||
dispatch(selectPost(rootId));
|
||||
}
|
||||
return {data: true};
|
||||
};
|
||||
|
||||
@@ -2,16 +2,6 @@
|
||||
// See LICENSE.txt for license information.
|
||||
import {ViewTypes} from '@constants';
|
||||
|
||||
export function updateThreadLastViewedAt(threadId: string, lastViewedAt: number) {
|
||||
return {
|
||||
type: ViewTypes.THREAD_LAST_VIEWED_AT,
|
||||
data: {
|
||||
threadId,
|
||||
lastViewedAt,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export const handleViewingGlobalThreadsScreen = () => (
|
||||
{
|
||||
type: ViewTypes.VIEWING_GLOBAL_THREADS_SCREEN,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import deepEqual from 'deep-equal';
|
||||
import {batchActions} from 'redux-batched-actions';
|
||||
|
||||
import {handleCRTPreferenceChange} from '@actions/views/crt';
|
||||
@@ -122,12 +121,9 @@ export function loadMe(user, deviceToken, skipDispatch = false) {
|
||||
data.teams = teams;
|
||||
data.teamMembers = teamMembers;
|
||||
data.teamUnreads = teamUnreads;
|
||||
data.config = config;
|
||||
data.url = Client4.getUrl();
|
||||
|
||||
if (!deepEqual(state.entities?.general?.config, config)) {
|
||||
data.config = config;
|
||||
}
|
||||
|
||||
actions.push({
|
||||
type: UserTypes.LOGIN,
|
||||
data,
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {fetchAppBindings, fetchThreadAppBindings} from '@mm-redux/actions/apps';
|
||||
import {getThreadAppsBindingsChannelId} from '@mm-redux/selectors/entities/apps';
|
||||
import {fetchAppBindings} from '@mm-redux/actions/apps';
|
||||
import {getCurrentChannelId} from '@mm-redux/selectors/entities/common';
|
||||
import {getCurrentUserId} from '@mm-redux/selectors/entities/users';
|
||||
import {ActionResult, DispatchFunc, GetStateFunc} from '@mm-redux/types/actions';
|
||||
@@ -11,17 +10,9 @@ import {appsEnabled} from '@utils/apps';
|
||||
export function handleRefreshAppsBindings() {
|
||||
return (dispatch: DispatchFunc, getState: GetStateFunc): ActionResult => {
|
||||
const state = getState();
|
||||
if (!appsEnabled(state)) {
|
||||
return {data: true};
|
||||
if (appsEnabled(state)) {
|
||||
dispatch(fetchAppBindings(getCurrentUserId(state), getCurrentChannelId(state)));
|
||||
}
|
||||
|
||||
dispatch(fetchAppBindings(getCurrentUserId(state), getCurrentChannelId(state)));
|
||||
|
||||
const threadChannelID = getThreadAppsBindingsChannelId(state);
|
||||
if (threadChannelID) {
|
||||
dispatch(fetchThreadAppBindings(getCurrentUserId(state), threadChannelID));
|
||||
}
|
||||
|
||||
return {data: true};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ export function handleChannelDeletedEvent(msg: WebSocketMessage) {
|
||||
const currentTeamId = getCurrentTeamId(state);
|
||||
const config = getConfig(state);
|
||||
const viewArchivedChannels = config.ExperimentalViewArchivedChannels === 'true';
|
||||
const actions: GenericAction[] = [{
|
||||
const actions: Array<GenericAction> = [{
|
||||
type: ChannelTypes.RECEIVED_CHANNEL_DELETED,
|
||||
data: {
|
||||
id: msg.data.channel_id,
|
||||
@@ -94,7 +94,7 @@ export function handleChannelMemberUpdatedEvent(msg: WebSocketMessage) {
|
||||
try {
|
||||
const channelMember = JSON.parse(msg.data.channelMember);
|
||||
const rolesToLoad = channelMember.roles.split(' ');
|
||||
const actions: GenericAction[] = [{
|
||||
const actions: Array<GenericAction> = [{
|
||||
type: ChannelTypes.RECEIVED_MY_CHANNEL_MEMBER,
|
||||
data: channelMember,
|
||||
}];
|
||||
@@ -129,7 +129,7 @@ export function handleChannelUnarchiveEvent(msg: WebSocketMessage) {
|
||||
const viewArchivedChannels = config.ExperimentalViewArchivedChannels === 'true';
|
||||
|
||||
if (msg.broadcast.team_id === currentTeamId) {
|
||||
const actions: GenericAction[] = [{
|
||||
const actions: Array<GenericAction> = [{
|
||||
type: ChannelTypes.RECEIVED_CHANNEL_UNARCHIVED,
|
||||
data: {
|
||||
id: msg.data.channel_id,
|
||||
|
||||
@@ -6,7 +6,7 @@ import {ActionResult, DispatchFunc, batchActions} from '@mm-redux/types/actions'
|
||||
import {WebSocketMessage} from '@mm-redux/types/websocket';
|
||||
|
||||
export function handleGroupUpdatedEvent(msg: WebSocketMessage) {
|
||||
return (dispatch: DispatchFunc): ActionResult => {
|
||||
return (dispatch: DispatchFunc) : ActionResult => {
|
||||
const data = JSON.parse(msg.data.group);
|
||||
dispatch(batchActions([
|
||||
{
|
||||
|
||||
@@ -11,7 +11,7 @@ import {getThreads} from '@mm-redux/actions/threads';
|
||||
import {getProfilesByIds, getStatusesByIds} from '@mm-redux/actions/users';
|
||||
import {General} from '@mm-redux/constants';
|
||||
import {getCurrentChannelId, getCurrentChannelStats} from '@mm-redux/selectors/entities/channels';
|
||||
import {getConfig, getFeatureFlagValue} from '@mm-redux/selectors/entities/general';
|
||||
import {getConfig} from '@mm-redux/selectors/entities/general';
|
||||
import {getPostIdsInChannel} from '@mm-redux/selectors/entities/posts';
|
||||
import {isCollapsedThreadsEnabled} from '@mm-redux/selectors/entities/preferences';
|
||||
import {getCurrentTeamId} from '@mm-redux/selectors/entities/teams';
|
||||
@@ -23,22 +23,7 @@ import {TeamMembership} from '@mm-redux/types/teams';
|
||||
import {WebSocketMessage} from '@mm-redux/types/websocket';
|
||||
import EventEmitter from '@mm-redux/utils/event_emitter';
|
||||
import {removeUserFromList} from '@mm-redux/utils/user_utils';
|
||||
import {loadCalls} from '@mmproducts/calls/store/actions/calls';
|
||||
import {
|
||||
handleCallStarted,
|
||||
handleCallUserConnected,
|
||||
handleCallUserDisconnected,
|
||||
handleCallUserMuted,
|
||||
handleCallUserUnmuted,
|
||||
handleCallUserVoiceOn,
|
||||
handleCallUserVoiceOff,
|
||||
handleCallChannelEnabled,
|
||||
handleCallChannelDisabled,
|
||||
handleCallScreenOn,
|
||||
handleCallScreenOff,
|
||||
} from '@mmproducts/calls/store/actions/websockets';
|
||||
import {getChannelSinceValue} from '@utils/channels';
|
||||
import {semverFromServerVersion} from '@utils/general';
|
||||
import websocketClient from '@websocket';
|
||||
|
||||
import {handleRefreshAppsBindings} from './apps';
|
||||
@@ -112,7 +97,7 @@ export function doFirstConnect(now: number) {
|
||||
return async (dispatch: DispatchFunc, getState: GetStateFunc): Promise<ActionResult> => {
|
||||
const state = getState();
|
||||
const {lastDisconnectAt} = state.websocket;
|
||||
const actions: GenericAction[] = [wsConnected(now)];
|
||||
const actions: Array<GenericAction> = [wsConnected(now)];
|
||||
|
||||
if (lastDisconnectAt) {
|
||||
const currentUserId = getCurrentUserId(state);
|
||||
@@ -151,7 +136,7 @@ export function doReconnect(now: number) {
|
||||
const currentUserId = getCurrentUserId(state);
|
||||
const users = getUsers(state);
|
||||
const {lastDisconnectAt} = state.websocket;
|
||||
const actions: GenericAction[] = [];
|
||||
const actions: Array<GenericAction> = [];
|
||||
|
||||
dispatch(batchActions([
|
||||
wsConnected(now),
|
||||
@@ -162,10 +147,6 @@ export function doReconnect(now: number) {
|
||||
const {data: me}: any = await dispatch(loadMe(null, null, true));
|
||||
|
||||
if (!me.error) {
|
||||
if (getFeatureFlagValue(getState(), 'CallsMobile') === 'true') {
|
||||
dispatch(loadCalls());
|
||||
}
|
||||
|
||||
const roles = [];
|
||||
|
||||
if (me.roles?.length) {
|
||||
@@ -344,7 +325,7 @@ function handleClose(connectFailCount: number) {
|
||||
}
|
||||
|
||||
function handleEvent(msg: WebSocketMessage) {
|
||||
return (dispatch: DispatchFunc, getState: GetStateFunc) => {
|
||||
return (dispatch: DispatchFunc) => {
|
||||
switch (msg.event) {
|
||||
case WebsocketEvents.POSTED:
|
||||
case WebsocketEvents.EPHEMERAL_MESSAGE:
|
||||
@@ -440,43 +421,16 @@ function handleEvent(msg: WebSocketMessage) {
|
||||
return dispatch(handleSidebarCategoryOrderUpdated(msg));
|
||||
}
|
||||
|
||||
if (getFeatureFlagValue(getState(), 'CallsMobile') === 'true') {
|
||||
switch (msg.event) {
|
||||
case WebsocketEvents.CALLS_CHANNEL_ENABLED:
|
||||
return dispatch(handleCallChannelEnabled(msg));
|
||||
case WebsocketEvents.CALLS_CHANNEL_DISABLED:
|
||||
return dispatch(handleCallChannelDisabled(msg));
|
||||
case WebsocketEvents.CALLS_USER_CONNECTED:
|
||||
handleCallUserConnected(dispatch, getState, msg);
|
||||
break;
|
||||
case WebsocketEvents.CALLS_USER_DISCONNECTED:
|
||||
return dispatch(handleCallUserDisconnected(msg));
|
||||
case WebsocketEvents.CALLS_USER_MUTED:
|
||||
return dispatch(handleCallUserMuted(msg));
|
||||
case WebsocketEvents.CALLS_USER_UNMUTED:
|
||||
return dispatch(handleCallUserUnmuted(msg));
|
||||
case WebsocketEvents.CALLS_USER_VOICE_ON:
|
||||
handleCallUserVoiceOn(msg);
|
||||
break;
|
||||
case WebsocketEvents.CALLS_USER_VOICE_OFF:
|
||||
handleCallUserVoiceOff(msg);
|
||||
break;
|
||||
case WebsocketEvents.CALLS_CALL_START:
|
||||
return dispatch(handleCallStarted(msg));
|
||||
case WebsocketEvents.CALLS_SCREEN_ON:
|
||||
return dispatch(handleCallScreenOn(msg));
|
||||
case WebsocketEvents.CALLS_SCREEN_OFF:
|
||||
return dispatch(handleCallScreenOff(msg));
|
||||
}
|
||||
}
|
||||
|
||||
return {data: true};
|
||||
};
|
||||
}
|
||||
|
||||
function handleHelloEvent(msg: WebSocketMessage) {
|
||||
const serverVersion = semverFromServerVersion(msg.data.server_version);
|
||||
EventEmitter.emit(General.SERVER_VERSION_CHANGED, serverVersion);
|
||||
const serverVersion = msg.data.server_version;
|
||||
if (serverVersion && Client4.serverVersion !== serverVersion) {
|
||||
Client4.serverVersion = serverVersion;
|
||||
EventEmitter.emit(General.SERVER_VERSION_CHANGED, serverVersion);
|
||||
}
|
||||
}
|
||||
|
||||
// Helpers
|
||||
|
||||
@@ -43,7 +43,7 @@ export function handleNewPostEvent(msg: WebSocketMessage) {
|
||||
ownPost: data.user_id === currentUserId,
|
||||
};
|
||||
|
||||
const actions: GenericAction[] = [];
|
||||
const actions: Array<GenericAction> = [];
|
||||
|
||||
const exists = selectPost(state, post.pending_post_id);
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ import {WebSocketMessage} from '@mm-redux/types/websocket';
|
||||
export function handlePreferenceChangedEvent(msg: WebSocketMessage) {
|
||||
return async (dispatch: DispatchFunc, getState: GetStateFunc): Promise<ActionResult> => {
|
||||
const preference = JSON.parse(msg.data.preference);
|
||||
const actions: GenericAction[] = [{
|
||||
const actions: Array<GenericAction> = [{
|
||||
type: PreferenceTypes.RECEIVED_PREFERENCES,
|
||||
data: [preference],
|
||||
}];
|
||||
@@ -36,7 +36,7 @@ export function handlePreferencesChangedEvent(msg: WebSocketMessage) {
|
||||
return async (dispatch: DispatchFunc, getState: GetStateFunc): Promise<ActionResult> => {
|
||||
const preferences: PreferenceType[] = JSON.parse(msg.data.preferences);
|
||||
const posts = getAllPosts(getState());
|
||||
const actions: GenericAction[] = [{
|
||||
const actions: Array<GenericAction> = [{
|
||||
type: PreferenceTypes.RECEIVED_PREFERENCES,
|
||||
data: preferences,
|
||||
}];
|
||||
|
||||
@@ -19,7 +19,7 @@ export function handleLeaveTeamEvent(msg: Partial<WebSocketMessage>) {
|
||||
const currentUser = getCurrentUser(state);
|
||||
|
||||
if (currentUser.id === msg.data.user_id) {
|
||||
const actions: GenericAction[] = [{type: TeamTypes.LEAVE_TEAM, data: teams[msg.data.team_id]}];
|
||||
const actions: Array<GenericAction> = [{type: TeamTypes.LEAVE_TEAM, data: teams[msg.data.team_id]}];
|
||||
if (isGuest(currentUser.roles)) {
|
||||
const notVisible = await notVisibleUsersActions(state);
|
||||
if (notVisible.length) {
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import {batchActions} from 'redux-batched-actions';
|
||||
|
||||
import {updateThreadLastViewedAt} from '@actions/views/threads';
|
||||
import {handleThreadArrived, handleReadChanged, handleAllMarkedRead, handleFollowChanged, getThread as fetchThread} from '@mm-redux/actions/threads';
|
||||
import {getCurrentUserId} from '@mm-redux/selectors/entities/common';
|
||||
import {getSelectedPost} from '@mm-redux/selectors/entities/posts';
|
||||
import {getCurrentTeamId} from '@mm-redux/selectors/entities/teams';
|
||||
import {getThread} from '@mm-redux/selectors/entities/threads';
|
||||
import {ActionResult, DispatchFunc, GenericAction, GetStateFunc} from '@mm-redux/types/actions';
|
||||
import {ActionResult, DispatchFunc, GetStateFunc} from '@mm-redux/types/actions';
|
||||
import {WebSocketMessage} from '@mm-redux/types/websocket';
|
||||
|
||||
export function handleThreadUpdated(msg: WebSocketMessage) {
|
||||
@@ -31,31 +27,21 @@ export function handleThreadReadChanged(msg: WebSocketMessage) {
|
||||
const thread = getThread(state, msg.data.thread_id);
|
||||
|
||||
// Mark only following threads as read.
|
||||
if (thread) {
|
||||
const actions: GenericAction[] = [];
|
||||
const selectedPost = getSelectedPost(state);
|
||||
if (selectedPost?.id !== thread.id) {
|
||||
actions.push(updateThreadLastViewedAt(thread.id, msg.data.timestamp));
|
||||
}
|
||||
if (thread.is_following) {
|
||||
actions.push(
|
||||
handleReadChanged(
|
||||
msg.data.thread_id,
|
||||
msg.broadcast.team_id,
|
||||
msg.data.channel_id,
|
||||
{
|
||||
lastViewedAt: msg.data.timestamp,
|
||||
prevUnreadMentions: thread.unread_mentions,
|
||||
newUnreadMentions: msg.data.unread_mentions,
|
||||
prevUnreadReplies: thread.unread_replies,
|
||||
newUnreadReplies: msg.data.unread_replies,
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
if (actions.length) {
|
||||
dispatch(batchActions(actions));
|
||||
}
|
||||
if (thread?.is_following) {
|
||||
dispatch(
|
||||
handleReadChanged(
|
||||
msg.data.thread_id,
|
||||
msg.broadcast.team_id,
|
||||
msg.data.channel_id,
|
||||
{
|
||||
lastViewedAt: msg.data.timestamp,
|
||||
prevUnreadMentions: thread.unread_mentions,
|
||||
newUnreadMentions: msg.data.unread_mentions,
|
||||
prevUnreadReplies: thread.unread_replies,
|
||||
newUnreadReplies: msg.data.unread_replies,
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
dispatch(handleAllMarkedRead(msg.broadcast.team_id));
|
||||
|
||||
@@ -31,7 +31,7 @@ export function handleUserAddedEvent(msg: WebSocketMessage) {
|
||||
const currentTeamId = getCurrentTeamId(state);
|
||||
const currentUserId = getCurrentUserId(state);
|
||||
const teamId = msg.data.team_id;
|
||||
const actions: GenericAction[] = [{
|
||||
const actions: Array<GenericAction> = [{
|
||||
type: ChannelTypes.CHANNEL_MEMBER_ADDED,
|
||||
data: {
|
||||
channel_id: msg.broadcast.channel_id,
|
||||
@@ -71,7 +71,7 @@ export function handleUserRemovedEvent(msg: WebSocketMessage) {
|
||||
const currentChannelId = getCurrentChannelId(state);
|
||||
const currentTeamId = getCurrentTeamId(state);
|
||||
const currentUser = getCurrentUser(state);
|
||||
const actions: GenericAction[] = [];
|
||||
const actions: Array<GenericAction> = [];
|
||||
let channelId;
|
||||
let userId;
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ const ClientApps = (superclass: any) => class extends superclass {
|
||||
`${this.getAppsProxyRoute()}/api/v1/call`,
|
||||
{method: 'post', body: JSON.stringify(callCopy)},
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
getAppsBindings = async (userID: string, channelID: string, teamID: string) => {
|
||||
const params = {
|
||||
@@ -39,7 +39,7 @@ const ClientApps = (superclass: any) => class extends superclass {
|
||||
`${this.getAppsProxyRoute()}/api/v1/bindings${buildQueryString(params)}`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export default ClientApps;
|
||||
|
||||
@@ -91,7 +91,7 @@ export default class ClientBase {
|
||||
|
||||
getWebSocketUrl = () => {
|
||||
return `${this.getBaseRoute()}/websocket`;
|
||||
};
|
||||
}
|
||||
|
||||
setAcceptLanguage(locale: string) {
|
||||
this.defaultHeaders['Accept-Language'] = locale;
|
||||
@@ -290,10 +290,6 @@ export default class ClientBase {
|
||||
return `${this.url}/plugins/com.mattermost.apps`;
|
||||
}
|
||||
|
||||
getCallsRoute() {
|
||||
return `${this.url}/plugins/com.mattermost.calls`;
|
||||
}
|
||||
|
||||
// Client Helpers
|
||||
handleRedirectProtocol = (url: string, response: RNFetchBlobFetchRepsonse) => {
|
||||
const serverUrl = this.getUrl();
|
||||
|
||||
@@ -19,21 +19,21 @@ const ClientBots = (superclass: any) => class extends superclass {
|
||||
`${this.getBotRoute(botUserId)}`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
getBots = async (page = 0, perPage = PER_PAGE_DEFAULT) => {
|
||||
return this.doFetch(
|
||||
`${this.getBotsRoute()}${buildQueryString({page, per_page: perPage})}`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
getBotsIncludeDeleted = async (page = 0, perPage = PER_PAGE_DEFAULT) => {
|
||||
return this.doFetch(
|
||||
`${this.getBotsRoute()}${buildQueryString({include_deleted: true, page, per_page: perPage})}`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export default ClientBots;
|
||||
|
||||
@@ -41,7 +41,7 @@ const ClientFiles = (superclass: any) => class extends superclass {
|
||||
`${this.getFileRoute(fileId)}/link`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export default ClientFiles;
|
||||
|
||||
@@ -27,7 +27,7 @@ const ClientGroups = (superclass: any) => class extends superclass {
|
||||
`${this.getUsersRoute()}/${userID}/groups`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
}
|
||||
getAllGroupsAssociatedToTeam = async (teamID: string, filterAllowReference = false) => {
|
||||
return this.doFetch(
|
||||
`${this.getBaseRoute()}/teams/${teamID}/groups${buildQueryString({paginate: false, filter_allow_reference: filterAllowReference})}`,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import ClientCalls, {ClientCallsMix} from '@mmproducts/calls/client/rest';
|
||||
import mix from '@utils/mix';
|
||||
|
||||
import ClientApps, {ClientAppsMix} from './apps';
|
||||
@@ -35,8 +34,7 @@ interface Client extends ClientBase,
|
||||
ClientSharedChannelsMix,
|
||||
ClientTeamsMix,
|
||||
ClientTosMix,
|
||||
ClientUsersMix,
|
||||
ClientCallsMix
|
||||
ClientUsersMix
|
||||
{}
|
||||
|
||||
class Client extends mix(ClientBase).with(
|
||||
@@ -54,7 +52,6 @@ class Client extends mix(ClientBase).with(
|
||||
ClientTeams,
|
||||
ClientTos,
|
||||
ClientUsers,
|
||||
ClientCalls,
|
||||
) {}
|
||||
|
||||
const Client4 = new Client();
|
||||
|
||||
@@ -202,7 +202,7 @@ const ClientPosts = (superclass: any) => class extends superclass {
|
||||
`${this.getUserRoute(userId)}/posts/${postId}/set_unread`,
|
||||
{method: 'post', body: JSON.stringify({collapsed_threads_supported: true})},
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
pinPost = async (postId: string) => {
|
||||
analytics.trackAPI('api_posts_pin');
|
||||
@@ -250,13 +250,8 @@ const ClientPosts = (superclass: any) => class extends superclass {
|
||||
searchPostsWithParams = async (teamId: string, params: any) => {
|
||||
analytics.trackAPI('api_posts_search', {team_id: teamId});
|
||||
|
||||
let route = `${this.getPostsRoute()}/search`;
|
||||
if (teamId) {
|
||||
route = `${this.getTeamRoute(teamId)}/posts/search`;
|
||||
}
|
||||
|
||||
return this.doFetch(
|
||||
route,
|
||||
`${this.getTeamRoute(teamId)}/posts/search`,
|
||||
{method: 'post', body: JSON.stringify(params)},
|
||||
);
|
||||
};
|
||||
|
||||
@@ -12,14 +12,14 @@ const ClientTos = (superclass: any) => class extends superclass {
|
||||
`${this.getUserRoute('me')}/terms_of_service`,
|
||||
{method: 'post', body: JSON.stringify({termsOfServiceId, accepted})},
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
getTermsOfService = async () => {
|
||||
return this.doFetch(
|
||||
`${this.getBaseRoute()}/terms_of_service`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export default ClientTos;
|
||||
|
||||
@@ -66,14 +66,14 @@ const ClientUsers = (superclass: any) => class extends superclass {
|
||||
`${this.getUsersRoute()}${buildQueryString(queryParams)}`,
|
||||
{method: 'post', body: JSON.stringify(user)},
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
patchMe = async (userPatch: Partial<UserProfile>) => {
|
||||
return this.doFetch(
|
||||
`${this.getUserRoute('me')}/patch`,
|
||||
{method: 'put', body: JSON.stringify(userPatch)},
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
patchUser = async (userPatch: Partial<UserProfile> & {id: string}) => {
|
||||
analytics.trackAPI('api_users_patch');
|
||||
@@ -82,7 +82,7 @@ const ClientUsers = (superclass: any) => class extends superclass {
|
||||
`${this.getUserRoute(userPatch.id)}/patch`,
|
||||
{method: 'put', body: JSON.stringify(userPatch)},
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
updateUser = async (user: UserProfile) => {
|
||||
analytics.trackAPI('api_users_update');
|
||||
@@ -91,7 +91,7 @@ const ClientUsers = (superclass: any) => class extends superclass {
|
||||
`${this.getUserRoute(user.id)}`,
|
||||
{method: 'put', body: JSON.stringify(user)},
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
demoteUserToGuest = async (userId: string) => {
|
||||
analytics.trackAPI('api_users_demote_user_to_guest');
|
||||
@@ -100,7 +100,7 @@ const ClientUsers = (superclass: any) => class extends superclass {
|
||||
`${this.getUserRoute(userId)}/demote`,
|
||||
{method: 'post'},
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
getKnownUsers = async () => {
|
||||
analytics.trackAPI('api_get_known_users');
|
||||
@@ -108,7 +108,7 @@ const ClientUsers = (superclass: any) => class extends superclass {
|
||||
`${this.getUsersRoute()}/known`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
sendPasswordResetEmail = async (email: string) => {
|
||||
analytics.trackAPI('api_users_send_password_reset');
|
||||
@@ -117,7 +117,7 @@ const ClientUsers = (superclass: any) => class extends superclass {
|
||||
`${this.getUsersRoute()}/password/reset/send`,
|
||||
{method: 'post', body: JSON.stringify({email})},
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
setDefaultProfileImage = async (userId: string) => {
|
||||
analytics.trackAPI('api_users_set_default_profile_picture');
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`FormattedRelativeTime should match snapshot 1`] = `
|
||||
<Text>
|
||||
a few seconds ago
|
||||
</Text>
|
||||
`;
|
||||
@@ -137,7 +137,7 @@ export default class AtMention extends React.PureComponent {
|
||||
this.setState({
|
||||
user,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
const {isSearchResult, mentionName, mentionStyle, onPostPress, teammateNameDisplay, textStyle, mentionKeys, theme} = this.props;
|
||||
|
||||
@@ -146,7 +146,7 @@ export default class AttachmentButton extends PureComponent {
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
attachPhotoFromCamera = () => {
|
||||
return this.attachFileFromCamera('camera', 'photo');
|
||||
@@ -274,7 +274,7 @@ export default class AttachmentButton extends PureComponent {
|
||||
}
|
||||
|
||||
return files;
|
||||
};
|
||||
}
|
||||
|
||||
hasPhotoPermission = async (source, mediaType = '') => {
|
||||
if (Platform.OS === 'ios') {
|
||||
|
||||
@@ -66,7 +66,7 @@ const GroupMentionItem = (props) => {
|
||||
>
|
||||
<View style={style.rowPicture}>
|
||||
<CompassIcon
|
||||
name='account-multiple-outline'
|
||||
name='account-group-outline'
|
||||
style={style.rowIcon}
|
||||
/>
|
||||
</View>
|
||||
|
||||
@@ -142,7 +142,7 @@ export default class Autocomplete extends PureComponent {
|
||||
|
||||
handleAppCommandCountChange = (appCommandCount) => {
|
||||
this.setState({appCommandCount});
|
||||
};
|
||||
}
|
||||
|
||||
handleCommandCountChange = (commandCount) => {
|
||||
this.setState({commandCount});
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import moment from 'moment-timezone';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, {PureComponent} from 'react';
|
||||
import {intlShape} from 'react-intl';
|
||||
@@ -37,8 +36,6 @@ export default class DateSuggestion extends PureComponent {
|
||||
|
||||
this.state = {
|
||||
mentionComplete: false,
|
||||
initialVisibleMonth: (new Date()).toISOString(),
|
||||
futureScrollRange: 0,
|
||||
active: false,
|
||||
sections: [],
|
||||
};
|
||||
@@ -77,7 +74,7 @@ export default class DateSuggestion extends PureComponent {
|
||||
|
||||
setCalendarActive = (active) => {
|
||||
this.setState({active});
|
||||
};
|
||||
}
|
||||
|
||||
completeMention = (day) => {
|
||||
const mention = day.dateString;
|
||||
@@ -101,11 +98,7 @@ export default class DateSuggestion extends PureComponent {
|
||||
|
||||
onChangeText(completedDraft, true);
|
||||
this.props.onResultCountChange(1);
|
||||
|
||||
const initialVisibleMonth = (new Date(day.dateString)).toISOString();
|
||||
const futureScrollRange = moment().diff(moment(day.dateString), 'month');
|
||||
|
||||
this.setState({mentionComplete: true, initialVisibleMonth, futureScrollRange});
|
||||
this.setState({mentionComplete: true});
|
||||
};
|
||||
|
||||
resetComponent() {
|
||||
@@ -165,10 +158,10 @@ export default class DateSuggestion extends PureComponent {
|
||||
{Boolean(calendarWidth) &&
|
||||
<CalendarList
|
||||
testID='autocomplete.date_suggestion'
|
||||
current={this.state.initialVisibleMonth}
|
||||
current={currentDate}
|
||||
maxDate={currentDate}
|
||||
pastScrollRange={24}
|
||||
futureScrollRange={this.state.futureScrollRange}
|
||||
futureScrollRange={0}
|
||||
scrollingEnabled={true}
|
||||
calendarWidth={calendarWidth}
|
||||
pagingEnabled={true}
|
||||
|
||||
@@ -4685,6 +4685,7 @@ exports[`components/autocomplete/emoji_suggestion should match snapshot 2`] = `
|
||||
keyExtractor={[Function]}
|
||||
keyboardShouldPersistTaps="always"
|
||||
nestedScrollEnabled={false}
|
||||
numColumns={1}
|
||||
pageSize={10}
|
||||
removeClippedSubviews={true}
|
||||
renderItem={[Function]}
|
||||
|
||||
@@ -140,7 +140,7 @@ export default class EmojiSuggestion extends PureComponent {
|
||||
});
|
||||
};
|
||||
|
||||
getItemLayout = ({index}) => ({length: 40, offset: 40 * index, index});
|
||||
getItemLayout = ({index}) => ({length: 40, offset: 40 * index, index})
|
||||
|
||||
keyExtractor = (item) => item;
|
||||
|
||||
@@ -174,7 +174,7 @@ export default class EmojiSuggestion extends PureComponent {
|
||||
});
|
||||
|
||||
this.props.onResultCountChange(0);
|
||||
};
|
||||
}
|
||||
|
||||
searchEmojis = (searchTerm) => {
|
||||
const {emojis} = this.props;
|
||||
|
||||
@@ -31,6 +31,7 @@ exports[`components/autocomplete/slash_suggestion should match snapshot 1`] = `
|
||||
keyExtractor={[Function]}
|
||||
keyboardShouldPersistTaps="always"
|
||||
nestedScrollEnabled={false}
|
||||
numColumns={1}
|
||||
removeClippedSubviews={true}
|
||||
renderItem={[Function]}
|
||||
style={
|
||||
|
||||
@@ -33,11 +33,7 @@ describe('AppCommandParser', () => {
|
||||
...reduxTestState,
|
||||
entities: {
|
||||
...reduxTestState.entities,
|
||||
apps: {
|
||||
bindings,
|
||||
threadBindings: bindings,
|
||||
threadBindingsForms: {},
|
||||
},
|
||||
apps: {bindings},
|
||||
},
|
||||
} as any;
|
||||
const testStore = await mockStore(initialState);
|
||||
@@ -419,60 +415,6 @@ describe('AppCommandParser', () => {
|
||||
command: '/jira issue create --project == test',
|
||||
submit: {expectError: 'Multiple `=` signs are not allowed.'},
|
||||
},
|
||||
{
|
||||
title: 'rest field',
|
||||
command: '/jira issue rest hello world',
|
||||
autocomplete: {verify: (parsed: ParsedCommand): void => {
|
||||
expect(parsed.state).toBe(ParseState.Rest);
|
||||
expect(parsed.binding?.label).toBe('rest');
|
||||
expect(parsed.incomplete).toBe('hello world');
|
||||
expect(parsed.values?.summary).toBe(undefined);
|
||||
}},
|
||||
submit: {verify: (parsed: ParsedCommand): void => {
|
||||
expect(parsed.state).toBe(ParseState.Rest);
|
||||
expect(parsed.binding?.label).toBe('rest');
|
||||
expect(parsed.values?.summary).toBe('hello world');
|
||||
}},
|
||||
},
|
||||
{
|
||||
title: 'rest field with other field',
|
||||
command: '/jira issue rest --verbose true hello world',
|
||||
autocomplete: {verify: (parsed: ParsedCommand): void => {
|
||||
expect(parsed.state).toBe(ParseState.Rest);
|
||||
expect(parsed.binding?.label).toBe('rest');
|
||||
expect(parsed.incomplete).toBe('hello world');
|
||||
expect(parsed.values?.summary).toBe(undefined);
|
||||
expect(parsed.values?.verbose).toBe('true');
|
||||
}},
|
||||
submit: {verify: (parsed: ParsedCommand): void => {
|
||||
expect(parsed.state).toBe(ParseState.Rest);
|
||||
expect(parsed.binding?.label).toBe('rest');
|
||||
expect(parsed.values?.summary).toBe('hello world');
|
||||
expect(parsed.values?.verbose).toBe('true');
|
||||
}},
|
||||
},
|
||||
{
|
||||
title: 'rest field as flag with other field',
|
||||
command: '/jira issue rest --summary "hello world" --verbose true',
|
||||
autocomplete: {verify: (parsed: ParsedCommand): void => {
|
||||
expect(parsed.state).toBe(ParseState.EndValue);
|
||||
expect(parsed.binding?.label).toBe('rest');
|
||||
expect(parsed.incomplete).toBe('true');
|
||||
expect(parsed.values?.summary).toBe('hello world');
|
||||
expect(parsed.values?.verbose).toBe(undefined);
|
||||
}},
|
||||
submit: {verify: (parsed: ParsedCommand): void => {
|
||||
expect(parsed.state).toBe(ParseState.EndValue);
|
||||
expect(parsed.binding?.label).toBe('rest');
|
||||
expect(parsed.values?.summary).toBe('hello world');
|
||||
expect(parsed.values?.verbose).toBe('true');
|
||||
}},
|
||||
},
|
||||
{
|
||||
title: 'error: rest after rest field flag',
|
||||
command: '/jira issue rest --summary "hello world" --verbose true hello world',
|
||||
submit: {expectError: 'Unable to identify argument.'},
|
||||
},
|
||||
];
|
||||
|
||||
table.forEach((tc) => {
|
||||
@@ -562,13 +504,6 @@ describe('AppCommandParser', () => {
|
||||
IconData: 'Create icon',
|
||||
Description: 'Create a new Jira issue',
|
||||
},
|
||||
{
|
||||
Suggestion: 'rest',
|
||||
Complete: 'jira issue rest',
|
||||
Hint: 'rest hint',
|
||||
IconData: 'rest icon',
|
||||
Description: 'rest description',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -589,14 +524,6 @@ describe('AppCommandParser', () => {
|
||||
IconData: 'Create icon',
|
||||
Description: 'Create a new Jira issue',
|
||||
},
|
||||
{
|
||||
Suggestion: 'rest',
|
||||
Complete: 'JiRa IsSuE rest',
|
||||
Hint: 'rest hint',
|
||||
IconData: 'rest icon',
|
||||
Description: 'rest description',
|
||||
},
|
||||
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -928,7 +855,7 @@ describe('AppCommandParser', () => {
|
||||
context: {
|
||||
app_id: 'jira',
|
||||
channel_id: 'current_channel_id',
|
||||
location: '/command/jira/issue/create',
|
||||
location: '/command',
|
||||
root_id: 'root_id',
|
||||
team_id: 'team_id',
|
||||
},
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
/* eslint-disable max-lines */
|
||||
|
||||
import {
|
||||
AppsTypes,
|
||||
AppCallRequest,
|
||||
AppBinding,
|
||||
AppField,
|
||||
@@ -33,6 +32,7 @@ import {
|
||||
EXECUTE_CURRENT_COMMAND_ITEM_ID,
|
||||
COMMAND_SUGGESTION_ERROR,
|
||||
getExecuteSuggestion,
|
||||
displayError,
|
||||
createCallRequest,
|
||||
selectUserByUsername,
|
||||
getUserByUsername,
|
||||
@@ -45,13 +45,10 @@ import {
|
||||
filterEmptyOptions,
|
||||
autocompleteUsersInChannel,
|
||||
autocompleteChannels,
|
||||
ExtendedAutocompleteSuggestion,
|
||||
getChannelSuggestions,
|
||||
getUserSuggestions,
|
||||
inTextMentionSuggestions,
|
||||
ExtendedAutocompleteSuggestion,
|
||||
getAppCommandForm,
|
||||
getAppRHSCommandForm,
|
||||
makeRHSAppBindingSelector,
|
||||
} from './app_command_parser_dependencies';
|
||||
|
||||
export interface Store {
|
||||
@@ -77,7 +74,6 @@ export enum ParseState {
|
||||
EndQuotedValue = 'EndQuotedValue',
|
||||
EndTickedValue = 'EndTickedValue',
|
||||
Error = 'Error',
|
||||
Rest = 'Rest',
|
||||
}
|
||||
|
||||
interface FormsCache {
|
||||
@@ -89,7 +85,6 @@ interface Intl {
|
||||
}
|
||||
|
||||
const getCommandBindings = makeAppBindingsSelector(AppBindingLocations.COMMAND);
|
||||
const getRHSCommandBindings = makeRHSAppBindingSelector(AppBindingLocations.COMMAND);
|
||||
|
||||
export class ParsedCommand {
|
||||
state = ParseState.Start;
|
||||
@@ -254,7 +249,7 @@ export class ParsedCommand {
|
||||
}
|
||||
|
||||
return this;
|
||||
};
|
||||
}
|
||||
|
||||
// parseForm parses the rest of the command using the previously matched form.
|
||||
public parseForm = (autocompleteMode = false): ParsedCommand => {
|
||||
@@ -304,20 +299,12 @@ export class ParsedCommand {
|
||||
// Positional parameter.
|
||||
this.position++;
|
||||
// eslint-disable-next-line no-loop-func
|
||||
let field = fields.find((f: AppField) => f.position === this.position);
|
||||
const field = fields.find((f: AppField) => f.position === this.position);
|
||||
if (!field) {
|
||||
field = fields.find((f) => f.position === -1 && f.type === AppFieldTypes.TEXT);
|
||||
if (!field || this.values[field.name]) {
|
||||
return this.asError(this.intl.formatMessage({
|
||||
id: 'apps.error.parser.no_argument_pos_x',
|
||||
defaultMessage: 'Unable to identify argument.',
|
||||
}));
|
||||
}
|
||||
this.incompleteStart = this.i;
|
||||
this.incomplete = '';
|
||||
this.field = field;
|
||||
this.state = ParseState.Rest;
|
||||
break;
|
||||
return this.asError(this.intl.formatMessage({
|
||||
id: 'apps.error.parser.no_argument_pos_x',
|
||||
defaultMessage: 'Unable to identify argument.',
|
||||
}));
|
||||
}
|
||||
this.field = field;
|
||||
this.state = ParseState.StartValue;
|
||||
@@ -327,28 +314,6 @@ export class ParsedCommand {
|
||||
break;
|
||||
}
|
||||
|
||||
case ParseState.Rest: {
|
||||
if (!this.field) {
|
||||
return this.asError(this.intl.formatMessage({
|
||||
id: 'apps.error.parser.missing_field_value',
|
||||
defaultMessage: 'Field value is missing.',
|
||||
}));
|
||||
}
|
||||
|
||||
if (autocompleteMode && c === '') {
|
||||
return this;
|
||||
}
|
||||
|
||||
if (c === '') {
|
||||
this.values[this.field.name] = this.incomplete;
|
||||
return this;
|
||||
}
|
||||
|
||||
this.i++;
|
||||
this.incomplete += c;
|
||||
break;
|
||||
}
|
||||
|
||||
case ParseState.ParameterSeparator: {
|
||||
this.incompleteStart = this.i;
|
||||
switch (c) {
|
||||
@@ -505,7 +470,7 @@ export class ParsedCommand {
|
||||
if (this.incompleteStart === this.i - 1) {
|
||||
return this.asError(this.intl.formatMessage({
|
||||
id: 'apps.error.parser.empty_value',
|
||||
defaultMessage: 'Empty values are not allowed.',
|
||||
defaultMessage: 'empty values are not allowed',
|
||||
}));
|
||||
}
|
||||
this.i++;
|
||||
@@ -545,7 +510,7 @@ export class ParsedCommand {
|
||||
if (this.incompleteStart === this.i - 1) {
|
||||
return this.asError(this.intl.formatMessage({
|
||||
id: 'apps.error.parser.empty_value',
|
||||
defaultMessage: 'Empty values are not allowed.',
|
||||
defaultMessage: 'empty values are not allowed',
|
||||
}));
|
||||
}
|
||||
this.i++;
|
||||
@@ -579,13 +544,13 @@ export class ParsedCommand {
|
||||
(!autocompleteMode && this.incomplete !== 'true' && this.incomplete !== 'false'))) {
|
||||
// reset back where the value started, and treat as a new parameter
|
||||
this.i = this.incompleteStart;
|
||||
this.values[this.field.name] = 'true';
|
||||
this.values![this.field.name] = 'true';
|
||||
this.state = ParseState.StartParameter;
|
||||
} else {
|
||||
if (autocompleteMode && c === '') {
|
||||
return this;
|
||||
}
|
||||
this.values[this.field.name] = this.incomplete;
|
||||
this.values![this.field.name] = this.incomplete;
|
||||
this.incomplete = '';
|
||||
this.incompleteStart = this.i;
|
||||
if (c === '') {
|
||||
@@ -597,7 +562,7 @@ export class ParsedCommand {
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class AppCommandParser {
|
||||
@@ -607,6 +572,8 @@ export class AppCommandParser {
|
||||
private rootPostID?: string;
|
||||
private intl: Intl;
|
||||
|
||||
forms: {[location: string]: AppForm} = {};
|
||||
|
||||
constructor(store: Store|null, intl: Intl, channelID: string, teamID = '', rootPostID = '') {
|
||||
this.store = store || getStore() as Store;
|
||||
this.channelID = channelID;
|
||||
@@ -649,62 +616,60 @@ export class AppCommandParser {
|
||||
}
|
||||
|
||||
return this.composeCallFromParsed(parsed);
|
||||
};
|
||||
}
|
||||
|
||||
private async addDefaultAndReadOnlyValues(parsed: ParsedCommand) {
|
||||
if (!parsed.form?.fields) {
|
||||
return;
|
||||
}
|
||||
|
||||
await Promise.all(parsed.form.fields.map(async (f) => {
|
||||
await Promise.all(parsed.form?.fields?.map(async (f) => {
|
||||
if (!f.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (f.readonly || !(f.name in parsed.values)) {
|
||||
switch (f.type) {
|
||||
case AppFieldTypes.TEXT:
|
||||
parsed.values[f.name] = f.value as string;
|
||||
break;
|
||||
case AppFieldTypes.BOOL:
|
||||
parsed.values[f.name] = 'true';
|
||||
break;
|
||||
case AppFieldTypes.USER: {
|
||||
const userID = (f.value as AppSelectOption).value;
|
||||
let user = selectUser(this.store.getState(), userID);
|
||||
if (!user) {
|
||||
const dispatchResult = await this.store.dispatch(getUser(userID));
|
||||
if ('error' in dispatchResult) {
|
||||
// Silently fail on default value
|
||||
break;
|
||||
}
|
||||
user = dispatchResult.data;
|
||||
}
|
||||
parsed.values[f.name] = user.username;
|
||||
break;
|
||||
}
|
||||
case AppFieldTypes.CHANNEL: {
|
||||
const channelID = (f.value as AppSelectOption).label;
|
||||
let channel = selectChannel(this.store.getState(), channelID);
|
||||
if (!channel) {
|
||||
const dispatchResult = await this.store.dispatch(getChannel(channelID));
|
||||
if ('error' in dispatchResult) {
|
||||
// Silently fail on default value
|
||||
break;
|
||||
}
|
||||
channel = dispatchResult.data;
|
||||
}
|
||||
parsed.values[f.name] = channel.name;
|
||||
break;
|
||||
}
|
||||
case AppFieldTypes.STATIC_SELECT:
|
||||
case AppFieldTypes.DYNAMIC_SELECT:
|
||||
parsed.values[f.name] = (f.value as AppSelectOption).value;
|
||||
break;
|
||||
case AppFieldTypes.MARKDOWN:
|
||||
if (!f.readonly || f.name in parsed.values) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Do nothing
|
||||
switch (f.type) {
|
||||
case AppFieldTypes.TEXT:
|
||||
parsed.values[f.name] = f.value as string;
|
||||
break;
|
||||
case AppFieldTypes.BOOL:
|
||||
parsed.values[f.name] = 'true';
|
||||
break;
|
||||
case AppFieldTypes.USER: {
|
||||
const userID = (f.value as AppSelectOption).value;
|
||||
let user = selectUser(this.store.getState(), userID);
|
||||
if (!user) {
|
||||
const dispatchResult = await this.store.dispatch(getUser(userID));
|
||||
if ('error' in dispatchResult) {
|
||||
// Silently fail on default value
|
||||
break;
|
||||
}
|
||||
user = dispatchResult.data;
|
||||
}
|
||||
parsed.values[f.name] = user.username;
|
||||
break;
|
||||
}
|
||||
case AppFieldTypes.CHANNEL: {
|
||||
const channelID = (f.value as AppSelectOption).label;
|
||||
let channel = selectChannel(this.store.getState(), channelID);
|
||||
if (!channel) {
|
||||
const dispatchResult = await this.store.dispatch(getChannel(channelID));
|
||||
if ('error' in dispatchResult) {
|
||||
// Silently fail on default value
|
||||
break;
|
||||
}
|
||||
channel = dispatchResult.data;
|
||||
}
|
||||
parsed.values[f.name] = channel.name;
|
||||
break;
|
||||
}
|
||||
case AppFieldTypes.STATIC_SELECT:
|
||||
case AppFieldTypes.DYNAMIC_SELECT:
|
||||
parsed.values[f.name] = (f.value as AppSelectOption).value;
|
||||
break;
|
||||
case AppFieldTypes.MARKDOWN:
|
||||
|
||||
// Do nothing
|
||||
}
|
||||
}) || []);
|
||||
}
|
||||
@@ -738,7 +703,7 @@ export class AppCommandParser {
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
// getSuggestions returns suggestions for subcommands and/or form arguments
|
||||
public getSuggestions = async (pretext: string): Promise<ExtendedAutocompleteSuggestion[]> => {
|
||||
@@ -790,7 +755,7 @@ export class AppCommandParser {
|
||||
suggestions = this.getNoMatchingSuggestion();
|
||||
}
|
||||
return suggestions.map((suggestion) => this.decorateSuggestionComplete(parsed, suggestion));
|
||||
};
|
||||
}
|
||||
|
||||
getNoMatchingSuggestion = () => {
|
||||
return [{
|
||||
@@ -803,7 +768,7 @@ export class AppCommandParser {
|
||||
IconData: COMMAND_SUGGESTION_ERROR,
|
||||
Description: '',
|
||||
}];
|
||||
};
|
||||
}
|
||||
|
||||
getErrorSuggestion = (parsed: ParsedCommand) => {
|
||||
return [{
|
||||
@@ -816,7 +781,7 @@ export class AppCommandParser {
|
||||
IconData: COMMAND_SUGGESTION_ERROR,
|
||||
Description: parsed.error,
|
||||
}];
|
||||
};
|
||||
}
|
||||
|
||||
// composeCallFromParsed creates the form submission call
|
||||
private composeCallFromParsed = async (parsed: ParsedCommand): Promise<{call: AppCallRequest | null; errorMessage?: string}> => {
|
||||
@@ -846,7 +811,7 @@ export class AppCommandParser {
|
||||
|
||||
const context = this.getAppContext(parsed.binding);
|
||||
return {call: createCallRequest(call, context, {}, values, parsed.command)};
|
||||
};
|
||||
}
|
||||
|
||||
private expandOptions = async (parsed: ParsedCommand, values: AppCallValues): Promise<{errorMessage?: string}> => {
|
||||
if (!parsed.form?.fields) {
|
||||
@@ -935,7 +900,7 @@ export class AppCommandParser {
|
||||
errorMessage = errorMessage + errors[v] + '\n';
|
||||
});
|
||||
return {errorMessage};
|
||||
};
|
||||
}
|
||||
|
||||
// decorateSuggestionComplete applies the necessary modifications for a suggestion to be processed
|
||||
private decorateSuggestionComplete = (parsed: ParsedCommand, choice: AutocompleteSuggestion): AutocompleteSuggestion => {
|
||||
@@ -956,29 +921,29 @@ export class AppCommandParser {
|
||||
...choice,
|
||||
Complete: complete,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
// getCommandBindings returns the commands in the redux store.
|
||||
// They are grouped by app id since each app has one base command
|
||||
private getCommandBindings = (): AppBinding[] => {
|
||||
const state = this.store.getState();
|
||||
if (this.rootPostID) {
|
||||
return getRHSCommandBindings(state);
|
||||
}
|
||||
return getCommandBindings(state);
|
||||
};
|
||||
const bindings = getCommandBindings(this.store.getState());
|
||||
return bindings;
|
||||
}
|
||||
|
||||
// getChannel gets the channel in which the user is typing the command
|
||||
private getChannel = (): Channel | null => {
|
||||
const state = this.store.getState();
|
||||
return selectChannel(state, this.channelID);
|
||||
};
|
||||
}
|
||||
|
||||
public setChannelContext = (channelID: string, teamID = '', rootPostID?: string) => {
|
||||
if (this.channelID !== channelID || this.rootPostID !== rootPostID || this.teamID !== teamID) {
|
||||
this.forms = {};
|
||||
}
|
||||
this.channelID = channelID;
|
||||
this.rootPostID = rootPostID;
|
||||
this.teamID = teamID;
|
||||
};
|
||||
}
|
||||
|
||||
// isAppCommand determines if subcommand/form suggestions need to be returned.
|
||||
// When this returns true, the caller knows that the parser should handle all suggestions for the current command string.
|
||||
@@ -1000,7 +965,7 @@ export class AppCommandParser {
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
// getAppContext collects post/channel/team info for performing calls
|
||||
private getAppContext = (binding: AppBinding): AppContext => {
|
||||
@@ -1016,18 +981,15 @@ export class AppCommandParser {
|
||||
}
|
||||
|
||||
context.channel_id = channel.id;
|
||||
context.team_id = channel.team_id || getCurrentTeamId(this.store.getState());
|
||||
context.team_id = this.teamID || channel.team_id || getCurrentTeamId(this.store.getState());
|
||||
|
||||
return context;
|
||||
};
|
||||
}
|
||||
|
||||
// fetchForm unconditionaly retrieves the form for the given binding (subcommand)
|
||||
private fetchForm = async (binding: AppBinding): Promise<{form?: AppForm; error?: string} | undefined> => {
|
||||
if (!binding.call) {
|
||||
return {error: this.intl.formatMessage({
|
||||
id: 'apps.error.parser.missing_call',
|
||||
defaultMessage: 'Missing binding call.',
|
||||
})};
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const payload = createCallRequest(
|
||||
@@ -1066,29 +1028,32 @@ export class AppCommandParser {
|
||||
}
|
||||
|
||||
return {form: callResponse.form};
|
||||
};
|
||||
}
|
||||
|
||||
public getForm = async (location: string, binding: AppBinding): Promise<{form?: AppForm; error?: string} | undefined> => {
|
||||
const rootID = this.rootPostID || '';
|
||||
const key = `${this.channelID}-${rootID}-${location}`;
|
||||
const form = this.rootPostID ? getAppRHSCommandForm(this.store.getState(), key) : getAppCommandForm(this.store.getState(), key);
|
||||
const form = this.forms[key];
|
||||
if (form) {
|
||||
return {form};
|
||||
}
|
||||
|
||||
this.forms = {};
|
||||
const fetched = await this.fetchForm(binding);
|
||||
if (fetched?.form) {
|
||||
let actionType: string = AppsTypes.RECEIVED_APP_COMMAND_FORM;
|
||||
if (this.rootPostID) {
|
||||
actionType = AppsTypes.RECEIVED_APP_RHS_COMMAND_FORM;
|
||||
}
|
||||
this.store.dispatch({
|
||||
data: {form: fetched.form, location: key},
|
||||
type: actionType,
|
||||
});
|
||||
this.forms[key] = fetched.form;
|
||||
}
|
||||
return fetched;
|
||||
};
|
||||
}
|
||||
|
||||
// displayError shows an error that was caught by the parser
|
||||
private displayError = (err: any): void => {
|
||||
let errStr = err as string;
|
||||
if (err.message) {
|
||||
errStr = err.message;
|
||||
}
|
||||
displayError(this.intl, errStr);
|
||||
}
|
||||
|
||||
// getSuggestionsForSubCommands returns suggestions for a subcommand's name
|
||||
private getCommandSuggestions = (parsed: ParsedCommand): AutocompleteSuggestion[] => {
|
||||
@@ -1111,7 +1076,7 @@ export class AppCommandParser {
|
||||
});
|
||||
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
// getParameterSuggestions computes suggestions for positional argument values, flag names, and flag argument values
|
||||
private getParameterSuggestions = async (parsed: ParsedCommand): Promise<ExtendedAutocompleteSuggestion[]> => {
|
||||
@@ -1139,17 +1104,9 @@ export class AppCommandParser {
|
||||
case ParseState.EndTickedValue:
|
||||
case ParseState.TickValue:
|
||||
return this.getValueSuggestions(parsed, '`');
|
||||
case ParseState.Rest: {
|
||||
const execute = getExecuteSuggestion(parsed);
|
||||
const value = await this.getValueSuggestions(parsed);
|
||||
if (execute) {
|
||||
return [execute, ...value];
|
||||
}
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return [];
|
||||
};
|
||||
}
|
||||
|
||||
// getMissingFields collects the required fields that were not supplied in a submission
|
||||
private getMissingFields = (parsed: ParsedCommand): AppField[] => {
|
||||
@@ -1169,7 +1126,7 @@ export class AppCommandParser {
|
||||
}
|
||||
|
||||
return missing;
|
||||
};
|
||||
}
|
||||
|
||||
// getFlagNameSuggestions returns suggestions for flag names
|
||||
private getFlagNameSuggestions = (parsed: ParsedCommand): AutocompleteSuggestion[] => {
|
||||
@@ -1201,7 +1158,7 @@ export class AppCommandParser {
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
||||
}
|
||||
|
||||
// getSuggestionsForField gets suggestions for a positional or flag field value
|
||||
private getValueSuggestions = async (parsed: ParsedCommand, delimiter?: string): Promise<ExtendedAutocompleteSuggestion[]> => {
|
||||
@@ -1242,7 +1199,7 @@ export class AppCommandParser {
|
||||
Hint: '',
|
||||
IconData: parsed.binding?.icon || '',
|
||||
}];
|
||||
};
|
||||
}
|
||||
|
||||
// getStaticSelectSuggestions returns suggestions specified in the field's options property
|
||||
private getStaticSelectSuggestions = (parsed: ParsedCommand, delimiter?: string): AutocompleteSuggestion[] => {
|
||||
@@ -1275,7 +1232,7 @@ export class AppCommandParser {
|
||||
IconData: opt.icon_data || parsed.binding?.icon || '',
|
||||
};
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
// getDynamicSelectSuggestions fetches and returns suggestions from the server
|
||||
private getDynamicSelectSuggestions = async (parsed: ParsedCommand, delimiter?: string): Promise<AutocompleteSuggestion[]> => {
|
||||
@@ -1364,7 +1321,7 @@ export class AppCommandParser {
|
||||
IconData: s.icon_data || parsed.binding?.icon || '',
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
private makeDynamicSelectSuggestionError = (message: string): AutocompleteSuggestion[] => {
|
||||
const errMsg = this.intl.formatMessage({
|
||||
@@ -1375,15 +1332,15 @@ export class AppCommandParser {
|
||||
});
|
||||
return [{
|
||||
Complete: '',
|
||||
Suggestion: '',
|
||||
Hint: this.intl.formatMessage({
|
||||
Suggestion: this.intl.formatMessage({
|
||||
id: 'apps.suggestion.dynamic.error',
|
||||
defaultMessage: 'Dynamic select error',
|
||||
}),
|
||||
Hint: '',
|
||||
IconData: COMMAND_SUGGESTION_ERROR,
|
||||
Description: errMsg,
|
||||
}];
|
||||
};
|
||||
}
|
||||
|
||||
private getUserFieldSuggestions = async (parsed: ParsedCommand): Promise<AutocompleteSuggestion[]> => {
|
||||
let input = parsed.incomplete.trim();
|
||||
@@ -1392,7 +1349,7 @@ export class AppCommandParser {
|
||||
}
|
||||
const {data} = await this.store.dispatch(autocompleteUsersInChannel(input, this.channelID));
|
||||
return getUserSuggestions(data);
|
||||
};
|
||||
}
|
||||
|
||||
private getChannelFieldSuggestions = async (parsed: ParsedCommand): Promise<AutocompleteSuggestion[]> => {
|
||||
let input = parsed.incomplete.trim();
|
||||
@@ -1401,7 +1358,7 @@ export class AppCommandParser {
|
||||
}
|
||||
const {data} = await this.store.dispatch(autocompleteChannels(this.teamID, input));
|
||||
return getChannelSuggestions(data);
|
||||
};
|
||||
}
|
||||
|
||||
// getBooleanSuggestions returns true/false suggestions
|
||||
private getBooleanSuggestions = (parsed: ParsedCommand): AutocompleteSuggestion[] => {
|
||||
@@ -1426,7 +1383,7 @@ export class AppCommandParser {
|
||||
});
|
||||
}
|
||||
return suggestions;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function isMultiword(value: string) {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {intlShape} from 'react-intl';
|
||||
import {Alert} from 'react-native';
|
||||
|
||||
import {getUserByUsername, getUser, autocompleteUsers} from '@mm-redux/actions/users';
|
||||
import {getCurrentTeamId, getCurrentTeam} from '@mm-redux/selectors/entities/teams';
|
||||
@@ -36,8 +37,6 @@ export type {
|
||||
DoAppCallResult,
|
||||
} from 'types/actions/apps';
|
||||
|
||||
export {AppsTypes} from '@mm-redux/action_types';
|
||||
|
||||
export type {AutocompleteSuggestion};
|
||||
|
||||
export type {
|
||||
@@ -66,7 +65,7 @@ export {
|
||||
COMMAND_SUGGESTION_USER,
|
||||
} from '@mm-redux/constants/apps';
|
||||
|
||||
export {makeAppBindingsSelector, makeRHSAppBindingSelector, getAppCommandForm, getAppRHSCommandForm} from '@mm-redux/selectors/entities/apps';
|
||||
export {makeAppBindingsSelector} from '@mm-redux/selectors/entities/apps';
|
||||
|
||||
export {getPost} from '@mm-redux/selectors/entities/posts';
|
||||
export {getChannel as selectChannel, getCurrentChannel, getChannelByName as selectChannelByName} from '@mm-redux/selectors/entities/channels';
|
||||
@@ -113,6 +112,14 @@ export const getExecuteSuggestion = (_: ParsedCommand): AutocompleteSuggestion |
|
||||
return null;
|
||||
};
|
||||
|
||||
export const displayError = (intl: typeof intlShape, body: string) => {
|
||||
const title = intl.formatMessage({
|
||||
id: 'mobile.general.error.title',
|
||||
defaultMessage: 'Error',
|
||||
});
|
||||
Alert.alert(title, body);
|
||||
};
|
||||
|
||||
export const errorMessage = (intl: typeof intlShape, error: string, _command: string, _position: number): string => { // eslint-disable-line @typescript-eslint/no-unused-vars
|
||||
return intl.formatMessage({
|
||||
id: 'apps.error.parser',
|
||||
|
||||
@@ -181,37 +181,6 @@ export const createCommand: AppBinding = {
|
||||
} as AppForm,
|
||||
};
|
||||
|
||||
export const restCommand: AppBinding = {
|
||||
app_id: 'jira',
|
||||
label: 'rest',
|
||||
location: 'rest',
|
||||
description: 'rest description',
|
||||
icon: 'rest icon',
|
||||
hint: 'rest hint',
|
||||
form: {
|
||||
call: {
|
||||
path: '/create-issue',
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'summary',
|
||||
label: 'summary',
|
||||
description: 'The Jira issue summary',
|
||||
type: AppFieldTypes.TEXT,
|
||||
hint: 'The thing is working great!',
|
||||
position: -1,
|
||||
},
|
||||
{
|
||||
name: 'verbose',
|
||||
label: 'verbose',
|
||||
description: 'display details',
|
||||
type: AppFieldTypes.BOOL,
|
||||
hint: 'yes or no!',
|
||||
},
|
||||
],
|
||||
} as AppForm,
|
||||
};
|
||||
|
||||
export const testBindings: AppBinding[] = [
|
||||
{
|
||||
app_id: '',
|
||||
@@ -235,7 +204,6 @@ export const testBindings: AppBinding[] = [
|
||||
bindings: [
|
||||
viewCommand,
|
||||
createCommand,
|
||||
restCommand,
|
||||
],
|
||||
}],
|
||||
},
|
||||
|
||||
@@ -16,6 +16,7 @@ exports[`components/autocomplete/app_slash_suggestion should match snapshot 1`]
|
||||
keyExtractor={[Function]}
|
||||
keyboardShouldPersistTaps="always"
|
||||
nestedScrollEnabled={false}
|
||||
numColumns={1}
|
||||
removeClippedSubviews={true}
|
||||
renderItem={[Function]}
|
||||
style={
|
||||
|
||||
@@ -27,12 +27,7 @@ const makeStore = async (bindings: AppBinding[]) => {
|
||||
...reduxTestState,
|
||||
entities: {
|
||||
...reduxTestState.entities,
|
||||
apps: {
|
||||
bindings,
|
||||
bindingsForms: {},
|
||||
threadBindings: bindings,
|
||||
threadBindingsForms: {},
|
||||
},
|
||||
apps: {bindings},
|
||||
},
|
||||
} as any;
|
||||
const testStore = await mockStore(initialState);
|
||||
|
||||
@@ -109,13 +109,13 @@ export default class AppSlashSuggestion extends PureComponent<Props, State> {
|
||||
isAppCommand = (pretext: string, channelID: string, teamID = '', rootID?: string) => {
|
||||
this.appCommandParser.setChannelContext(channelID, teamID, rootID);
|
||||
return this.appCommandParser.isAppCommand(pretext);
|
||||
};
|
||||
}
|
||||
|
||||
fetchAndShowAppCommandSuggestions = async (pretext: string, channelID: string, teamID = '', rootID?: string) => {
|
||||
this.appCommandParser.setChannelContext(channelID, teamID, rootID);
|
||||
const suggestions = await this.appCommandParser.getSuggestions(pretext);
|
||||
this.updateSuggestions(suggestions);
|
||||
};
|
||||
}
|
||||
|
||||
updateSuggestions = (matches: ExtendedAutocompleteSuggestion[]) => {
|
||||
this.setState({
|
||||
@@ -123,7 +123,7 @@ export default class AppSlashSuggestion extends PureComponent<Props, State> {
|
||||
dataSource: matches,
|
||||
});
|
||||
this.props.onResultCountChange(matches.length);
|
||||
};
|
||||
}
|
||||
|
||||
completeSuggestion = (command: string) => {
|
||||
const {onChangeText} = this.props;
|
||||
@@ -157,13 +157,13 @@ export default class AppSlashSuggestion extends PureComponent<Props, State> {
|
||||
return () => {
|
||||
this.completeSuggestion(base);
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
completeChannelMention = (base: string): (channelName: string) => void => {
|
||||
return () => {
|
||||
this.completeSuggestion(base);
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
keyExtractor = (item: ExtendedAutocompleteSuggestion): string => item.Suggestion + item.type + item.item;
|
||||
|
||||
@@ -198,7 +198,7 @@ export default class AppSlashSuggestion extends PureComponent<Props, State> {
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
const {maxListHeight, theme, nestedScrollEnabled} = this.props;
|
||||
|
||||
@@ -144,13 +144,13 @@ export default class SlashSuggestion extends PureComponent<Props, State> {
|
||||
});
|
||||
|
||||
this.updateSuggestions(matches);
|
||||
};
|
||||
}
|
||||
|
||||
getAppBaseCommandSuggestions = (pretext: string, channelID: string, teamID = '', rootID?: string): AutocompleteSuggestion[] => {
|
||||
this.appCommandParser.setChannelContext(channelID, teamID, rootID);
|
||||
const suggestions = this.appCommandParser.getSuggestionsBase(pretext);
|
||||
return suggestions;
|
||||
};
|
||||
}
|
||||
|
||||
updateSuggestions = (matches: AutocompleteSuggestion[]) => {
|
||||
this.setState({
|
||||
@@ -158,7 +158,7 @@ export default class SlashSuggestion extends PureComponent<Props, State> {
|
||||
dataSource: matches,
|
||||
});
|
||||
this.props.onResultCountChange(matches.length);
|
||||
};
|
||||
}
|
||||
|
||||
filterCommands = (matchTerm: string, commands: Command[]): AutocompleteSuggestion[] => {
|
||||
const data = commands.filter((command) => {
|
||||
@@ -179,11 +179,11 @@ export default class SlashSuggestion extends PureComponent<Props, State> {
|
||||
IconData: item.icon_url || item.autocomplete_icon_data || '',
|
||||
};
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
contains = (matches: AutocompleteSuggestion[], complete: string): boolean => {
|
||||
return matches.findIndex((match) => match.Complete === complete) !== -1;
|
||||
};
|
||||
}
|
||||
|
||||
completeSuggestion = (command: string) => {
|
||||
const {onChangeText} = this.props;
|
||||
@@ -219,7 +219,7 @@ export default class SlashSuggestion extends PureComponent<Props, State> {
|
||||
complete={item.Complete}
|
||||
icon={item.IconData}
|
||||
/>
|
||||
);
|
||||
)
|
||||
|
||||
render() {
|
||||
const {maxListHeight, theme, nestedScrollEnabled} = this.props;
|
||||
|
||||
@@ -155,15 +155,13 @@ const SlashSuggestionItem = (props: Props) => {
|
||||
</View>
|
||||
<View style={style.suggestionContainer}>
|
||||
<Text style={style.suggestionName}>{`${suggestionText}`}</Text>
|
||||
{Boolean(description) &&
|
||||
<Text
|
||||
ellipsizeMode='tail'
|
||||
numberOfLines={1}
|
||||
style={style.suggestionDescription}
|
||||
>
|
||||
{description}
|
||||
</Text>
|
||||
}
|
||||
<Text
|
||||
ellipsizeMode='tail'
|
||||
numberOfLines={1}
|
||||
style={style.suggestionDescription}
|
||||
>
|
||||
{description}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</TouchableWithFeedback>
|
||||
|
||||
@@ -48,7 +48,7 @@ export default class SpecialMentionItem extends PureComponent {
|
||||
<View style={style.row}>
|
||||
<View style={style.rowPicture}>
|
||||
<CompassIcon
|
||||
name='account-multiple-outline'
|
||||
name='account-group-outline'
|
||||
style={style.rowIcon}
|
||||
/>
|
||||
</View>
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import React, {PureComponent} from 'react';
|
||||
import {intlShape} from 'react-intl';
|
||||
import {Text, View, Platform} from 'react-native';
|
||||
import {Text, View} from 'react-native';
|
||||
|
||||
import {goToScreen} from '@actions/navigation';
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
@@ -11,46 +12,33 @@ import FormattedText from '@components/formatted_text';
|
||||
import Markdown from '@components/markdown';
|
||||
import TouchableWithFeedback from '@components/touchable_with_feedback';
|
||||
import {ViewTypes} from '@constants';
|
||||
import {ActionResult} from '@mm-redux/types/actions';
|
||||
import {Channel} from '@mm-redux/types/channels';
|
||||
import {DialogOption} from '@mm-redux/types/integrations';
|
||||
import {Theme} from '@mm-redux/types/theme';
|
||||
import {UserProfile} from '@mm-redux/types/users';
|
||||
import {displayUsername} from '@mm-redux/utils/user_utils';
|
||||
import {getMarkdownBlockStyles, getMarkdownTextStyles} from '@utils/markdown';
|
||||
import {preventDoubleTap} from '@utils/tap';
|
||||
import {makeStyleSheetFromTheme, changeOpacity} from '@utils/theme';
|
||||
|
||||
type Selection = DialogOption | Channel | UserProfile | DialogOption[] | Channel[] | UserProfile[];
|
||||
|
||||
type Props = {
|
||||
actions: {
|
||||
setAutocompleteSelector: (dataSource: any, onSelect: any, options: any, getDynamicOptions: any) => Promise<ActionResult>;
|
||||
export default class AutocompleteSelector extends PureComponent {
|
||||
static propTypes = {
|
||||
actions: PropTypes.shape({
|
||||
setAutocompleteSelector: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
getDynamicOptions: PropTypes.func,
|
||||
label: PropTypes.string,
|
||||
placeholder: PropTypes.string.isRequired,
|
||||
dataSource: PropTypes.string,
|
||||
options: PropTypes.arrayOf(PropTypes.object),
|
||||
selected: PropTypes.object,
|
||||
optional: PropTypes.bool,
|
||||
showRequiredAsterisk: PropTypes.bool,
|
||||
teammateNameDisplay: PropTypes.string,
|
||||
theme: PropTypes.object.isRequired,
|
||||
onSelected: PropTypes.func,
|
||||
helpText: PropTypes.node,
|
||||
errorText: PropTypes.node,
|
||||
roundedBorders: PropTypes.bool,
|
||||
disabled: PropTypes.bool,
|
||||
};
|
||||
getDynamicOptions?: (term: string) => Promise<ActionResult>;
|
||||
label?: string;
|
||||
placeholder?: string;
|
||||
dataSource?: string;
|
||||
options?: DialogOption[];
|
||||
selected?: DialogOption | DialogOption[];
|
||||
optional?: boolean;
|
||||
showRequiredAsterisk?: boolean;
|
||||
teammateNameDisplay?: string;
|
||||
theme: Theme;
|
||||
onSelected?: ((item: DialogOption) => void) | ((item: DialogOption[]) => void);
|
||||
helpText?: string;
|
||||
errorText?: string;
|
||||
roundedBorders?: boolean;
|
||||
disabled?: boolean;
|
||||
isMultiselect?: boolean;
|
||||
}
|
||||
|
||||
type State = {
|
||||
selectedText: string;
|
||||
selected?: DialogOption | DialogOption[];
|
||||
}
|
||||
|
||||
export default class AutocompleteSelector extends PureComponent<Props, State> {
|
||||
static contextTypes = {
|
||||
intl: intlShape,
|
||||
};
|
||||
@@ -61,45 +49,26 @@ export default class AutocompleteSelector extends PureComponent<Props, State> {
|
||||
roundedBorders: true,
|
||||
};
|
||||
|
||||
constructor(props: Props) {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
selectedText: '',
|
||||
selectedText: null,
|
||||
};
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(props: Props, state: State) {
|
||||
if (!props.selected || props.selected === state.selected) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!props.isMultiselect) {
|
||||
static getDerivedStateFromProps(props, state) {
|
||||
if (props.selected && props.selected !== state.selected) {
|
||||
return {
|
||||
selectedText: (props.selected as DialogOption).text,
|
||||
selectedText: props.selected.text,
|
||||
selected: props.selected,
|
||||
};
|
||||
}
|
||||
|
||||
const options = props.selected as DialogOption[];
|
||||
let selectedText = '';
|
||||
const selected: DialogOption[] = [];
|
||||
|
||||
options.forEach((option) => {
|
||||
if (selectedText !== '') {
|
||||
selectedText += ', ';
|
||||
}
|
||||
selectedText += option.text;
|
||||
selected.push(option);
|
||||
});
|
||||
|
||||
return {
|
||||
selectedText,
|
||||
selected,
|
||||
};
|
||||
return null;
|
||||
}
|
||||
|
||||
handleSelect = (selected: Selection) => {
|
||||
handleSelect = (selected) => {
|
||||
if (!selected) {
|
||||
return;
|
||||
}
|
||||
@@ -109,113 +78,34 @@ export default class AutocompleteSelector extends PureComponent<Props, State> {
|
||||
teammateNameDisplay,
|
||||
} = this.props;
|
||||
|
||||
if (!this.props.isMultiselect) {
|
||||
let selectedText: string;
|
||||
let selectedValue: string;
|
||||
switch (dataSource) {
|
||||
case ViewTypes.DATA_SOURCE_USERS: {
|
||||
const typedSelected = selected as UserProfile;
|
||||
selectedText = displayUsername(typedSelected, teammateNameDisplay || '');
|
||||
selectedValue = typedSelected.id;
|
||||
break;
|
||||
}
|
||||
case ViewTypes.DATA_SOURCE_CHANNELS: {
|
||||
const typedSelected = selected as Channel;
|
||||
selectedText = typedSelected.display_name;
|
||||
selectedValue = typedSelected.id;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
const typedSelected = selected as DialogOption;
|
||||
selectedText = typedSelected.text;
|
||||
selectedValue = typedSelected.value;
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({selectedText});
|
||||
|
||||
if (this.props.onSelected) {
|
||||
(this.props.onSelected as (opt: DialogOption) => void)({text: selectedText, value: selectedValue});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let selectedText = '';
|
||||
const selectedOptions: DialogOption[] = [];
|
||||
switch (dataSource) {
|
||||
case ViewTypes.DATA_SOURCE_USERS: {
|
||||
const typedSelected = selected as UserProfile[];
|
||||
typedSelected.forEach((option) => {
|
||||
if (selectedText !== '') {
|
||||
selectedText += ', ';
|
||||
}
|
||||
const text = displayUsername(option, teammateNameDisplay || '');
|
||||
selectedText += text;
|
||||
selectedOptions.push({text, value: option.id});
|
||||
});
|
||||
break;
|
||||
}
|
||||
case ViewTypes.DATA_SOURCE_CHANNELS: {
|
||||
const typedSelected = selected as Channel[];
|
||||
typedSelected.forEach((option) => {
|
||||
if (selectedText !== '') {
|
||||
selectedText += ', ';
|
||||
}
|
||||
const text = option.display_name;
|
||||
selectedText += text;
|
||||
selectedOptions.push({text, value: option.id});
|
||||
});
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
const typedSelected = selected as DialogOption[];
|
||||
typedSelected.forEach((option) => {
|
||||
if (selectedText !== '') {
|
||||
selectedText += ', ';
|
||||
}
|
||||
selectedText += option.text;
|
||||
selectedOptions.push(option);
|
||||
});
|
||||
break;
|
||||
}
|
||||
let selectedText;
|
||||
let selectedValue;
|
||||
if (dataSource === ViewTypes.DATA_SOURCE_USERS) {
|
||||
selectedText = displayUsername(selected, teammateNameDisplay);
|
||||
selectedValue = selected.id;
|
||||
} else if (dataSource === ViewTypes.DATA_SOURCE_CHANNELS) {
|
||||
selectedText = selected.display_name;
|
||||
selectedValue = selected.id;
|
||||
} else {
|
||||
selectedText = selected.text;
|
||||
selectedValue = selected.value;
|
||||
}
|
||||
|
||||
this.setState({selectedText});
|
||||
|
||||
if (this.props.onSelected) {
|
||||
(this.props.onSelected as (opt: DialogOption[]) => void)(selectedOptions);
|
||||
this.props.onSelected({text: selectedText, value: selectedValue});
|
||||
}
|
||||
};
|
||||
|
||||
goToSelectorScreen = preventDoubleTap(async () => {
|
||||
const closeButton = await CompassIcon.getImageSource(Platform.select({ios: 'arrow-back-ios', default: 'arrow-left'}), 24, this.props.theme.sidebarHeaderTextColor);
|
||||
|
||||
goToSelectorScreen = preventDoubleTap(() => {
|
||||
const {formatMessage} = this.context.intl;
|
||||
const {actions, dataSource, options, placeholder, getDynamicOptions, theme} = this.props;
|
||||
const {actions, dataSource, options, placeholder, getDynamicOptions} = this.props;
|
||||
const screen = 'SelectorScreen';
|
||||
const title = placeholder || formatMessage({id: 'mobile.action_menu.select', defaultMessage: 'Select an option'});
|
||||
const buttonName = formatMessage({id: 'mobile.forms.select.done', defaultMessage: 'Done'});
|
||||
|
||||
actions.setAutocompleteSelector(dataSource, this.handleSelect, options, getDynamicOptions);
|
||||
let screenOptions = {};
|
||||
if (this.props.isMultiselect) {
|
||||
screenOptions = {
|
||||
topBar: {
|
||||
leftButtons: [{
|
||||
id: 'close-dialog',
|
||||
icon: closeButton,
|
||||
}],
|
||||
rightButtons: [{
|
||||
id: 'submit-form',
|
||||
showAsAction: 'always',
|
||||
text: buttonName,
|
||||
}],
|
||||
leftButtonColor: theme.sidebarHeaderTextColor,
|
||||
rightButtonColor: theme.sidebarHeaderTextColor,
|
||||
},
|
||||
};
|
||||
}
|
||||
goToScreen(screen, title, {isMultiselect: this.props.isMultiselect, selected: this.state.selected}, screenOptions);
|
||||
goToScreen(screen, title);
|
||||
});
|
||||
|
||||
render() {
|
||||
@@ -236,8 +126,6 @@ export default class AutocompleteSelector extends PureComponent<Props, State> {
|
||||
const textStyles = getMarkdownTextStyles(theme);
|
||||
const blockStyles = getMarkdownBlockStyles(theme);
|
||||
|
||||
const chevron = Platform.select({ios: 'chevron-right', default: 'chevron-down'});
|
||||
|
||||
let text = placeholder || intl.formatMessage({id: 'mobile.action_menu.select', defaultMessage: 'Select an option'});
|
||||
let selectedStyle = style.dropdownPlaceholder;
|
||||
|
||||
@@ -327,8 +215,8 @@ export default class AutocompleteSelector extends PureComponent<Props, State> {
|
||||
{text}
|
||||
</Text>
|
||||
<CompassIcon
|
||||
name={chevron}
|
||||
color={changeOpacity(theme.centerChannelColor, 0.32)}
|
||||
name='chevron-down'
|
||||
color={changeOpacity(theme.centerChannelColor, 0.5)}
|
||||
style={style.icon}
|
||||
/>
|
||||
</View>
|
||||
@@ -340,7 +228,7 @@ export default class AutocompleteSelector extends PureComponent<Props, State> {
|
||||
}
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
const input = {
|
||||
borderWidth: 1,
|
||||
borderColor: changeOpacity(theme.centerChannelColor, 0.1),
|
||||
@@ -375,9 +263,8 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
},
|
||||
icon: {
|
||||
position: 'absolute',
|
||||
top: 6,
|
||||
top: 13,
|
||||
right: 12,
|
||||
fontSize: 28,
|
||||
},
|
||||
labelContainer: {
|
||||
flexDirection: 'row',
|
||||
@@ -2,29 +2,23 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
import {ActionCreatorsMapObject, bindActionCreators, Dispatch} from 'redux';
|
||||
import {bindActionCreators} from 'redux';
|
||||
|
||||
import {setAutocompleteSelector} from '@actions/views/post';
|
||||
import {getTeammateNameDisplaySetting, getTheme} from '@mm-redux/selectors/entities/preferences';
|
||||
|
||||
import AutocompleteSelector from './autocomplete_selector';
|
||||
|
||||
import type {Action, ActionResult, GenericAction} from '@mm-redux/types/actions';
|
||||
import type {GlobalState} from '@mm-redux/types/store';
|
||||
|
||||
function mapStateToProps(state: GlobalState) {
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
teammateNameDisplay: getTeammateNameDisplaySetting(state),
|
||||
theme: getTheme(state),
|
||||
};
|
||||
}
|
||||
|
||||
type Actions = {
|
||||
setAutocompleteSelector: (dataSource: any, onSelect: any, options: any, getDynamicOptions: any) => Promise<ActionResult>;
|
||||
}
|
||||
function mapDispatchToProps(dispatch: Dispatch<GenericAction>) {
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators<ActionCreatorsMapObject<Action>, Actions>({
|
||||
actions: bindActionCreators({
|
||||
setAutocompleteSelector,
|
||||
}, dispatch),
|
||||
};
|
||||
27
app/components/avatars/avatars.stories.tsx
Normal file
27
app/components/avatars/avatars.stories.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {withKnobs, number, select} from '@storybook/addon-knobs';
|
||||
import {storiesOf} from '@storybook/react-native';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import {Preferences} from '@mm-redux/constants';
|
||||
import {getProfiles} from '@mm-redux/selectors/entities/users';
|
||||
import {UserProfile} from '@mm-redux/types/users';
|
||||
import Store from '@store/store';
|
||||
|
||||
import Avatars from './avatars';
|
||||
|
||||
const state = Store.redux?.getState();
|
||||
const users = getProfiles(state, {});
|
||||
const userIds = users.map((user:UserProfile) => user.id);
|
||||
|
||||
storiesOf('Avatars', module).
|
||||
addDecorator(withKnobs).
|
||||
add('Avatars', () => (
|
||||
<Avatars
|
||||
userIds={userIds.slice(0, number(`number of named participants (max ${userIds.length})`, userIds.length))}
|
||||
theme={select('theme', Preferences.THEMES, Preferences.THEMES.denim)}
|
||||
/>
|
||||
));
|
||||
@@ -86,7 +86,6 @@ export interface AvatarsProps {
|
||||
breakAt?: number;
|
||||
style?: StyleProp<ViewStyle>;
|
||||
theme: Theme;
|
||||
listTitle?: JSX.Element;
|
||||
}
|
||||
|
||||
export default class Avatars extends PureComponent<AvatarsProps> {
|
||||
@@ -95,12 +94,11 @@ export default class Avatars extends PureComponent<AvatarsProps> {
|
||||
};
|
||||
|
||||
showParticipantsList = () => {
|
||||
const {userIds, listTitle} = this.props;
|
||||
const {userIds} = this.props;
|
||||
|
||||
const screen = 'ParticipantsList';
|
||||
const passProps = {
|
||||
userIds,
|
||||
listTitle,
|
||||
};
|
||||
|
||||
showModalOverCurrentContext(screen, passProps);
|
||||
|
||||
@@ -83,7 +83,7 @@ export default class ChannelLink extends React.PureComponent {
|
||||
this.props.onChannelLinkPress(channel);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
const channel = this.state.channel;
|
||||
|
||||
@@ -92,7 +92,7 @@ export default class ChannelLoader extends PureComponent {
|
||||
delay: 500,
|
||||
useNativeDriver: false,
|
||||
}).start();
|
||||
};
|
||||
}
|
||||
|
||||
buildSections({key, style, bg, color}) {
|
||||
return (
|
||||
@@ -128,7 +128,7 @@ export default class ChannelLoader extends PureComponent {
|
||||
const {height} = e.nativeEvent.layout;
|
||||
const maxRows = calculateMaxRows(height);
|
||||
this.setState({maxRows});
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
|
||||
@@ -25,6 +25,7 @@ exports[`CustomList should match snapshot with FlatList 1`] = `
|
||||
keyboardDismissMode="on-drag"
|
||||
keyboardShouldPersistTaps="always"
|
||||
maxToRenderPerBatch={16}
|
||||
numColumns={1}
|
||||
onLayout={[Function]}
|
||||
onScroll={[Function]}
|
||||
removeClippedSubviews={true}
|
||||
|
||||
@@ -52,35 +52,33 @@ export default class ChannelListRow extends React.PureComponent {
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={style.outerContainer}>
|
||||
<CustomListRow
|
||||
id={this.props.id}
|
||||
onPress={this.props.onPress ? this.onPress : null}
|
||||
enabled={this.props.enabled}
|
||||
selectable={this.props.selectable}
|
||||
selected={this.props.selected}
|
||||
testID={testID}
|
||||
<CustomListRow
|
||||
id={this.props.id}
|
||||
onPress={this.props.onPress ? this.onPress : null}
|
||||
enabled={this.props.enabled}
|
||||
selectable={this.props.selectable}
|
||||
selected={this.props.selected}
|
||||
testID={testID}
|
||||
>
|
||||
<View
|
||||
style={style.container}
|
||||
testID={itemTestID}
|
||||
>
|
||||
<View
|
||||
style={style.container}
|
||||
testID={itemTestID}
|
||||
>
|
||||
<View style={style.titleContainer}>
|
||||
<CompassIcon
|
||||
name={icon}
|
||||
style={style.icon}
|
||||
/>
|
||||
<Text
|
||||
style={style.displayName}
|
||||
testID={channelDisplayNameTestID}
|
||||
>
|
||||
{this.props.channel.display_name}
|
||||
</Text>
|
||||
</View>
|
||||
{purpose}
|
||||
<View style={style.titleContainer}>
|
||||
<CompassIcon
|
||||
name={icon}
|
||||
style={style.icon}
|
||||
/>
|
||||
<Text
|
||||
style={style.displayName}
|
||||
testID={channelDisplayNameTestID}
|
||||
>
|
||||
{this.props.channel.display_name}
|
||||
</Text>
|
||||
</View>
|
||||
</CustomListRow>
|
||||
</View>
|
||||
{purpose}
|
||||
</View>
|
||||
</CustomListRow>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -103,12 +101,7 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
container: {
|
||||
flex: 1,
|
||||
flexDirection: 'column',
|
||||
},
|
||||
outerContainer: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
paddingHorizontal: 15,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
purpose: {
|
||||
marginTop: 7,
|
||||
|
||||
@@ -42,23 +42,21 @@ export default class OptionListRow extends React.PureComponent {
|
||||
const style = getStyleFromTheme(theme);
|
||||
|
||||
return (
|
||||
<View style={style.container}>
|
||||
<CustomListRow
|
||||
id={value}
|
||||
onPress={this.onPress}
|
||||
enabled={enabled}
|
||||
selectable={selectable}
|
||||
selected={selected}
|
||||
>
|
||||
<View style={style.textContainer}>
|
||||
<View>
|
||||
<Text style={style.optionText}>
|
||||
{text}
|
||||
</Text>
|
||||
</View>
|
||||
<CustomListRow
|
||||
id={value}
|
||||
onPress={this.onPress}
|
||||
enabled={enabled}
|
||||
selectable={selectable}
|
||||
selected={selected}
|
||||
>
|
||||
<View style={style.textContainer}>
|
||||
<View>
|
||||
<Text style={style.optionText}>
|
||||
{text}
|
||||
</Text>
|
||||
</View>
|
||||
</CustomListRow>
|
||||
</View>
|
||||
</View>
|
||||
</CustomListRow>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@ exports[`UserListRow should match snapshot 1`] = `
|
||||
Object {
|
||||
"flex": 1,
|
||||
"flexDirection": "row",
|
||||
"marginHorizontal": 10,
|
||||
"overflow": "hidden",
|
||||
"paddingHorizontal": 15,
|
||||
}
|
||||
}
|
||||
>
|
||||
@@ -142,8 +142,8 @@ exports[`UserListRow should match snapshot for currentUser with (you) populated
|
||||
Object {
|
||||
"flex": 1,
|
||||
"flexDirection": "row",
|
||||
"marginHorizontal": 10,
|
||||
"overflow": "hidden",
|
||||
"paddingHorizontal": 15,
|
||||
}
|
||||
}
|
||||
>
|
||||
@@ -278,8 +278,8 @@ exports[`UserListRow should match snapshot for deactivated user 1`] = `
|
||||
Object {
|
||||
"flex": 1,
|
||||
"flexDirection": "row",
|
||||
"marginHorizontal": 10,
|
||||
"overflow": "hidden",
|
||||
"paddingHorizontal": 15,
|
||||
}
|
||||
}
|
||||
>
|
||||
@@ -427,8 +427,8 @@ exports[`UserListRow should match snapshot for guest user 1`] = `
|
||||
Object {
|
||||
"flex": 1,
|
||||
"flexDirection": "row",
|
||||
"marginHorizontal": 10,
|
||||
"overflow": "hidden",
|
||||
"paddingHorizontal": 15,
|
||||
}
|
||||
}
|
||||
>
|
||||
@@ -563,8 +563,8 @@ exports[`UserListRow should match snapshot for remote user 1`] = `
|
||||
Object {
|
||||
"flex": 1,
|
||||
"flexDirection": "row",
|
||||
"marginHorizontal": 10,
|
||||
"overflow": "hidden",
|
||||
"paddingHorizontal": 15,
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
@@ -165,7 +165,7 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
container: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
paddingHorizontal: 15,
|
||||
marginHorizontal: 10,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
profileContainer: {
|
||||
|
||||
@@ -74,7 +74,6 @@ exports[`EditChannelInfo should match snapshot 1`] = `
|
||||
}
|
||||
>
|
||||
<TextInputWithLocalizedPlaceholder
|
||||
allowFontScaling={true}
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
disableFullscreenUI={true}
|
||||
@@ -141,7 +140,6 @@ exports[`EditChannelInfo should match snapshot 1`] = `
|
||||
}
|
||||
>
|
||||
<TextInputWithLocalizedPlaceholder
|
||||
allowFontScaling={true}
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
blurOnSubmit={false}
|
||||
@@ -231,7 +229,6 @@ exports[`EditChannelInfo should match snapshot 1`] = `
|
||||
}
|
||||
>
|
||||
<TextInputWithLocalizedPlaceholder
|
||||
allowFontScaling={true}
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
blurOnSubmit={false}
|
||||
|
||||
@@ -163,7 +163,7 @@ export default class EditChannelInfo extends PureComponent {
|
||||
|
||||
onHeaderLayout = ({nativeEvent}) => {
|
||||
this.setState({headerPosition: nativeEvent.layout.y});
|
||||
};
|
||||
}
|
||||
|
||||
onKeyboardDidShow = () => {
|
||||
this.setState({keyboardVisible: true});
|
||||
@@ -172,15 +172,15 @@ export default class EditChannelInfo extends PureComponent {
|
||||
this.setState({headerHasFocus: false});
|
||||
this.scrollHeaderToTop();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
onKeyboardDidHide = () => {
|
||||
this.setState({keyboardVisible: false});
|
||||
};
|
||||
}
|
||||
|
||||
onKeyboardOffsetChanged = (keyboardPosition) => {
|
||||
this.setState({keyboardPosition});
|
||||
};
|
||||
}
|
||||
|
||||
onHeaderFocus = () => {
|
||||
if (this.state.keyboardVisible) {
|
||||
@@ -194,7 +194,7 @@ export default class EditChannelInfo extends PureComponent {
|
||||
if (this.scroll.current) {
|
||||
this.scroll.current.scrollTo({x: 0, y: this.state.headerPosition});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
@@ -279,7 +279,6 @@ export default class EditChannelInfo extends PureComponent {
|
||||
onPress={() => {
|
||||
this.onTypeSelect(General.OPEN_CHANNEL);
|
||||
}}
|
||||
testID='edit_channel_info.type.public.action'
|
||||
>
|
||||
<FormattedText
|
||||
style={style.touchableText}
|
||||
@@ -306,7 +305,6 @@ export default class EditChannelInfo extends PureComponent {
|
||||
onPress={() => {
|
||||
this.onTypeSelect(General.PRIVATE_CHANNEL);
|
||||
}}
|
||||
testID='edit_channel_info.type.private.action'
|
||||
>
|
||||
<FormattedText
|
||||
style={style.touchableText}
|
||||
@@ -336,7 +334,6 @@ export default class EditChannelInfo extends PureComponent {
|
||||
</View>
|
||||
<View style={style.inputContainer}>
|
||||
<TextInputWithLocalizedPlaceholder
|
||||
allowFontScaling={true}
|
||||
testID='edit_channel_info.name.input'
|
||||
ref={this.nameInput}
|
||||
value={displayName}
|
||||
@@ -367,7 +364,6 @@ export default class EditChannelInfo extends PureComponent {
|
||||
</View>
|
||||
<View style={style.inputContainer}>
|
||||
<TextInputWithLocalizedPlaceholder
|
||||
allowFontScaling={true}
|
||||
testID='edit_channel_info.purpose.input'
|
||||
ref={this.purposeInput}
|
||||
value={purpose}
|
||||
@@ -411,7 +407,6 @@ export default class EditChannelInfo extends PureComponent {
|
||||
</View>
|
||||
<View style={style.inputContainer}>
|
||||
<TextInputWithLocalizedPlaceholder
|
||||
allowFontScaling={true}
|
||||
testID='edit_channel_info.header.input'
|
||||
ref={this.headerInput}
|
||||
value={header}
|
||||
|
||||
@@ -109,7 +109,7 @@ export default class EmojiPicker extends PureComponent {
|
||||
|
||||
setSearchBarRef = (ref) => {
|
||||
this.searchBarRef = ref;
|
||||
};
|
||||
}
|
||||
|
||||
setSectionListRef = (ref) => {
|
||||
this.sectionListRef = ref;
|
||||
@@ -121,7 +121,7 @@ export default class EmojiPicker extends PureComponent {
|
||||
const emojis = this.renderableEmojis(this.props.emojisBySection, this.props.deviceWidth);
|
||||
this.setState({emojis});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
renderableEmojis = (emojis, deviceWidth) => {
|
||||
const numberOfColumns = this.getNumberOfColumns(deviceWidth);
|
||||
@@ -501,7 +501,7 @@ export default class EmojiPicker extends PureComponent {
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const getStyleSheetFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
|
||||
@@ -18,7 +18,7 @@ export default class EmojiPickerRow extends Component {
|
||||
emojiSize: PropTypes.number.isRequired,
|
||||
items: PropTypes.array.isRequired,
|
||||
onEmojiPress: PropTypes.func.isRequired,
|
||||
};
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps) {
|
||||
return !shallowEqual(this.props.items, nextProps.items);
|
||||
|
||||
@@ -62,7 +62,7 @@ export default class ErrorList extends PureComponent {
|
||||
clearErrors: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
errors: PropTypes.array.isRequired,
|
||||
};
|
||||
}
|
||||
|
||||
renderErrorsList() {
|
||||
const {errors} = this.props;
|
||||
|
||||
@@ -54,7 +54,7 @@ class FormattedMarkdownText extends React.PureComponent {
|
||||
|
||||
createParser = () => {
|
||||
return new Parser();
|
||||
};
|
||||
}
|
||||
|
||||
createRenderer = () => {
|
||||
return new Renderer({
|
||||
@@ -73,39 +73,39 @@ class FormattedMarkdownText extends React.PureComponent {
|
||||
atMention: this.renderAtMention,
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
computeTextStyle = (baseStyle, context) => {
|
||||
return concatStyles(baseStyle, context.map((type) => this.textStyles[type]));
|
||||
};
|
||||
}
|
||||
|
||||
renderText = ({context, literal}) => {
|
||||
const style = this.computeTextStyle(this.props.style || this.baseTextStyle, context);
|
||||
return <Text style={style}>{literal}</Text>;
|
||||
};
|
||||
}
|
||||
|
||||
renderCodeSpan = ({context, literal}) => {
|
||||
const style = this.computeTextStyle([this.baseTextStyle, this.textStyles.code], context);
|
||||
return <Text style={style}>{literal}</Text>;
|
||||
};
|
||||
}
|
||||
|
||||
renderLink = ({children, href}) => {
|
||||
const url = href[0] === TARGET_BLANK_URL_PREFIX ? href.substring(1, href.length) : href;
|
||||
return <MarkdownLink href={url}>{children}</MarkdownLink>;
|
||||
};
|
||||
}
|
||||
|
||||
renderBreak = () => {
|
||||
return <Text>{'\n'}</Text>;
|
||||
};
|
||||
}
|
||||
|
||||
renderParagraph = ({children}) => {
|
||||
return <Text>{children}</Text>;
|
||||
};
|
||||
}
|
||||
|
||||
renderHTML = (props) => {
|
||||
console.warn(`HTML used in FormattedMarkdownText component with id ${this.props.id}`); // eslint-disable-line no-console
|
||||
return this.renderText(props);
|
||||
};
|
||||
}
|
||||
|
||||
renderAtMention = ({context, mentionName}) => {
|
||||
const style = getStyleSheet(this.props.theme);
|
||||
@@ -118,7 +118,7 @@ class FormattedMarkdownText extends React.PureComponent {
|
||||
textStyle={[this.computeTextStyle(this.props.baseTextStyle, context), style.atMentionOpacity]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
const {id, defaultMessage, values, theme} = this.props;
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {shallow} from 'enzyme';
|
||||
import moment from 'moment';
|
||||
import React from 'react';
|
||||
|
||||
import FormattedRelativeTime from './formatted_relative_time';
|
||||
|
||||
jest.mock('react', () => ({
|
||||
...jest.requireActual('react'),
|
||||
useEffect: (f) => f(),
|
||||
}));
|
||||
|
||||
describe('FormattedRelativeTime', () => {
|
||||
const baseProps = {
|
||||
value: moment.now() - 15000,
|
||||
updateIntervalInSeconds: 10000,
|
||||
};
|
||||
|
||||
test('should match snapshot', () => {
|
||||
const wrapper = shallow(<FormattedRelativeTime {...baseProps}/>);
|
||||
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match string in the past', () => {
|
||||
const props = {...baseProps, value: moment.now() - ((10 * 60 * 60 * 1000) + (30 * 60 * 1000) + (25 * 1000) + 500)};
|
||||
const wrapper = shallow(<FormattedRelativeTime {...props}/>);
|
||||
|
||||
expect(wrapper.getElement().props.children).toBe('11 hours ago');
|
||||
});
|
||||
|
||||
test('should match string in the future', () => {
|
||||
const props = {...baseProps, value: moment.now() + 15500};
|
||||
const wrapper = shallow(<FormattedRelativeTime {...props}/>);
|
||||
|
||||
expect(wrapper.getElement().props.children).toBe('in a few seconds');
|
||||
});
|
||||
|
||||
test('should re-render after updateIntervalInSeconds', () => {
|
||||
jest.useFakeTimers();
|
||||
const props = {...baseProps, value: moment.now(), updateIntervalInSeconds: 120};
|
||||
const wrapper = shallow(<FormattedRelativeTime {...props}/>);
|
||||
expect(wrapper.getElement().props.children).toBe('a few seconds ago');
|
||||
jest.advanceTimersByTime(60000);
|
||||
expect(wrapper.getElement().props.children).toBe('a few seconds ago');
|
||||
jest.advanceTimersByTime(120000);
|
||||
expect(wrapper.getElement().props.children).toBe('2 minutes ago');
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
test('should not re-render if updateIntervalInSeconds is not passed', () => {
|
||||
jest.useFakeTimers();
|
||||
const props = {value: baseProps.value};
|
||||
const wrapper = shallow(<FormattedRelativeTime {...props}/>);
|
||||
expect(wrapper.getElement().props.children).toBe('a few seconds ago');
|
||||
jest.advanceTimersByTime(120000000000);
|
||||
expect(wrapper.getElement().props.children).toBe('a few seconds ago');
|
||||
jest.useRealTimers();
|
||||
});
|
||||
});
|
||||
@@ -1,44 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import moment from 'moment-timezone';
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import {Text, TextProps} from 'react-native';
|
||||
|
||||
import type {UserTimezone} from '@mm-redux/types/users';
|
||||
|
||||
type FormattedRelativeTimeProps = TextProps & {
|
||||
timezone?: UserTimezone | string;
|
||||
value: number | string | Date;
|
||||
updateIntervalInSeconds?: number;
|
||||
}
|
||||
|
||||
const FormattedRelativeTime = ({timezone, value, updateIntervalInSeconds, ...props}: FormattedRelativeTimeProps) => {
|
||||
const getFormattedRelativeTime = () => {
|
||||
let zone = timezone;
|
||||
if (typeof timezone === 'object') {
|
||||
zone = timezone.useAutomaticTimezone ? timezone.automaticTimezone : timezone.manualTimezone;
|
||||
}
|
||||
|
||||
return timezone ? moment.tz(value, zone as string).fromNow() : moment(value).fromNow();
|
||||
};
|
||||
|
||||
const [formattedTime, setFormattedTime] = useState(getFormattedRelativeTime());
|
||||
useEffect(() => {
|
||||
if (updateIntervalInSeconds) {
|
||||
const interval = setInterval(() => setFormattedTime(getFormattedRelativeTime()), updateIntervalInSeconds * 1000);
|
||||
return () => {
|
||||
clearInterval(interval);
|
||||
};
|
||||
}
|
||||
return () => null;
|
||||
}, [updateIntervalInSeconds]);
|
||||
|
||||
return (
|
||||
<Text {...props}>
|
||||
{formattedTime}
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
|
||||
export default FormattedRelativeTime;
|
||||
@@ -5,14 +5,9 @@ import React from 'react';
|
||||
import {injectIntl, intlShape} from 'react-intl';
|
||||
import {Alert, FlatList} from 'react-native';
|
||||
|
||||
import {goToScreen} from '@actions/navigation';
|
||||
import {THREAD} from '@constants/screen';
|
||||
import EventEmitter from '@mm-redux/utils/event_emitter';
|
||||
|
||||
import ThreadList from './thread_list';
|
||||
|
||||
import type {ActionResult} from '@mm-redux/types/actions';
|
||||
import type {Post} from '@mm-redux/types/posts';
|
||||
import type {Team} from '@mm-redux/types/teams';
|
||||
import type {Theme} from '@mm-redux/types/theme';
|
||||
import type {ThreadsState, UserThread} from '@mm-redux/types/threads';
|
||||
@@ -21,19 +16,17 @@ import type {$ID} from '@mm-redux/types/utilities';
|
||||
|
||||
type Props = {
|
||||
actions: {
|
||||
getPostThread: (postId: string) => void;
|
||||
getThreads: (userId: $ID<UserProfile>, teamId: $ID<Team>, before?: $ID<UserThread>, after?: $ID<UserThread>, perPage?: number, deleted?: boolean, unread?: boolean) => Promise<ActionResult>;
|
||||
handleViewingGlobalThreadsAll: () => void;
|
||||
handleViewingGlobalThreadsUnreads: () => void;
|
||||
markAllThreadsInTeamRead: (userId: $ID<UserProfile>, teamId: $ID<Team>) => void;
|
||||
selectPost: (postId: string) => void;
|
||||
};
|
||||
allThreadIds: Array<$ID<UserThread>>;
|
||||
allThreadIds: $ID<UserThread>[];
|
||||
intl: typeof intlShape;
|
||||
teamId: $ID<Team>;
|
||||
theme: Theme;
|
||||
threadCount: ThreadsState['counts'][$ID<Team>];
|
||||
unreadThreadIds: Array<$ID<UserThread>>;
|
||||
unreadThreadIds: $ID<UserThread>[];
|
||||
userId: $ID<UserProfile>;
|
||||
viewingUnreads: boolean;
|
||||
}
|
||||
@@ -45,7 +38,6 @@ function GlobalThreadsList({actions, allThreadIds, intl, teamId, theme, threadCo
|
||||
const listRef = React.useRef<FlatList>(null);
|
||||
|
||||
const [isLoading, setIsLoading] = React.useState<boolean>(true);
|
||||
const [isRefreshing, setIsRefreshing] = React.useState<boolean>(false);
|
||||
|
||||
const scrollToTop = () => {
|
||||
listRef.current?.scrollToOffset({offset: 0});
|
||||
@@ -87,16 +79,6 @@ function GlobalThreadsList({actions, allThreadIds, intl, teamId, theme, threadCo
|
||||
}
|
||||
};
|
||||
|
||||
const onRefresh = async () => {
|
||||
if (!isLoading) {
|
||||
if (!isRefreshing) {
|
||||
setIsRefreshing(true);
|
||||
}
|
||||
await loadThreads('', '', viewingUnreads);
|
||||
setIsRefreshing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const markAllAsRead = () => {
|
||||
Alert.alert(
|
||||
intl.formatMessage({
|
||||
@@ -126,32 +108,13 @@ function GlobalThreadsList({actions, allThreadIds, intl, teamId, theme, threadCo
|
||||
);
|
||||
};
|
||||
|
||||
const goToThread = React.useCallback((post: Post) => {
|
||||
actions.getPostThread(post.id);
|
||||
actions.selectPost(post.id);
|
||||
const passProps = {
|
||||
channelId: post.channel_id,
|
||||
rootId: post.id,
|
||||
};
|
||||
goToScreen(THREAD, '', passProps);
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
EventEmitter.on('goToThread', goToThread);
|
||||
return () => {
|
||||
EventEmitter.off('goToThread', goToThread);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ThreadList
|
||||
haveUnreads={haveUnreads}
|
||||
isLoading={isLoading}
|
||||
isRefreshing={isRefreshing}
|
||||
listRef={listRef}
|
||||
loadMoreThreads={loadMoreThreads}
|
||||
markAllAsRead={markAllAsRead}
|
||||
onRefresh={onRefresh}
|
||||
testID={'global_threads'}
|
||||
theme={theme}
|
||||
threadIds={ids}
|
||||
|
||||
@@ -4,9 +4,7 @@
|
||||
import {connect} from 'react-redux';
|
||||
import {bindActionCreators, Dispatch} from 'redux';
|
||||
|
||||
import {getPostThread} from '@actions/views/post';
|
||||
import {handleViewingGlobalThreadsAll, handleViewingGlobalThreadsUnreads} from '@actions/views/threads';
|
||||
import {selectPost} from '@mm-redux/actions/posts';
|
||||
import {getThreads, markAllThreadsInTeamRead} from '@mm-redux/actions/threads';
|
||||
import {getCurrentUserId} from '@mm-redux/selectors/entities/common';
|
||||
import {getTheme} from '@mm-redux/selectors/entities/preferences';
|
||||
@@ -34,12 +32,10 @@ function mapStateToProps(state: GlobalState) {
|
||||
function mapDispatchToProps(dispatch: Dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators({
|
||||
getPostThread,
|
||||
getThreads,
|
||||
handleViewingGlobalThreadsAll,
|
||||
handleViewingGlobalThreadsUnreads,
|
||||
markAllThreadsInTeamRead,
|
||||
selectPost,
|
||||
}, dispatch),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
exports[`Global Thread Item Should render thread item with unread messages dot 1`] = `
|
||||
<TouchableHighlight
|
||||
onLongPress={[Function]}
|
||||
onPress={[Function]}
|
||||
testID="thread_item.post1.item"
|
||||
underlayColor="rgba(28,88,217,0.08)"
|
||||
@@ -186,7 +185,6 @@ exports[`Global Thread Item Should render thread item with unread messages dot 1
|
||||
|
||||
exports[`Global Thread Item Should show unread mentions count 1`] = `
|
||||
<TouchableHighlight
|
||||
onLongPress={[Function]}
|
||||
onPress={[Function]}
|
||||
testID="thread_item.post1.item"
|
||||
underlayColor="rgba(28,88,217,0.08)"
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
import {connect} from 'react-redux';
|
||||
import {bindActionCreators, Dispatch} from 'redux';
|
||||
|
||||
import {getPost} from '@actions/views/post';
|
||||
import {getPost, getPostThread} from '@actions/views/post';
|
||||
import {selectPost} from '@mm-redux/actions/posts';
|
||||
import {getChannel} from '@mm-redux/selectors/entities/channels';
|
||||
import {getPost as getPostSelector} from '@mm-redux/selectors/entities/posts';
|
||||
import {getThread} from '@mm-redux/selectors/entities/threads';
|
||||
@@ -29,6 +30,8 @@ function mapDispatchToProps(dispatch: Dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators({
|
||||
getPost,
|
||||
getPostThread,
|
||||
selectPost,
|
||||
}, dispatch),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -5,12 +5,13 @@ import {shallow} from 'enzyme';
|
||||
import React from 'react';
|
||||
import {Text} from 'react-native';
|
||||
|
||||
import * as navigationActions from '@actions/navigation';
|
||||
import {THREAD} from '@constants/screen';
|
||||
import {Preferences} from '@mm-redux/constants';
|
||||
import {Channel} from '@mm-redux/types/channels';
|
||||
import {Post} from '@mm-redux/types/posts';
|
||||
import {UserThread} from '@mm-redux/types/threads';
|
||||
import {UserProfile} from '@mm-redux/types/users';
|
||||
import EventEmitter from '@mm-redux/utils/event_emitter';
|
||||
import {intl} from '@test/intl-test-helper';
|
||||
|
||||
import {ThreadItem} from './thread_item';
|
||||
@@ -98,7 +99,7 @@ describe('Global Thread Item', () => {
|
||||
});
|
||||
|
||||
test('Should goto threads when pressed on thread item', () => {
|
||||
EventEmitter.emit = jest.fn();
|
||||
const goToScreen = jest.spyOn(navigationActions, 'goToScreen');
|
||||
const wrapper = shallow(
|
||||
<ThreadItem
|
||||
{...baseProps}
|
||||
@@ -107,6 +108,6 @@ describe('Global Thread Item', () => {
|
||||
const threadItem = wrapper.find({testID: `${testIDPrefix}.item`});
|
||||
expect(threadItem.exists()).toBeTruthy();
|
||||
threadItem.simulate('press');
|
||||
expect(EventEmitter.emit).toHaveBeenCalledWith('goToThread', expect.anything());
|
||||
expect(goToScreen).toHaveBeenCalledWith(THREAD, expect.anything(), expect.anything());
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,28 +3,29 @@
|
||||
|
||||
import React from 'react';
|
||||
import {injectIntl, intlShape} from 'react-intl';
|
||||
import {Keyboard, Text, TouchableHighlight, View} from 'react-native';
|
||||
import {View, Text, TouchableHighlight} from 'react-native';
|
||||
|
||||
import {showModalOverCurrentContext} from '@actions/navigation';
|
||||
import {goToScreen} from '@actions/navigation';
|
||||
import FriendlyDate from '@components/friendly_date';
|
||||
import RemoveMarkdown from '@components/remove_markdown';
|
||||
import {GLOBAL_THREADS} from '@constants/screen';
|
||||
import {GLOBAL_THREADS, THREAD} from '@constants/screen';
|
||||
import {Posts, Preferences} from '@mm-redux/constants';
|
||||
import EventEmitter from '@mm-redux/utils/event_emitter';
|
||||
import {Channel} from '@mm-redux/types/channels';
|
||||
import {Post} from '@mm-redux/types/posts';
|
||||
import {UserThread} from '@mm-redux/types/threads';
|
||||
import {UserProfile} from '@mm-redux/types/users';
|
||||
import {displayUsername} from '@mm-redux/utils/user_utils';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
import ThreadFooter from '../thread_footer';
|
||||
|
||||
import type {Channel} from '@mm-redux/types/channels';
|
||||
import type {Post} from '@mm-redux/types/posts';
|
||||
import type {Theme} from '@mm-redux/types/theme';
|
||||
import type {UserThread} from '@mm-redux/types/threads';
|
||||
import type {UserProfile} from '@mm-redux/types/users';
|
||||
|
||||
export type DispatchProps = {
|
||||
actions: {
|
||||
getPost: (postId: string) => void;
|
||||
getPostThread: (postId: string) => void;
|
||||
selectPost: (postId: string) => void;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -65,18 +66,13 @@ function ThreadItem({actions, channel, intl, post, threadId, testID, theme, thre
|
||||
const threadStarterName = displayUsername(threadStarter, Preferences.DISPLAY_PREFER_FULL_NAME);
|
||||
|
||||
const showThread = () => {
|
||||
EventEmitter.emit('goToThread', postItem);
|
||||
};
|
||||
|
||||
const showThreadOptions = () => {
|
||||
const screen = 'GlobalThreadOptions';
|
||||
actions.getPostThread(postItem.id);
|
||||
actions.selectPost(postItem.id);
|
||||
const passProps = {
|
||||
rootId: post.id,
|
||||
channelId: postItem.channel_id,
|
||||
rootId: postItem.id,
|
||||
};
|
||||
Keyboard.dismiss();
|
||||
requestAnimationFrame(() => {
|
||||
showModalOverCurrentContext(screen, passProps);
|
||||
});
|
||||
goToScreen(THREAD, '', passProps);
|
||||
};
|
||||
|
||||
const testIDPrefix = `${testID}.${postItem?.id}`;
|
||||
@@ -138,7 +134,6 @@ function ThreadItem({actions, channel, intl, post, threadId, testID, theme, thre
|
||||
return (
|
||||
<TouchableHighlight
|
||||
underlayColor={changeOpacity(theme.buttonBg, 0.08)}
|
||||
onLongPress={showThreadOptions}
|
||||
onPress={showThread}
|
||||
testID={`${testIDPrefix}.item`}
|
||||
>
|
||||
|
||||
@@ -86,98 +86,62 @@ exports[`Global Thread List Should render threads with functional tabs & mark al
|
||||
viewUnreadThreads={[MockFunction]}
|
||||
viewingUnreads={true}
|
||||
/>
|
||||
<PostListRefreshControl
|
||||
enabled={true}
|
||||
isInverted={false}
|
||||
onRefresh={[MockFunction]}
|
||||
refreshing={false}
|
||||
theme={
|
||||
<FlatList
|
||||
ListEmptyComponent={
|
||||
<EmptyState
|
||||
intl={
|
||||
Object {
|
||||
"defaultFormats": Object {},
|
||||
"defaultLocale": "en",
|
||||
"formatDate": [Function],
|
||||
"formatHTMLMessage": [Function],
|
||||
"formatMessage": [Function],
|
||||
"formatNumber": [Function],
|
||||
"formatPlural": [Function],
|
||||
"formatRelative": [Function],
|
||||
"formatTime": [Function],
|
||||
"formats": Object {},
|
||||
"formatters": Object {
|
||||
"getDateTimeFormat": [Function],
|
||||
"getMessageFormat": [Function],
|
||||
"getNumberFormat": [Function],
|
||||
"getPluralFormat": [Function],
|
||||
"getRelativeFormat": [Function],
|
||||
},
|
||||
"locale": "en",
|
||||
"messages": Object {},
|
||||
"now": [Function],
|
||||
"onError": [Function],
|
||||
"textComponent": "span",
|
||||
"timeZone": null,
|
||||
}
|
||||
}
|
||||
isUnreads={true}
|
||||
/>
|
||||
}
|
||||
ListFooterComponent={null}
|
||||
contentContainerStyle={
|
||||
Object {
|
||||
"awayIndicator": "#ffbc1f",
|
||||
"buttonBg": "#1c58d9",
|
||||
"buttonColor": "#ffffff",
|
||||
"centerChannelBg": "#ffffff",
|
||||
"centerChannelColor": "#3f4350",
|
||||
"codeTheme": "github",
|
||||
"dndIndicator": "#d24b4e",
|
||||
"errorTextColor": "#d24b4e",
|
||||
"linkColor": "#386fe5",
|
||||
"mentionBg": "#ffffff",
|
||||
"mentionColor": "#1e325c",
|
||||
"mentionHighlightBg": "#ffd470",
|
||||
"mentionHighlightLink": "#1b1d22",
|
||||
"newMessageSeparator": "#cc8f00",
|
||||
"onlineIndicator": "#3db887",
|
||||
"sidebarBg": "#1e325c",
|
||||
"sidebarHeaderBg": "#192a4d",
|
||||
"sidebarHeaderTextColor": "#ffffff",
|
||||
"sidebarTeamBarBg": "#14213e",
|
||||
"sidebarText": "#ffffff",
|
||||
"sidebarTextActiveBorder": "#5d89ea",
|
||||
"sidebarTextActiveColor": "#ffffff",
|
||||
"sidebarTextHoverBg": "#28427b",
|
||||
"sidebarUnreadText": "#ffffff",
|
||||
"type": "Denim",
|
||||
"flexGrow": 1,
|
||||
}
|
||||
}
|
||||
>
|
||||
<FlatList
|
||||
ListEmptyComponent={
|
||||
<EmptyState
|
||||
intl={
|
||||
Object {
|
||||
"defaultFormats": Object {},
|
||||
"defaultLocale": "en",
|
||||
"formatDate": [Function],
|
||||
"formatHTMLMessage": [Function],
|
||||
"formatMessage": [Function],
|
||||
"formatNumber": [Function],
|
||||
"formatPlural": [Function],
|
||||
"formatRelative": [Function],
|
||||
"formatTime": [Function],
|
||||
"formats": Object {},
|
||||
"formatters": Object {
|
||||
"getDateTimeFormat": [Function],
|
||||
"getMessageFormat": [Function],
|
||||
"getNumberFormat": [Function],
|
||||
"getPluralFormat": [Function],
|
||||
"getRelativeFormat": [Function],
|
||||
},
|
||||
"locale": "en",
|
||||
"messages": Object {},
|
||||
"now": [Function],
|
||||
"onError": [Function],
|
||||
"textComponent": "span",
|
||||
"timeZone": null,
|
||||
}
|
||||
}
|
||||
isUnreads={true}
|
||||
/>
|
||||
data={
|
||||
Array [
|
||||
"thread1",
|
||||
]
|
||||
}
|
||||
initialNumToRender={10}
|
||||
keyExtractor={[Function]}
|
||||
numColumns={1}
|
||||
onEndReached={[Function]}
|
||||
onEndReachedThreshold={2}
|
||||
removeClippedSubviews={true}
|
||||
renderItem={[Function]}
|
||||
scrollIndicatorInsets={
|
||||
Object {
|
||||
"right": 1,
|
||||
}
|
||||
ListFooterComponent={null}
|
||||
contentContainerStyle={
|
||||
Object {
|
||||
"flexGrow": 1,
|
||||
}
|
||||
}
|
||||
data={
|
||||
Array [
|
||||
"thread1",
|
||||
]
|
||||
}
|
||||
initialNumToRender={10}
|
||||
keyExtractor={[Function]}
|
||||
onEndReached={[Function]}
|
||||
onEndReachedThreshold={2}
|
||||
onScroll={[Function]}
|
||||
removeClippedSubviews={true}
|
||||
renderItem={[Function]}
|
||||
scrollIndicatorInsets={
|
||||
Object {
|
||||
"right": 1,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</PostListRefreshControl>
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
`;
|
||||
|
||||
@@ -25,11 +25,9 @@ describe('Global Thread List', () => {
|
||||
haveUnreads: true,
|
||||
intl,
|
||||
isLoading: false,
|
||||
isRefreshing: false,
|
||||
listRef: React.useRef<FlatList>(null),
|
||||
loadMoreThreads: jest.fn(),
|
||||
markAllAsRead,
|
||||
onRefresh: jest.fn(),
|
||||
testID,
|
||||
theme: Preferences.THEMES.denim,
|
||||
threadIds: ['thread1'],
|
||||
|
||||
@@ -2,13 +2,12 @@
|
||||
// See LICENSE.txt for license information.
|
||||
import React from 'react';
|
||||
import {injectIntl, intlShape} from 'react-intl';
|
||||
import {FlatList, NativeSyntheticEvent, NativeScrollEvent, Platform, View} from 'react-native';
|
||||
import {FlatList, Platform, View} from 'react-native';
|
||||
|
||||
import EmptyState from '@components/global_threads/empty_state';
|
||||
import ThreadItem from '@components/global_threads/thread_item';
|
||||
import Loading from '@components/loading';
|
||||
import {INITIAL_BATCH_TO_RENDER} from '@components/post_list/post_list_config';
|
||||
import CustomRefreshControl from '@components/post_list/post_list_refresh_control';
|
||||
import {ViewTypes} from '@constants';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
@@ -22,24 +21,20 @@ export type Props = {
|
||||
haveUnreads: boolean;
|
||||
intl: typeof intlShape;
|
||||
isLoading: boolean;
|
||||
isRefreshing: boolean;
|
||||
loadMoreThreads: () => Promise<void>;
|
||||
listRef: React.RefObject<FlatList>;
|
||||
markAllAsRead: () => void;
|
||||
onRefresh: () => void;
|
||||
testID: string;
|
||||
theme: Theme;
|
||||
threadIds: Array<$ID<UserThread>>;
|
||||
threadIds: $ID<UserThread>[];
|
||||
viewAllThreads: () => void;
|
||||
viewUnreadThreads: () => void;
|
||||
viewingUnreads: boolean;
|
||||
};
|
||||
|
||||
function ThreadList({haveUnreads, intl, isLoading, isRefreshing, loadMoreThreads, listRef, markAllAsRead, onRefresh, testID, theme, threadIds, viewAllThreads, viewUnreadThreads, viewingUnreads}: Props) {
|
||||
function ThreadList({haveUnreads, intl, isLoading, loadMoreThreads, listRef, markAllAsRead, testID, theme, threadIds, viewAllThreads, viewUnreadThreads, viewingUnreads}: Props) {
|
||||
const style = getStyleSheet(theme);
|
||||
|
||||
const [offsetY, setOffsetY] = React.useState(0);
|
||||
|
||||
const handleEndReached = React.useCallback(() => {
|
||||
loadMoreThreads();
|
||||
}, [loadMoreThreads, viewingUnreads]);
|
||||
@@ -56,17 +51,6 @@ function ThreadList({haveUnreads, intl, isLoading, isRefreshing, loadMoreThreads
|
||||
);
|
||||
}, [theme]);
|
||||
|
||||
const onScroll = React.useCallback((event: NativeSyntheticEvent<NativeScrollEvent>) => {
|
||||
if (Platform.OS === 'android') {
|
||||
const {y} = event.nativeEvent.contentOffset;
|
||||
if (y === 0) {
|
||||
setOffsetY(y);
|
||||
} else if (offsetY === 0 && y !== 0) {
|
||||
setOffsetY(y);
|
||||
}
|
||||
}
|
||||
}, [offsetY]);
|
||||
|
||||
const renderHeader = () => {
|
||||
if (!viewingUnreads && !threadIds.length) {
|
||||
return null;
|
||||
@@ -112,30 +96,21 @@ function ThreadList({haveUnreads, intl, isLoading, isRefreshing, loadMoreThreads
|
||||
return (
|
||||
<View style={style.container}>
|
||||
{renderHeader()}
|
||||
<CustomRefreshControl
|
||||
enabled={offsetY === 0}
|
||||
isInverted={false}
|
||||
refreshing={isRefreshing}
|
||||
onRefresh={onRefresh}
|
||||
theme={theme}
|
||||
>
|
||||
<FlatList
|
||||
contentContainerStyle={style.messagesContainer}
|
||||
data={threadIds}
|
||||
keyExtractor={keyExtractor}
|
||||
ListEmptyComponent={renderEmptyList()}
|
||||
ListFooterComponent={renderFooter()}
|
||||
onEndReached={handleEndReached}
|
||||
onEndReachedThreshold={2}
|
||||
onScroll={onScroll}
|
||||
ref={listRef}
|
||||
renderItem={renderPost}
|
||||
initialNumToRender={INITIAL_BATCH_TO_RENDER}
|
||||
maxToRenderPerBatch={Platform.select({android: 5})}
|
||||
removeClippedSubviews={true}
|
||||
scrollIndicatorInsets={style.listScrollIndicator}
|
||||
/>
|
||||
</CustomRefreshControl>
|
||||
<FlatList
|
||||
contentContainerStyle={style.messagesContainer}
|
||||
data={threadIds}
|
||||
keyExtractor={keyExtractor}
|
||||
ListEmptyComponent={renderEmptyList()}
|
||||
ListFooterComponent={renderFooter()}
|
||||
onEndReached={handleEndReached}
|
||||
onEndReachedThreshold={2}
|
||||
ref={listRef}
|
||||
renderItem={renderPost}
|
||||
initialNumToRender={INITIAL_BATCH_TO_RENDER}
|
||||
maxToRenderPerBatch={Platform.select({android: 5})}
|
||||
removeClippedSubviews={true}
|
||||
scrollIndicatorInsets={style.listScrollIndicator}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import AsyncStorage from '@react-native-community/async-storage';
|
||||
|
||||
import {PureComponent} from 'react';
|
||||
import {Dimensions, EmitterSubscription} from 'react-native';
|
||||
|
||||
@@ -92,13 +92,13 @@ export default class InteractiveDialogController extends PureComponent {
|
||||
};
|
||||
|
||||
showModal('InteractiveDialog', dialog.title, null, options);
|
||||
};
|
||||
}
|
||||
|
||||
handleCancel = (dialog, url) => {
|
||||
if (dialog.notify_on_cancel) {
|
||||
this.props.actions.submitInteractiveDialog({...dialog, url, cancelled: true});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
return null;
|
||||
|
||||
18
app/components/loading.stories.tsx
Normal file
18
app/components/loading.stories.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {withKnobs, select} from '@storybook/addon-knobs';
|
||||
import {storiesOf} from '@storybook/react-native';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import Loading from './loading';
|
||||
|
||||
storiesOf('Loading Icon Story', module).
|
||||
addDecorator(withKnobs).
|
||||
add('loading icon', () => (
|
||||
<Loading
|
||||
size={select('size', {Large: 'large', Small: 'small'}, 'large')}
|
||||
color={select('color', {Red: 'red', Blue: 'blue', Yellow: 'yellow', Black: 'black'}, 'red')}
|
||||
/>
|
||||
));
|
||||
@@ -29,7 +29,7 @@ export default class MarkdownEmoji extends PureComponent {
|
||||
|
||||
createParser = () => {
|
||||
return new Parser();
|
||||
};
|
||||
}
|
||||
|
||||
createRenderer = () => {
|
||||
return new Renderer({
|
||||
@@ -80,7 +80,7 @@ export default class MarkdownEmoji extends PureComponent {
|
||||
renderNewLine = ({context}) => {
|
||||
const style = this.computeTextStyle(this.props.baseTextStyle, context);
|
||||
return <Text style={style}>{'\n'}</Text>;
|
||||
};
|
||||
}
|
||||
|
||||
renderEditedIndicator = ({context}) => {
|
||||
let spacer = '';
|
||||
|
||||
@@ -4,14 +4,12 @@
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {getCurrentUrl} from '@mm-redux/selectors/entities/general';
|
||||
import {getTheme} from '@mm-redux/selectors/entities/preferences';
|
||||
|
||||
import MarkdownImage from './markdown_image';
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
serverURL: getCurrentUrl(state),
|
||||
theme: getTheme(state),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
Text,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import {SvgUri} from 'react-native-svg';
|
||||
import parseUrl from 'url-parse';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
@@ -42,7 +41,6 @@ export default class MarkdownImage extends ImageViewPort {
|
||||
linkDestination: PropTypes.string,
|
||||
postId: PropTypes.string,
|
||||
source: PropTypes.string.isRequired,
|
||||
theme: PropTypes.object,
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
@@ -52,19 +50,18 @@ export default class MarkdownImage extends ImageViewPort {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
const metadata = props.imagesMetadata?.[props.source] || Object.values(props.imagesMetadata || {})?.[0];
|
||||
const metadata = props.imagesMetadata?.[props.source] || Object.values(props.imagesMetadata || {})[0];
|
||||
this.fileId = generateId();
|
||||
this.state = {
|
||||
originalHeight: metadata?.height || 0,
|
||||
originalWidth: metadata?.width || 0,
|
||||
failed: isGifTooLarge(metadata),
|
||||
format: metadata?.format,
|
||||
uri: null,
|
||||
};
|
||||
}
|
||||
|
||||
getFileInfo = () => {
|
||||
const {format, originalHeight, originalWidth} = this.state;
|
||||
const {originalHeight, originalWidth} = this.state;
|
||||
const link = decodeURIComponent(this.getSource());
|
||||
let filename = parseUrl(link.substr(link.lastIndexOf('/'))).pathname.replace('/', '');
|
||||
let extension = filename.split('.').pop();
|
||||
@@ -79,7 +76,6 @@ export default class MarkdownImage extends ImageViewPort {
|
||||
id: this.fileId,
|
||||
name: filename,
|
||||
extension,
|
||||
format,
|
||||
has_preview_image: true,
|
||||
post_id: this.props.postId,
|
||||
uri: link,
|
||||
@@ -179,14 +175,13 @@ export default class MarkdownImage extends ImageViewPort {
|
||||
render() {
|
||||
let image = null;
|
||||
const fileInfo = this.getFileInfo();
|
||||
const {height, width} = calculateDimensions(fileInfo?.height, fileInfo?.width, getViewPortWidth(this.props.isReplyPost, this.hasPermanentSidebar()));
|
||||
const {height, width} = calculateDimensions(fileInfo.height, fileInfo.width, getViewPortWidth(this.props.isReplyPost, this.hasPermanentSidebar()));
|
||||
|
||||
if (this.state.failed) {
|
||||
image = (
|
||||
<CompassIcon
|
||||
name='file-image-broken-outline-large'
|
||||
name='jumbo-attachment-image-broken'
|
||||
size={24}
|
||||
color={this.props.theme?.centerChannelColor}
|
||||
/>
|
||||
);
|
||||
} else if (width && height) {
|
||||
@@ -209,7 +204,11 @@ export default class MarkdownImage extends ImageViewPort {
|
||||
);
|
||||
} else {
|
||||
// React Native complains if we try to pass resizeMode as a style
|
||||
const source = fileInfo.uri ? {uri: fileInfo.uri} : null;
|
||||
let source = null;
|
||||
if (fileInfo.uri) {
|
||||
source = {uri: fileInfo.uri};
|
||||
}
|
||||
|
||||
image = (
|
||||
<TouchableWithFeedback
|
||||
onLongPress={this.handleLinkLongPress}
|
||||
@@ -225,14 +224,6 @@ export default class MarkdownImage extends ImageViewPort {
|
||||
</TouchableWithFeedback>
|
||||
);
|
||||
}
|
||||
} else if (fileInfo?.format === 'svg') {
|
||||
image = (
|
||||
<SvgUri
|
||||
uri={fileInfo.uri}
|
||||
style={{flex: 1}}
|
||||
onError={this.handleSizeFailed}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (image && this.props.linkDestination) {
|
||||
|
||||
@@ -9,7 +9,7 @@ import {intlShape} from 'react-intl';
|
||||
import {Alert, Text} from 'react-native';
|
||||
import urlParse from 'url-parse';
|
||||
|
||||
import {dismissAllModals, popToRoot, showModal} from '@actions/navigation';
|
||||
import {dismissAllModals, popToRoot} from '@actions/navigation';
|
||||
import Config from '@assets/config';
|
||||
import {DeepLinkTypes} from '@constants';
|
||||
import {getCurrentServerUrl} from '@init/credentials';
|
||||
@@ -59,20 +59,13 @@ export default class MarkdownLink extends PureComponent {
|
||||
const match = matchDeepLink(url, serverURL, siteURL);
|
||||
|
||||
if (match) {
|
||||
switch (match.type) {
|
||||
case DeepLinkTypes.CHANNEL:
|
||||
if (match.type === DeepLinkTypes.CHANNEL) {
|
||||
await handleSelectChannelByName(match.channelName, match.teamName, errorBadChannel, intl);
|
||||
await dismissAllModals();
|
||||
await popToRoot();
|
||||
break;
|
||||
case DeepLinkTypes.PERMALINK: {
|
||||
} else if (match.type === DeepLinkTypes.PERMALINK) {
|
||||
const teamName = match.teamName === PERMALINK_GENERIC_TEAM_NAME_REDIRECT ? currentTeamName : match.teamName;
|
||||
showPermalink(intl, teamName, match.postId);
|
||||
break;
|
||||
}
|
||||
case DeepLinkTypes.PLUGIN:
|
||||
showModal('PluginInternal', match.id, {link: url});
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
const onError = () => {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user