Merge branch 'develop' into single-server
# Conflicts: # android/app/build.gradle # android/app/src/play/google-services.json # app/lib/database/index.js # app/sagas/init.js # app/sagas/login.js # ios/Pods/Manifest.lock # ios/Pods/Pods.xcodeproj/project.pbxproj # ios/Pods/Target Support Files/MMKV/MMKV-prefix.pch # ios/Pods/Target Support Files/MMKV/MMKV.debug.xcconfig # ios/Pods/Target Support Files/MMKV/MMKV.release.xcconfig # ios/Pods/Target Support Files/MMKVCore/MMKVCore.debug.xcconfig # ios/Pods/Target Support Files/MMKVCore/MMKVCore.release.xcconfig # ios/Pods/Target Support Files/Pods-RocketChatRN/Pods-RocketChatRN-acknowledgements.markdown # ios/Pods/Target Support Files/Pods-RocketChatRN/Pods-RocketChatRN-acknowledgements.plist # ios/Pods/Target Support Files/Pods-RocketChatRN/Pods-RocketChatRN.debug.xcconfig # ios/Pods/Target Support Files/Pods-RocketChatRN/Pods-RocketChatRN.release.xcconfig # ios/Pods/Target Support Files/Pods-ShareRocketChatRN/Pods-ShareRocketChatRN-acknowledgements.markdown # ios/Pods/Target Support Files/Pods-ShareRocketChatRN/Pods-ShareRocketChatRN-acknowledgements.plist # ios/Pods/Target Support Files/Pods-ShareRocketChatRN/Pods-ShareRocketChatRN.debug.xcconfig # ios/Pods/Target Support Files/Pods-ShareRocketChatRN/Pods-ShareRocketChatRN.release.xcconfig # ios/Pods/Target Support Files/RNConfigReader/RNConfigReader.debug.xcconfig # ios/Pods/Target Support Files/RNConfigReader/RNConfigReader.release.xcconfig # ios/Pods/Target Support Files/react-native-cookies/react-native-cookies.debug.xcconfig # ios/Pods/Target Support Files/react-native-simple-crypto/react-native-simple-crypto.debug.xcconfig # ios/RocketChatRN.xcodeproj/project.pbxproj
|
@ -119,7 +119,7 @@ jobs:
|
|||
- save_cache: *save-npm-cache-linux
|
||||
|
||||
# Android builds
|
||||
android-build:
|
||||
android-play-build:
|
||||
<<: *defaults
|
||||
docker:
|
||||
- image: circleci/android:api-28-node
|
||||
|
@ -150,7 +150,7 @@ jobs:
|
|||
# echo -e "android.enableAapt2=false" >> ./gradle.properties
|
||||
echo -e "android.useAndroidX=true" >> ./gradle.properties
|
||||
echo -e "android.enableJetifier=true" >> ./gradle.properties
|
||||
echo -e "FLIPPER_VERSION=0.33.1" >> ./gradle.properties
|
||||
echo -e "FLIPPER_VERSION=0.51.0" >> ./gradle.properties
|
||||
|
||||
if [[ $KEYSTORE ]]; then
|
||||
echo $KEYSTORE_BASE64 | base64 --decode > ./app/$KEYSTORE
|
||||
|
@ -167,8 +167,10 @@ jobs:
|
|||
- run:
|
||||
name: Set Google Services
|
||||
command: |
|
||||
cp google-services.prod.json google-services.json
|
||||
working_directory: android/app
|
||||
if [[ $KEYSTORE ]]; then
|
||||
echo $GOOGLE_SERVICES_ANDROID | base64 --decode > google-services.json
|
||||
fi
|
||||
working_directory: android/app/src/play
|
||||
|
||||
- run:
|
||||
name: Config variables
|
||||
|
@ -176,12 +178,12 @@ jobs:
|
|||
echo -e "export default { BUGSNAG_API_KEY: '$BUGSNAG_KEY' };" > ./config.js
|
||||
|
||||
- run:
|
||||
name: Build Android App
|
||||
name: Build Android Play App
|
||||
command: |
|
||||
if [[ $KEYSTORE ]]; then
|
||||
bundle exec fastlane android release
|
||||
bundle exec fastlane android playRelease
|
||||
else
|
||||
bundle exec fastlane android build
|
||||
bundle exec fastlane android playBuild
|
||||
fi
|
||||
working_directory: android
|
||||
|
||||
|
@ -192,8 +194,8 @@ jobs:
|
|||
yarn generate-source-maps-android upload \
|
||||
--api-key=$BUGSNAG_KEY \
|
||||
--app-version=$CIRCLE_BUILD_NUM \
|
||||
--minifiedFile=android/app/build/generated/assets/react/release/app.bundle \
|
||||
--source-map=android/app/build/generated/sourcemaps/react/release/app.bundle.map \
|
||||
--minifiedFile=android/app/build/generated/assets/react/play/release/app.bundle \
|
||||
--source-map=android/app/build/generated/sourcemaps/react/play/release/app.bundle.map \
|
||||
--minified-url=app.bundle \
|
||||
--upload-sources
|
||||
fi
|
||||
|
@ -211,7 +213,68 @@ jobs:
|
|||
- android/fastlane/report.xml
|
||||
- android/app/build/outputs
|
||||
|
||||
android-google-play-alpha:
|
||||
android-foss-build:
|
||||
<<: *defaults
|
||||
docker:
|
||||
- image: circleci/android:api-28-node
|
||||
|
||||
environment:
|
||||
JAVA_OPTS: '-Xms512m -Xmx2g'
|
||||
GRADLE_OPTS: '-Xmx3g -Dorg.gradle.daemon=false -Dorg.gradle.jvmargs="-Xmx2g -XX:+HeapDumpOnOutOfMemoryError"'
|
||||
TERM: dumb
|
||||
<<: *bash-env
|
||||
|
||||
steps:
|
||||
- checkout
|
||||
|
||||
- run: *install-node
|
||||
|
||||
- restore_cache: *restore-npm-cache-linux
|
||||
|
||||
- run: *install-npm-modules
|
||||
|
||||
- run: *update-fastlane-android
|
||||
|
||||
- restore_cache: *restore-gradle-cache
|
||||
|
||||
- run:
|
||||
name: Configure Gradle
|
||||
command: |
|
||||
echo -e "" > ./gradle.properties
|
||||
# echo -e "android.enableAapt2=false" >> ./gradle.properties
|
||||
echo -e "android.useAndroidX=true" >> ./gradle.properties
|
||||
echo -e "android.enableJetifier=true" >> ./gradle.properties
|
||||
echo -e "FLIPPER_VERSION=0.51.0" >> ./gradle.properties
|
||||
echo -e "VERSIONCODE=$CIRCLE_BUILD_NUM" >> ./gradle.properties
|
||||
|
||||
if [[ $KEYSTORE ]]; then
|
||||
echo $KEYSTORE_BASE64 | base64 --decode > ./app/$KEYSTORE
|
||||
echo -e "KEYSTORE=$KEYSTORE" >> ./gradle.properties
|
||||
echo -e "KEYSTORE_PASSWORD=$KEYSTORE_PASSWORD" >> ./gradle.properties
|
||||
echo -e "KEY_ALIAS=$KEY_ALIAS" >> ./gradle.properties
|
||||
echo -e "KEY_PASSWORD=$KEYSTORE_PASSWORD" >> ./gradle.properties
|
||||
fi
|
||||
working_directory: android
|
||||
|
||||
- run:
|
||||
name: Build Android Foss App
|
||||
command: bundle exec fastlane android fossRelease
|
||||
working_directory: android
|
||||
|
||||
- store_artifacts:
|
||||
path: android/app/build/outputs
|
||||
|
||||
- save_cache: *save-npm-cache-linux
|
||||
|
||||
- save_cache: *save-gradle-cache
|
||||
|
||||
- persist_to_workspace:
|
||||
root: .
|
||||
paths:
|
||||
- android/fastlane/report.xml
|
||||
- android/app/build/outputs
|
||||
|
||||
android-google-play-beta:
|
||||
<<: *defaults
|
||||
docker:
|
||||
- image: circleci/android:api-28-node
|
||||
|
@ -231,7 +294,7 @@ jobs:
|
|||
|
||||
- run:
|
||||
name: Fastlane Play Store Upload
|
||||
command: bundle exec fastlane android alpha
|
||||
command: bundle exec fastlane android beta
|
||||
working_directory: android
|
||||
|
||||
# iOS builds
|
||||
|
@ -254,7 +317,9 @@ jobs:
|
|||
- run:
|
||||
name: Set Google Services
|
||||
command: |
|
||||
cp GoogleService-Info.prod.plist GoogleService-Info.plist
|
||||
if [[ $KEYSTORE ]]; then
|
||||
echo $GOOGLE_SERVICES_REACTNATIVE | base64 --decode > GoogleService-Info.plist
|
||||
fi
|
||||
working_directory: ios
|
||||
|
||||
- run:
|
||||
|
@ -337,13 +402,20 @@ workflows:
|
|||
requires:
|
||||
- ios-hold-testflight
|
||||
|
||||
- android-build:
|
||||
- android-play-build:
|
||||
requires:
|
||||
- lint-testunit
|
||||
- android-hold-google-play-alpha:
|
||||
- android-hold-google-play-beta:
|
||||
type: approval
|
||||
requires:
|
||||
- android-build
|
||||
- android-google-play-alpha:
|
||||
- android-play-build
|
||||
- android-google-play-beta:
|
||||
requires:
|
||||
- android-hold-google-play-alpha
|
||||
- android-hold-google-play-beta
|
||||
- android-hold-foss-build:
|
||||
type: approval
|
||||
requires:
|
||||
- lint-testunit
|
||||
- android-foss-build:
|
||||
requires:
|
||||
- android-hold-foss-build
|
||||
|
|
|
@ -1,6 +1,38 @@
|
|||
- Your Rocket.Chat Experimental app version: ####
|
||||
- Your Rocket.Chat server version: ####
|
||||
- Device (or Simulator) you're running with: ####
|
||||
<!--
|
||||
|
||||
**The app isn't connecting to your server?**
|
||||
Make sure your server supports WebSocket. These are the minimum requirements for Apache 2.4 and Nginx 1.3 or greater.
|
||||
Please see our guide for opening issues: https://rocket.chat/docs/contributing/reporting-issues
|
||||
|
||||
If you have questions or are looking for help/support please see: https://rocket.chat/docs/getting-support
|
||||
|
||||
If you are experiencing a bug please search our issues to be sure it is not already present: https://github.com/RocketChat/Rocket.Chat.ReactNative/issues
|
||||
|
||||
-->
|
||||
|
||||
### Description:
|
||||
|
||||
<!-- A clear and concise description of what the bug is. -->
|
||||
|
||||
### Environment Information:
|
||||
|
||||
- Rocket.Chat Server Version:
|
||||
- Rocket.Chat App Version:
|
||||
- Device Name:
|
||||
- OS Version:
|
||||
|
||||
### Steps to reproduce:
|
||||
|
||||
1. <!-- Go to '...' -->
|
||||
2. <!-- Click on '....' -->
|
||||
3. <!-- and so on... -->
|
||||
|
||||
### Expected behavior:
|
||||
|
||||
<!-- What you expect to happen -->
|
||||
|
||||
### Actual behavior:
|
||||
|
||||
<!-- What actually happens with SCREENSHOT, if applicable -->
|
||||
|
||||
### Additional context:
|
||||
|
||||
<!-- Add any other context about the problem here. -->
|
||||
|
|
|
@ -20,19 +20,23 @@ yarn
|
|||
|
||||
Run the app:
|
||||
```sh
|
||||
npx react-native run-ios
|
||||
yarn ios
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```sh
|
||||
npx react-native run-android
|
||||
yarn android
|
||||
```
|
||||
|
||||
At this point, the app should be running on the simulator or on your device!
|
||||
|
||||
*Note: npm won't work on this project*
|
||||
|
||||
### How to inspect the app
|
||||
|
||||
We use [Reactotron](https://github.com/infinitered/reactotron) to inspect logs, redux state, redux-sagas, HTTP requests, etc.
|
||||
|
||||
## Issues needing help
|
||||
|
||||
Didn't find a bug or want a new feature not already reported? Check out the [help wanted](https://github.com/RocketChat/Rocket.Chat.ReactNative/issues?q=is%3Aissue+is%3Aopen+label%3A%22%F0%9F%91%8B+help+wanted%22) or the [good first issue](https://github.com/RocketChat/Rocket.Chat.ReactNative/issues?q=is%3Aissue+is%3Aopen+label%3A%22%F0%9F%8D%AD+good+first+issue%22) labels.
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
export default {
|
||||
BuildConfigs: null
|
||||
};
|
|
@ -3333,6 +3333,165 @@ exports[`Storyshots Message list message 1`] = `
|
|||
>
|
||||
Edited
|
||||
</Text>
|
||||
<Text
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"fontSize": 20,
|
||||
"fontWeight": "300",
|
||||
"marginLeft": 10,
|
||||
"marginVertical": 30,
|
||||
},
|
||||
Object {
|
||||
"color": "#0d0e12",
|
||||
},
|
||||
Object {
|
||||
"marginBottom": 0,
|
||||
"marginTop": 30,
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
Encrypted
|
||||
</Text>
|
||||
<View>
|
||||
<View
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"flexDirection": "column",
|
||||
"paddingHorizontal": 14,
|
||||
"paddingVertical": 4,
|
||||
"width": "100%",
|
||||
},
|
||||
undefined,
|
||||
]
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"flexDirection": "row",
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"borderRadius": 4,
|
||||
"height": 36,
|
||||
"width": 36,
|
||||
},
|
||||
Object {
|
||||
"marginTop": 4,
|
||||
},
|
||||
]
|
||||
}
|
||||
/>
|
||||
<View
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"flex": 1,
|
||||
"marginLeft": 46,
|
||||
},
|
||||
Object {
|
||||
"marginLeft": 10,
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"flex": 1,
|
||||
"flexDirection": "row",
|
||||
}
|
||||
}
|
||||
>
|
||||
<Text
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"backgroundColor": "transparent",
|
||||
"fontFamily": "System",
|
||||
"fontSize": 12,
|
||||
"fontWeight": "300",
|
||||
"lineHeight": 22,
|
||||
"paddingLeft": 10,
|
||||
},
|
||||
Object {
|
||||
"color": "#9ca2a8",
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
10:00 AM
|
||||
</Text>
|
||||
</View>
|
||||
<View>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"flexDirection": "row",
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"flex": 1,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Text
|
||||
numberOfLines={0}
|
||||
style={
|
||||
Array [
|
||||
undefined,
|
||||
Object {
|
||||
"color": "#2f343d",
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
<Text
|
||||
accessibilityLabel="This message has error and is encrypted"
|
||||
numberOfLines={0}
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"backgroundColor": "transparent",
|
||||
"fontFamily": "System",
|
||||
"fontSize": 16,
|
||||
"fontWeight": "400",
|
||||
},
|
||||
Array [
|
||||
Object {},
|
||||
Object {
|
||||
"alignItems": "flex-start",
|
||||
"flexDirection": "row",
|
||||
"flexWrap": "wrap",
|
||||
"justifyContent": "flex-start",
|
||||
"marginBottom": 0,
|
||||
"marginTop": 0,
|
||||
},
|
||||
],
|
||||
]
|
||||
}
|
||||
>
|
||||
This message has error and is encrypted
|
||||
</Text>
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
<Text
|
||||
style={
|
||||
Array [
|
||||
|
|
|
@ -81,7 +81,6 @@ GEM
|
|||
xcodeproj (>= 1.13.0, < 2.0.0)
|
||||
xcpretty (~> 0.3.0)
|
||||
xcpretty-travis-formatter (>= 0.0.3)
|
||||
fastlane-plugin-appcenter (1.8.0)
|
||||
gh_inspector (1.1.3)
|
||||
google-api-client (0.36.4)
|
||||
addressable (~> 2.5, >= 2.5.1)
|
||||
|
@ -156,6 +155,7 @@ GEM
|
|||
uber (0.1.0)
|
||||
unf (0.1.4)
|
||||
unf_ext
|
||||
unf_ext (0.0.7.7)
|
||||
unf_ext (0.0.7.7-x64-mingw32)
|
||||
unicode-display_width (1.7.0)
|
||||
word_wrap (1.0.0)
|
||||
|
@ -171,11 +171,11 @@ GEM
|
|||
xcpretty (~> 0.2, >= 0.0.7)
|
||||
|
||||
PLATFORMS
|
||||
ruby
|
||||
x64-mingw32
|
||||
|
||||
DEPENDENCIES
|
||||
fastlane
|
||||
fastlane-plugin-appcenter
|
||||
|
||||
BUNDLED WITH
|
||||
2.0.2
|
||||
|
|
|
@ -1,8 +1,13 @@
|
|||
def taskRequests = getGradle().getStartParameter().getTaskRequests().toString().toLowerCase()
|
||||
def isPlay = !taskRequests.contains("foss")
|
||||
|
||||
apply plugin: "com.android.application"
|
||||
apply plugin: 'com.google.gms.google-services'
|
||||
apply plugin: 'com.google.firebase.crashlytics'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'com.bugsnag.android.gradle'
|
||||
|
||||
if (isPlay) {
|
||||
apply plugin: 'com.google.firebase.crashlytics'
|
||||
apply plugin: 'com.bugsnag.android.gradle'
|
||||
}
|
||||
|
||||
import com.android.build.OutputFile
|
||||
|
||||
|
@ -141,7 +146,9 @@ android {
|
|||
versionCode VERSIONCODE as Integer
|
||||
versionName VERSIONNAME as String
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
manifestPlaceholders = [BugsnagAPIKey: BugsnagAPIKey as String]
|
||||
if (isPlay) {
|
||||
manifestPlaceholders = [BugsnagAPIKey: BugsnagAPIKey as String]
|
||||
}
|
||||
missingDimensionStrategy "RNNotifications.reactNativeVersion", "reactNative60" // See note below!
|
||||
}
|
||||
|
||||
|
@ -168,8 +175,10 @@ android {
|
|||
minifyEnabled enableProguardInReleaseBuilds
|
||||
setProguardFiles([getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'])
|
||||
signingConfig signingConfigs.release
|
||||
firebaseCrashlytics {
|
||||
nativeSymbolUploadEnabled true
|
||||
if (isPlay) {
|
||||
firebaseCrashlytics {
|
||||
nativeSymbolUploadEnabled true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -182,6 +191,19 @@ android {
|
|||
// }
|
||||
|
||||
// applicationVariants are e.g. debug, release
|
||||
|
||||
flavorDimensions "type"
|
||||
productFlavors {
|
||||
foss {
|
||||
dimension = "type"
|
||||
buildConfigField "boolean", "FDROID_BUILD", "true"
|
||||
}
|
||||
play {
|
||||
dimension = "type"
|
||||
buildConfigField "boolean", "FDROID_BUILD", "false"
|
||||
}
|
||||
}
|
||||
|
||||
applicationVariants.all { variant ->
|
||||
variant.outputs.each { output ->
|
||||
// For each separate APK per architecture, set a unique version code as described here:
|
||||
|
@ -217,7 +239,7 @@ dependencies {
|
|||
implementation fileTree(dir: "libs", include: ["*.jar"])
|
||||
//noinspection GradleDynamicVersion
|
||||
implementation "com.facebook.react:react-native:+" // From node_modules
|
||||
implementation "com.google.firebase:firebase-messaging:18.0.0"
|
||||
playImplementation "com.google.firebase:firebase-messaging:18.0.0"
|
||||
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.0.0"
|
||||
debugImplementation("com.facebook.flipper:flipper:${FLIPPER_VERSION}") {
|
||||
exclude group:'com.facebook.fbjni'
|
||||
|
@ -240,6 +262,7 @@ dependencies {
|
|||
implementation "com.google.code.gson:gson:2.8.5"
|
||||
implementation "com.github.bumptech.glide:glide:4.9.0"
|
||||
annotationProcessor "com.github.bumptech.glide:compiler:4.9.0"
|
||||
implementation "com.tencent:mmkv-static:1.2.1"
|
||||
}
|
||||
|
||||
// Run this once to be able to run the application with BUCK
|
||||
|
@ -249,4 +272,7 @@ task copyDownloadableDepsToLibs(type: Copy) {
|
|||
into 'libs'
|
||||
}
|
||||
|
||||
apply from: file("../../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesAppBuildGradle(project)
|
||||
apply from: file("../../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesAppBuildGradle(project)
|
||||
if (isPlay) {
|
||||
apply plugin: 'com.google.gms.google-services'
|
||||
}
|
||||
|
|
|
@ -7,7 +7,8 @@
|
|||
android:name=".MainDebugApplication"
|
||||
tools:ignore="GoogleAppIndexingWarning"
|
||||
tools:replace="android:name"
|
||||
tools:targetApi="28" />
|
||||
tools:targetApi="28"
|
||||
android:networkSecurityConfig="@xml/network_security_config" />
|
||||
|
||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
|
||||
</manifest>
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<network-security-config xmlns:tools="http://schemas.android.com/tools">
|
||||
<base-config cleartextTrafficPermitted="true">
|
||||
<trust-anchors>
|
||||
<certificates src="system" />
|
||||
<certificates src="user"
|
||||
tools:ignore="AcceptsUserCertificates" />
|
||||
</trust-anchors>
|
||||
</base-config>
|
||||
</network-security-config>
|
|
@ -0,0 +1,20 @@
|
|||
package chat.rocket.reactnative;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.wix.reactnativenotifications.core.AppLaunchHelper;
|
||||
import com.wix.reactnativenotifications.core.AppLifecycleFacade;
|
||||
import com.wix.reactnativenotifications.core.JsIOHelper;
|
||||
import com.wix.reactnativenotifications.core.notification.PushNotification;
|
||||
|
||||
public class CustomPushNotification extends PushNotification {
|
||||
public static ReactApplicationContext reactApplicationContext;
|
||||
|
||||
public CustomPushNotification(Context context, Bundle bundle, AppLifecycleFacade appLifecycleFacade, AppLaunchHelper appLaunchHelper, JsIOHelper jsIoHelper) {
|
||||
super(context, bundle, appLifecycleFacade, appLaunchHelper, jsIoHelper);
|
||||
reactApplicationContext = new ReactApplicationContext(context);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
package chat.rocket.reactnative;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
|
||||
public class DismissNotification extends BroadcastReceiver {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,97 @@
|
|||
package chat.rocket.reactnative;
|
||||
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.bridge.Callback;
|
||||
|
||||
import com.ammarahmed.mmkv.SecureKeystore;
|
||||
import com.tencent.mmkv.MMKV;
|
||||
|
||||
import java.math.BigInteger;
|
||||
|
||||
class RNCallback implements Callback {
|
||||
public void invoke(Object... args) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
class Utils {
|
||||
static public String toHex(String arg) {
|
||||
try {
|
||||
return String.format("%x", new BigInteger(1, arg.getBytes("UTF-8")));
|
||||
} catch (Exception e) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class Ejson {
|
||||
String host;
|
||||
String rid;
|
||||
String type;
|
||||
Sender sender;
|
||||
String messageId;
|
||||
String notificationType;
|
||||
|
||||
private MMKV mmkv;
|
||||
|
||||
private String TOKEN_KEY = "reactnativemeteor_usertoken-";
|
||||
|
||||
public Ejson() {
|
||||
ReactApplicationContext reactApplicationContext = CustomPushNotification.reactApplicationContext;
|
||||
|
||||
// Start MMKV container
|
||||
MMKV.initialize(reactApplicationContext);
|
||||
SecureKeystore secureKeystore = new SecureKeystore(reactApplicationContext);
|
||||
|
||||
// https://github.com/ammarahm-ed/react-native-mmkv-storage/blob/master/src/loader.js#L31
|
||||
String alias = Utils.toHex("com.MMKV.default");
|
||||
|
||||
// Retrieve container password
|
||||
secureKeystore.getSecureKey(alias, new RNCallback() {
|
||||
@Override
|
||||
public void invoke(Object... args) {
|
||||
String error = (String) args[0];
|
||||
if (error == null) {
|
||||
String password = (String) args[1];
|
||||
mmkv = MMKV.mmkvWithID("default", MMKV.SINGLE_PROCESS_MODE, password);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public String getAvatarUri() {
|
||||
if (type == null) {
|
||||
return null;
|
||||
}
|
||||
return serverURL() + "/avatar/" + this.sender.username + "?rc_token=" + token() + "&rc_uid=" + userId();
|
||||
}
|
||||
|
||||
public String token() {
|
||||
String userId = userId();
|
||||
if (mmkv != null && userId != null) {
|
||||
return mmkv.decodeString(TOKEN_KEY.concat(userId));
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
public String userId() {
|
||||
String serverURL = serverURL();
|
||||
if (mmkv != null && serverURL != null) {
|
||||
return mmkv.decodeString(TOKEN_KEY.concat(serverURL));
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
public String serverURL() {
|
||||
String url = this.host;
|
||||
if (url != null && url.endsWith("/")) {
|
||||
url = url.substring(0, url.length() - 1);
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
public class Sender {
|
||||
String username;
|
||||
String _id;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
package chat.rocket.reactnative;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
|
||||
public class ReplyBroadcast extends BroadcastReceiver {
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="512"
|
||||
android:viewportHeight="512">
|
||||
<path
|
||||
android:pathData="M0,0h512v512h-512z"
|
||||
android:fillColor="#ffffff"/>
|
||||
</vector>
|
|
@ -0,0 +1,19 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="512"
|
||||
android:viewportHeight="512">
|
||||
<group>
|
||||
<clip-path android:pathData="M145,160h218v191.345h-218z M 0,0"/>
|
||||
<path
|
||||
android:pathData="M336.076,234.105L336.079,234.11C336.079,234.109 336.078,234.108 336.078,234.108C336.077,234.107 336.077,234.106 336.076,234.105ZM210.509,167.831C217.233,171.56 223.589,176.278 229.017,181.523C237.766,179.94 246.788,179.142 255.94,179.142C283.338,179.142 309.314,186.319 329.078,199.348C339.313,206.098 347.448,214.107 353.255,223.155C359.722,233.237 363,244.078 363,255.695C363,266.999 359.722,277.845 353.255,287.925C347.448,296.977 339.313,304.983 329.078,311.734C309.314,324.762 283.341,331.935 255.94,331.935C246.788,331.935 237.768,331.137 229.017,329.557C223.587,334.799 217.233,339.519 210.509,343.249C174.584,361.216 144.792,343.671 144.792,343.671C144.792,343.671 172.491,320.188 167.986,299.602C155.593,286.917 148.878,271.619 148.878,255.387C148.878,239.461 155.595,224.162 167.986,211.475C172.49,190.895 144.801,167.416 144.792,167.408C144.801,167.403 174.589,149.864 210.509,167.831Z"
|
||||
android:fillColor="#DB2323"/>
|
||||
<path
|
||||
android:pathData="M189.04,291.26C176.71,281.543 169.31,269.108 169.31,255.555C169.31,224.456 208.278,199.245 256.348,199.245C304.418,199.245 343.386,224.456 343.386,255.555C343.386,286.655 304.418,311.866 256.348,311.866C244.501,311.866 233.206,310.335 222.912,307.561L215.386,314.82C211.296,318.765 206.503,322.334 201.507,325.147C194.884,328.399 188.345,330.174 181.875,330.715C182.24,330.052 182.576,329.379 182.937,328.715C190.478,314.822 192.512,302.337 189.04,291.26Z"
|
||||
android:fillColor="#ffffff"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M214.708,268.127C207.625,268.127 201.883,262.413 201.883,255.364C201.883,248.316 207.625,242.602 214.708,242.602C221.791,242.602 227.533,248.316 227.533,255.364C227.533,262.413 221.791,268.127 214.708,268.127ZM255.998,268.127C248.915,268.127 243.173,262.413 243.173,255.364C243.173,248.316 248.915,242.602 255.998,242.602C263.08,242.602 268.822,248.316 268.822,255.364C268.822,262.413 263.08,268.127 255.998,268.127ZM297.287,268.127C290.204,268.127 284.462,262.413 284.462,255.364C284.462,248.316 290.204,242.602 297.287,242.602C304.37,242.602 310.112,248.316 310.112,255.364C310.112,262.413 304.37,268.127 297.287,268.127Z"
|
||||
android:fillColor="#DB2323"/>
|
||||
</group>
|
||||
</vector>
|
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android" android:opacity="opaque">
|
||||
<!-- the background color. it can be a system color or a custom one defined in colors.xml -->
|
||||
<item android:drawable="@color/splashBackground" />
|
||||
<item>
|
||||
<!-- the app logo, centered horizontally and vertically -->
|
||||
<bitmap
|
||||
android:src="@drawable/splash"
|
||||
android:gravity="center" />
|
||||
</item>
|
||||
</layer-list>
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@mipmap/ic_launcher_background"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@mipmap/ic_launcher_background"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 367 B |
After Width: | Height: | Size: 3.7 KiB |
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 233 B |
After Width: | Height: | Size: 2.4 KiB |
After Width: | Height: | Size: 2.7 KiB |
After Width: | Height: | Size: 508 B |
After Width: | Height: | Size: 5.3 KiB |
After Width: | Height: | Size: 4.0 KiB |
After Width: | Height: | Size: 897 B |
After Width: | Height: | Size: 8.3 KiB |
After Width: | Height: | Size: 5.6 KiB |
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 12 KiB |
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<item name="splashBackground" type="color">#000000</item>
|
||||
</resources>
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="primary_dark">#660B0B0B</color>
|
||||
<item name="splashBackground" type="color">#eeeff1</item>
|
||||
<item name="notification_text" type="color">#CC3333</item>
|
||||
</resources>
|
|
@ -0,0 +1,4 @@
|
|||
<resources>
|
||||
<string name="app_name">Rocket.Chat</string>
|
||||
<string name="share_extension_name">Rocket.Chat</string>
|
||||
</resources>
|
|
@ -0,0 +1,28 @@
|
|||
<resources>
|
||||
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
|
||||
<item name="android:colorEdgeEffect">#aaaaaa</item>
|
||||
<item name="colorPrimaryDark">@color/splashBackground</item>
|
||||
<item name="android:navigationBarColor">@color/splashBackground</item>
|
||||
</style>
|
||||
|
||||
<style name="Share.Window" parent="android:Theme">
|
||||
<item name="android:windowEnterAnimation">@null</item>
|
||||
<item name="android:windowExitAnimation">@null</item>
|
||||
</style>
|
||||
|
||||
<style name="Theme.Share.Transparent" parent="android:Theme">
|
||||
<item name="android:windowIsTranslucent">true</item>
|
||||
<item name="android:windowBackground">@color/primary_dark</item>
|
||||
<item name="android:windowContentOverlay">@null</item>
|
||||
<item name="android:windowNoTitle">true</item>
|
||||
<item name="android:windowIsFloating">true</item>
|
||||
<item name="android:backgroundDimEnabled">true</item>
|
||||
<item name="android:windowAnimationStyle">@style/Share.Window</item>
|
||||
</style>
|
||||
|
||||
<style name="BootTheme" parent="AppTheme">
|
||||
<item name="android:background">@drawable/launch_screen</item>
|
||||
<item name="colorPrimaryDark">@color/splashBackground</item>
|
||||
<item name="android:navigationBarColor">@color/splashBackground</item>
|
||||
</style>
|
||||
</resources>
|
|
@ -67,9 +67,6 @@
|
|||
<data android:mimeType="*/*" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<meta-data
|
||||
android:name="com.bugsnag.android.API_KEY"
|
||||
android:value="${BugsnagAPIKey}" />
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
|
|
@ -1,346 +0,0 @@
|
|||
package chat.rocket.reactnative;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.app.RemoteInput;
|
||||
import android.content.Intent;
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.drawable.Icon;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.app.Person;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.bumptech.glide.load.resource.bitmap.RoundedCorners;
|
||||
import com.bumptech.glide.request.RequestOptions;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.lang.InterruptedException;
|
||||
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.wix.reactnativenotifications.core.AppLaunchHelper;
|
||||
import com.wix.reactnativenotifications.core.AppLifecycleFacade;
|
||||
import com.wix.reactnativenotifications.core.JsIOHelper;
|
||||
import com.wix.reactnativenotifications.core.notification.PushNotification;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.Iterator;
|
||||
|
||||
import static com.wix.reactnativenotifications.Defs.NOTIFICATION_RECEIVED_EVENT_NAME;
|
||||
|
||||
public class CustomPushNotification extends PushNotification {
|
||||
public static ReactApplicationContext reactApplicationContext;
|
||||
final NotificationManager notificationManager;
|
||||
|
||||
public CustomPushNotification(Context context, Bundle bundle, AppLifecycleFacade appLifecycleFacade, AppLaunchHelper appLaunchHelper, JsIOHelper jsIoHelper) {
|
||||
super(context, bundle, appLifecycleFacade, appLaunchHelper, jsIoHelper);
|
||||
reactApplicationContext = new ReactApplicationContext(context);
|
||||
notificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
}
|
||||
|
||||
private static Map<String, List<Bundle>> notificationMessages = new HashMap<String, List<Bundle>>();
|
||||
public static String KEY_REPLY = "KEY_REPLY";
|
||||
public static String NOTIFICATION_ID = "NOTIFICATION_ID";
|
||||
|
||||
public static void clearMessages(int notId) {
|
||||
notificationMessages.remove(Integer.toString(notId));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReceived() throws InvalidNotificationException {
|
||||
Bundle received = mNotificationProps.asBundle();
|
||||
Ejson receivedEjson = new Gson().fromJson(received.getString("ejson", "{}"), Ejson.class);
|
||||
|
||||
if (receivedEjson.notificationType != null && receivedEjson.notificationType.equals("message-id-only")) {
|
||||
notificationLoad(receivedEjson, new Callback() {
|
||||
@Override
|
||||
public void call(@Nullable Bundle bundle) {
|
||||
if (bundle != null) {
|
||||
mNotificationProps = createProps(bundle);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// We should re-read these values since that can be changed by notificationLoad
|
||||
Bundle bundle = mNotificationProps.asBundle();
|
||||
Ejson loadedEjson = new Gson().fromJson(bundle.getString("ejson", "{}"), Ejson.class);
|
||||
String notId = bundle.getString("notId", "1");
|
||||
|
||||
if (notificationMessages.get(notId) == null) {
|
||||
notificationMessages.put(notId, new ArrayList<Bundle>());
|
||||
}
|
||||
|
||||
boolean hasSender = loadedEjson.sender != null;
|
||||
String title = bundle.getString("title");
|
||||
|
||||
bundle.putLong("time", new Date().getTime());
|
||||
bundle.putString("username", hasSender ? loadedEjson.sender.username : title);
|
||||
bundle.putString("senderId", hasSender ? loadedEjson.sender._id : "1");
|
||||
bundle.putString("avatarUri", loadedEjson.getAvatarUri());
|
||||
|
||||
notificationMessages.get(notId).add(bundle);
|
||||
postNotification(Integer.parseInt(notId));
|
||||
notifyReceivedToJS();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onOpened() {
|
||||
Bundle bundle = mNotificationProps.asBundle();
|
||||
final String notId = bundle.getString("notId", "1");
|
||||
notificationMessages.remove(notId);
|
||||
digestNotification();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Notification.Builder getNotificationBuilder(PendingIntent intent) {
|
||||
final Notification.Builder notification = new Notification.Builder(mContext);
|
||||
|
||||
Bundle bundle = mNotificationProps.asBundle();
|
||||
String notId = bundle.getString("notId", "1");
|
||||
String title = bundle.getString("title");
|
||||
String message = bundle.getString("message");
|
||||
Boolean notificationLoaded = bundle.getBoolean("notificationLoaded", false);
|
||||
Ejson ejson = new Gson().fromJson(bundle.getString("ejson", "{}"), Ejson.class);
|
||||
|
||||
notification
|
||||
.setContentTitle(title)
|
||||
.setContentText(message)
|
||||
.setContentIntent(intent)
|
||||
.setPriority(Notification.PRIORITY_HIGH)
|
||||
.setDefaults(Notification.DEFAULT_ALL)
|
||||
.setAutoCancel(true);
|
||||
|
||||
Integer notificationId = Integer.parseInt(notId);
|
||||
notificationColor(notification);
|
||||
notificationChannel(notification);
|
||||
notificationIcons(notification, bundle);
|
||||
notificationDismiss(notification, notificationId);
|
||||
|
||||
// if notificationType is null (RC < 3.5) or notificationType is different of message-id-only or notification was loaded successfully
|
||||
if (ejson.notificationType == null || !ejson.notificationType.equals("message-id-only") || notificationLoaded) {
|
||||
notificationStyle(notification, notificationId, bundle);
|
||||
notificationReply(notification, notificationId, bundle);
|
||||
|
||||
// message couldn't be loaded from server (Fallback notification)
|
||||
} else {
|
||||
Gson gson = new Gson();
|
||||
// iterate over the current notification ids to dismiss fallback notifications from same server
|
||||
for (Map.Entry<String, List<Bundle>> bundleList : notificationMessages.entrySet()) {
|
||||
// iterate over the notifications with this id (same host + rid)
|
||||
Iterator iterator = bundleList.getValue().iterator();
|
||||
while (iterator.hasNext()) {
|
||||
Bundle not = (Bundle) iterator.next();
|
||||
// get the notification info
|
||||
Ejson notEjson = gson.fromJson(not.getString("ejson", "{}"), Ejson.class);
|
||||
// if already has a notification from same server
|
||||
if (ejson.serverURL().equals(notEjson.serverURL())) {
|
||||
String id = not.getString("notId");
|
||||
// cancel this notification
|
||||
notificationManager.cancel(Integer.parseInt(id));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return notification;
|
||||
}
|
||||
|
||||
private void notifyReceivedToJS() {
|
||||
mJsIOHelper.sendEventToJS(NOTIFICATION_RECEIVED_EVENT_NAME, mNotificationProps.asBundle(), mAppLifecycleFacade.getRunningReactContext());
|
||||
}
|
||||
|
||||
private Bitmap getAvatar(String uri) {
|
||||
try {
|
||||
return Glide.with(mContext)
|
||||
.asBitmap()
|
||||
.apply(RequestOptions.bitmapTransform(new RoundedCorners(10)))
|
||||
.load(uri)
|
||||
.submit(100, 100)
|
||||
.get();
|
||||
} catch (final ExecutionException | InterruptedException e) {
|
||||
return largeIcon();
|
||||
}
|
||||
}
|
||||
|
||||
private Bitmap largeIcon() {
|
||||
final Resources res = mContext.getResources();
|
||||
String packageName = mContext.getPackageName();
|
||||
int largeIconResId = res.getIdentifier("ic_launcher", "mipmap", packageName);
|
||||
Bitmap largeIconBitmap = BitmapFactory.decodeResource(res, largeIconResId);
|
||||
return largeIconBitmap;
|
||||
}
|
||||
|
||||
private void notificationIcons(Notification.Builder notification, Bundle bundle) {
|
||||
final Resources res = mContext.getResources();
|
||||
String packageName = mContext.getPackageName();
|
||||
|
||||
int smallIconResId = res.getIdentifier("ic_notification", "mipmap", packageName);
|
||||
|
||||
Gson gson = new Gson();
|
||||
Ejson ejson = gson.fromJson(bundle.getString("ejson", "{}"), Ejson.class);
|
||||
|
||||
notification.setSmallIcon(smallIconResId);
|
||||
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
|
||||
notification.setLargeIcon(getAvatar(ejson.getAvatarUri()));
|
||||
}
|
||||
}
|
||||
|
||||
private void notificationChannel(Notification.Builder notification) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
String CHANNEL_ID = "rocketchatrn_channel_01";
|
||||
String CHANNEL_NAME = "All";
|
||||
|
||||
NotificationChannel channel = new NotificationChannel(CHANNEL_ID,
|
||||
CHANNEL_NAME,
|
||||
NotificationManager.IMPORTANCE_DEFAULT);
|
||||
|
||||
final NotificationManager notificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
notificationManager.createNotificationChannel(channel);
|
||||
|
||||
notification.setChannelId(CHANNEL_ID);
|
||||
}
|
||||
}
|
||||
|
||||
private String extractMessage(String message, Ejson ejson) {
|
||||
if (ejson.type != null && !ejson.type.equals("d")) {
|
||||
int pos = message.indexOf(":");
|
||||
int start = pos == -1 ? 0 : pos + 2;
|
||||
return message.substring(start, message.length());
|
||||
}
|
||||
return message;
|
||||
}
|
||||
|
||||
private void notificationColor(Notification.Builder notification) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
notification.setColor(mContext.getColor(R.color.notification_text));
|
||||
}
|
||||
}
|
||||
|
||||
private void notificationStyle(Notification.Builder notification, int notId, Bundle bundle) {
|
||||
List<Bundle> bundles = notificationMessages.get(Integer.toString(notId));
|
||||
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
|
||||
Notification.InboxStyle messageStyle = new Notification.InboxStyle();
|
||||
if (bundles != null) {
|
||||
for (int i = 0; i < bundles.size(); i++) {
|
||||
Bundle data = bundles.get(i);
|
||||
String message = data.getString("message");
|
||||
|
||||
messageStyle.addLine(message);
|
||||
}
|
||||
}
|
||||
|
||||
notification.setStyle(messageStyle);
|
||||
} else {
|
||||
Notification.MessagingStyle messageStyle;
|
||||
|
||||
Gson gson = new Gson();
|
||||
Ejson ejson = gson.fromJson(bundle.getString("ejson", "{}"), Ejson.class);
|
||||
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
|
||||
messageStyle = new Notification.MessagingStyle("");
|
||||
} else {
|
||||
Person sender = new Person.Builder()
|
||||
.setKey("")
|
||||
.setName("")
|
||||
.build();
|
||||
messageStyle = new Notification.MessagingStyle(sender);
|
||||
}
|
||||
|
||||
String title = bundle.getString("title");
|
||||
messageStyle.setConversationTitle(title);
|
||||
|
||||
if (bundles != null) {
|
||||
for (int i = 0; i < bundles.size(); i++) {
|
||||
Bundle data = bundles.get(i);
|
||||
|
||||
long timestamp = data.getLong("time");
|
||||
String message = data.getString("message");
|
||||
String username = data.getString("username");
|
||||
String senderId = data.getString("senderId");
|
||||
String avatarUri = data.getString("avatarUri");
|
||||
|
||||
String m = extractMessage(message, ejson);
|
||||
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
|
||||
messageStyle.addMessage(m, timestamp, username);
|
||||
} else {
|
||||
Bitmap avatar = getAvatar(avatarUri);
|
||||
|
||||
Person.Builder sender = new Person.Builder()
|
||||
.setKey(senderId)
|
||||
.setName(username);
|
||||
|
||||
if (avatar != null) {
|
||||
sender.setIcon(Icon.createWithBitmap(avatar));
|
||||
}
|
||||
|
||||
Person person = sender.build();
|
||||
|
||||
messageStyle.addMessage(m, timestamp, person);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
notification.setStyle(messageStyle);
|
||||
}
|
||||
}
|
||||
|
||||
private void notificationReply(Notification.Builder notification, int notificationId, Bundle bundle) {
|
||||
String notId = bundle.getString("notId", "1");
|
||||
String ejson = bundle.getString("ejson", "{}");
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N || notId.equals("1") || ejson.equals("{}")) {
|
||||
return;
|
||||
}
|
||||
String label = "Reply";
|
||||
|
||||
final Resources res = mContext.getResources();
|
||||
String packageName = mContext.getPackageName();
|
||||
int smallIconResId = res.getIdentifier("ic_notification", "mipmap", packageName);
|
||||
|
||||
Intent replyIntent = new Intent(mContext, ReplyBroadcast.class);
|
||||
replyIntent.setAction(KEY_REPLY);
|
||||
replyIntent.putExtra("pushNotification", bundle);
|
||||
|
||||
PendingIntent replyPendingIntent = PendingIntent.getBroadcast(mContext, notificationId, replyIntent, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
|
||||
RemoteInput remoteInput = new RemoteInput.Builder(KEY_REPLY)
|
||||
.setLabel(label)
|
||||
.build();
|
||||
|
||||
CharSequence title = label;
|
||||
Notification.Action replyAction = new Notification.Action.Builder(smallIconResId, title, replyPendingIntent)
|
||||
.addRemoteInput(remoteInput)
|
||||
.setAllowGeneratedReplies(true)
|
||||
.build();
|
||||
|
||||
notification
|
||||
.setShowWhen(true)
|
||||
.addAction(replyAction);
|
||||
}
|
||||
|
||||
private void notificationDismiss(Notification.Builder notification, int notificationId) {
|
||||
Intent intent = new Intent(mContext, DismissNotification.class);
|
||||
intent.putExtra(NOTIFICATION_ID, notificationId);
|
||||
|
||||
PendingIntent dismissPendingIntent = PendingIntent.getBroadcast(mContext, notificationId, intent, 0);
|
||||
|
||||
notification.setDeleteIntent(dismissPendingIntent);
|
||||
}
|
||||
|
||||
private void notificationLoad(Ejson ejson, Callback callback) {
|
||||
LoadNotification.load(reactApplicationContext, ejson, callback);
|
||||
}
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
package chat.rocket.reactnative;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
|
||||
public class DismissNotification extends BroadcastReceiver {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
int notId = intent.getExtras().getInt(CustomPushNotification.NOTIFICATION_ID);
|
||||
CustomPushNotification.clearMessages(notId);
|
||||
}
|
||||
}
|
|
@ -1,45 +0,0 @@
|
|||
package chat.rocket.reactnative;
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
import chat.rocket.userdefaults.RNUserDefaultsModule;
|
||||
|
||||
public class Ejson {
|
||||
String host;
|
||||
String rid;
|
||||
String type;
|
||||
Sender sender;
|
||||
String messageId;
|
||||
String notificationType;
|
||||
|
||||
private String TOKEN_KEY = "reactnativemeteor_usertoken-";
|
||||
private SharedPreferences sharedPreferences = RNUserDefaultsModule.getPreferences(CustomPushNotification.reactApplicationContext);
|
||||
|
||||
public String getAvatarUri() {
|
||||
if (type == null) {
|
||||
return null;
|
||||
}
|
||||
return serverURL() + "/avatar/" + this.sender.username + "?rc_token=" + token() + "&rc_uid=" + userId();
|
||||
}
|
||||
|
||||
public String token() {
|
||||
return sharedPreferences.getString(TOKEN_KEY.concat(userId()), "");
|
||||
}
|
||||
|
||||
public String userId() {
|
||||
return sharedPreferences.getString(TOKEN_KEY.concat(serverURL()), "");
|
||||
}
|
||||
|
||||
public String serverURL() {
|
||||
String url = this.host;
|
||||
if (url != null && url.endsWith("/")) {
|
||||
url = url.substring(0, url.length() - 1);
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
public class Sender {
|
||||
String username;
|
||||
String _id;
|
||||
}
|
||||
}
|
|
@ -1,101 +0,0 @@
|
|||
package chat.rocket.reactnative;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.content.Context;
|
||||
|
||||
import okhttp3.Call;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.HttpUrl;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
import okhttp3.Interceptor;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import java.io.IOException;
|
||||
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
|
||||
import chat.rocket.userdefaults.RNUserDefaultsModule;
|
||||
|
||||
class JsonResponse {
|
||||
Data data;
|
||||
|
||||
class Data {
|
||||
Notification notification;
|
||||
|
||||
class Notification {
|
||||
String notId;
|
||||
String title;
|
||||
String text;
|
||||
Payload payload;
|
||||
|
||||
class Payload {
|
||||
String host;
|
||||
String rid;
|
||||
String type;
|
||||
Sender sender;
|
||||
String messageId;
|
||||
String notificationType;
|
||||
String name;
|
||||
String messageType;
|
||||
|
||||
class Sender {
|
||||
String _id;
|
||||
String username;
|
||||
String name;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class LoadNotification {
|
||||
private static int RETRY_COUNT = 0;
|
||||
private static int[] TIMEOUT = new int[]{ 0, 1, 3, 5, 10 };
|
||||
private static String TOKEN_KEY = "reactnativemeteor_usertoken-";
|
||||
|
||||
public static void load(ReactApplicationContext reactApplicationContext, final Ejson ejson, Callback callback) {
|
||||
final OkHttpClient client = new OkHttpClient();
|
||||
HttpUrl.Builder url = HttpUrl.parse(ejson.serverURL().concat("/api/v1/push.get")).newBuilder();
|
||||
|
||||
Request request = new Request.Builder()
|
||||
.header("x-user-id", ejson.userId())
|
||||
.header("x-auth-token", ejson.token())
|
||||
.url(url.addQueryParameter("id", ejson.messageId).build())
|
||||
.build();
|
||||
|
||||
runRequest(client, request, callback);
|
||||
}
|
||||
|
||||
private static void runRequest(OkHttpClient client, Request request, Callback callback) {
|
||||
try {
|
||||
Thread.sleep(TIMEOUT[RETRY_COUNT] * 1000);
|
||||
|
||||
Response response = client.newCall(request).execute();
|
||||
String body = response.body().string();
|
||||
if (!response.isSuccessful()) {
|
||||
throw new Exception("Error");
|
||||
}
|
||||
|
||||
Gson gson = new Gson();
|
||||
JsonResponse json = gson.fromJson(body, JsonResponse.class);
|
||||
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putString("notId", json.data.notification.notId);
|
||||
bundle.putString("title", json.data.notification.title);
|
||||
bundle.putString("message", json.data.notification.text);
|
||||
bundle.putString("ejson", gson.toJson(json.data.notification.payload));
|
||||
bundle.putBoolean("notificationLoaded", true);
|
||||
|
||||
callback.call(bundle);
|
||||
|
||||
} catch (Exception e) {
|
||||
if (RETRY_COUNT <= TIMEOUT.length) {
|
||||
RETRY_COUNT++;
|
||||
runRequest(client, request, callback);
|
||||
} else {
|
||||
callback.call(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,13 +1,33 @@
|
|||
package chat.rocket.reactnative;
|
||||
|
||||
import com.facebook.react.ReactActivityDelegate;
|
||||
import com.facebook.react.ReactRootView;
|
||||
import com.swmansion.gesturehandler.react.RNGestureHandlerEnabledRootView;
|
||||
import android.os.Bundle;
|
||||
import com.facebook.react.ReactFragmentActivity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.res.Configuration;
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
import com.facebook.react.bridge.Arguments;
|
||||
import com.facebook.react.bridge.WritableMap;
|
||||
import com.facebook.react.ReactRootView;
|
||||
import com.facebook.react.ReactActivityDelegate;
|
||||
import com.facebook.react.ReactFragmentActivity;
|
||||
|
||||
import com.swmansion.gesturehandler.react.RNGestureHandlerEnabledRootView;
|
||||
import com.zoontek.rnbootsplash.RNBootSplash;
|
||||
import com.tencent.mmkv.MMKV;
|
||||
import com.google.gson.Gson;
|
||||
|
||||
class ThemePreferences {
|
||||
String currentTheme;
|
||||
String darkLevel;
|
||||
}
|
||||
|
||||
class SortPreferences {
|
||||
String sortBy;
|
||||
Boolean groupByType;
|
||||
Boolean showFavorites;
|
||||
Boolean showUnread;
|
||||
}
|
||||
|
||||
public class MainActivity extends ReactFragmentActivity {
|
||||
|
||||
|
@ -16,6 +36,61 @@ public class MainActivity extends ReactFragmentActivity {
|
|||
// https://github.com/software-mansion/react-native-screens/issues/17#issuecomment-424704067
|
||||
super.onCreate(null);
|
||||
RNBootSplash.init(R.drawable.launch_screen, MainActivity.this);
|
||||
|
||||
MMKV.initialize(MainActivity.this);
|
||||
|
||||
// Start the MMKV container
|
||||
MMKV defaultMMKV = MMKV.defaultMMKV();
|
||||
boolean alreadyMigrated = defaultMMKV.decodeBool("alreadyMigrated");
|
||||
|
||||
if (!alreadyMigrated) {
|
||||
// MMKV Instance that will be used by JS
|
||||
MMKV mmkv = MMKV.mmkvWithID("default");
|
||||
|
||||
// SharedPreferences -> MMKV (Migration)
|
||||
SharedPreferences sharedPreferences = getSharedPreferences("react-native", Context.MODE_PRIVATE);
|
||||
mmkv.importFromSharedPreferences(sharedPreferences);
|
||||
|
||||
// SharedPreferences only save strings, so we saved this value as a String and now we'll need to cast into a MMKV object
|
||||
|
||||
// Theme preferences object
|
||||
String THEME_PREFERENCES_KEY = "RC_THEME_PREFERENCES_KEY";
|
||||
String themeJson = sharedPreferences.getString(THEME_PREFERENCES_KEY, "");
|
||||
if (!themeJson.isEmpty()) {
|
||||
ThemePreferences themePreferences = new Gson().fromJson(themeJson, ThemePreferences.class);
|
||||
WritableMap themeMap = new Arguments().createMap();
|
||||
themeMap.putString("currentTheme", themePreferences.currentTheme);
|
||||
themeMap.putString("darkLevel", themePreferences.darkLevel);
|
||||
Bundle bundle = Arguments.toBundle(themeMap);
|
||||
mmkv.encode(THEME_PREFERENCES_KEY, bundle);
|
||||
}
|
||||
|
||||
// Sort preferences object
|
||||
String SORT_PREFS_KEY = "RC_SORT_PREFS_KEY";
|
||||
String sortJson = sharedPreferences.getString(SORT_PREFS_KEY, "");
|
||||
if (!sortJson.isEmpty()) {
|
||||
SortPreferences sortPreferences = new Gson().fromJson(sortJson, SortPreferences.class);
|
||||
WritableMap sortMap = new Arguments().createMap();
|
||||
sortMap.putString("sortBy", sortPreferences.sortBy);
|
||||
if (sortPreferences.groupByType != null) {
|
||||
sortMap.putBoolean("groupByType", sortPreferences.groupByType);
|
||||
}
|
||||
if (sortPreferences.showFavorites != null) {
|
||||
sortMap.putBoolean("showFavorites", sortPreferences.showFavorites);
|
||||
}
|
||||
if (sortPreferences.showUnread != null) {
|
||||
sortMap.putBoolean("showUnread", sortPreferences.showUnread);
|
||||
}
|
||||
Bundle bundle = Arguments.toBundle(sortMap);
|
||||
mmkv.encode(SORT_PREFS_KEY, bundle);
|
||||
}
|
||||
|
||||
// Remove all our keys of SharedPreferences
|
||||
sharedPreferences.edit().clear().commit();
|
||||
|
||||
// Mark migration complete
|
||||
defaultMMKV.encode("alreadyMigrated", true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -49,8 +49,10 @@ public class MainApplication extends Application implements ReactApplication, IN
|
|||
protected List<ReactPackage> getPackages() {
|
||||
@SuppressWarnings("UnnecessaryLocalVariable")
|
||||
List<ReactPackage> packages = new PackageList(this).getPackages();
|
||||
if (!BuildConfig.FDROID_BUILD) {
|
||||
packages.add(new RNNotificationsPackage(MainApplication.this));
|
||||
}
|
||||
packages.add(new KeyboardInputPackage(MainApplication.this));
|
||||
packages.add(new RNNotificationsPackage(MainApplication.this));
|
||||
packages.add(new WatermelonDBPackage());
|
||||
packages.add(new RNCViewPagerPackage());
|
||||
// packages.add(new ModuleRegistryAdapter(mModuleRegistryProvider));
|
||||
|
|
|
@ -1,157 +0,0 @@
|
|||
package chat.rocket.reactnative;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.RemoteInput;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.res.Resources;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import java.io.IOException;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import okhttp3.Call;
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.Response;
|
||||
|
||||
import chat.rocket.userdefaults.RNUserDefaultsModule;
|
||||
import com.wix.reactnativenotifications.core.NotificationIntentAdapter;
|
||||
|
||||
public class ReplyBroadcast extends BroadcastReceiver {
|
||||
private Context mContext;
|
||||
private Bundle bundle;
|
||||
private NotificationManager notificationManager;
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
|
||||
final CharSequence message = getReplyMessage(intent);
|
||||
if (message == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
mContext = context;
|
||||
bundle = NotificationIntentAdapter.extractPendingNotificationDataFromIntent(intent);
|
||||
notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
|
||||
String notId = bundle.getString("notId");
|
||||
|
||||
Gson gson = new Gson();
|
||||
Ejson ejson = gson.fromJson(bundle.getString("ejson", "{}"), Ejson.class);
|
||||
|
||||
replyToMessage(ejson, Integer.parseInt(notId), message);
|
||||
}
|
||||
}
|
||||
|
||||
protected void replyToMessage(final Ejson ejson, final int notId, final CharSequence message) {
|
||||
String serverURL = ejson.serverURL();
|
||||
String rid = ejson.rid;
|
||||
|
||||
if (serverURL == null || rid == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final OkHttpClient client = new OkHttpClient();
|
||||
final MediaType JSON = MediaType.parse("application/json; charset=utf-8");
|
||||
|
||||
String json = buildMessage(rid, message.toString());
|
||||
|
||||
CustomPushNotification.clearMessages(notId);
|
||||
|
||||
RequestBody body = RequestBody.create(JSON, json);
|
||||
Request request = new Request.Builder()
|
||||
.header("x-auth-token", ejson.token())
|
||||
.header("x-user-id", ejson.userId())
|
||||
.url(String.format("%s/api/v1/chat.sendMessage", serverURL))
|
||||
.post(body)
|
||||
.build();
|
||||
|
||||
client.newCall(request).enqueue(new okhttp3.Callback() {
|
||||
@Override
|
||||
public void onFailure(Call call, IOException e) {
|
||||
Log.i("RCNotification", String.format("Reply FAILED exception %s", e.getMessage()));
|
||||
onReplyFailed(notificationManager, notId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResponse(Call call, final Response response) throws IOException {
|
||||
if (response.isSuccessful()) {
|
||||
Log.d("RCNotification", "Reply SUCCESS");
|
||||
onReplySuccess(notificationManager, notId);
|
||||
} else {
|
||||
Log.i("RCNotification", String.format("Reply FAILED status %s BODY %s", response.code(), response.body().string()));
|
||||
onReplyFailed(notificationManager, notId);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private String getMessageId() {
|
||||
final String ALPHA_NUMERIC_STRING = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
int count = 17;
|
||||
StringBuilder builder = new StringBuilder();
|
||||
while (count-- != 0) {
|
||||
int character = (int)(Math.random()*ALPHA_NUMERIC_STRING.length());
|
||||
builder.append(ALPHA_NUMERIC_STRING.charAt(character));
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
protected String buildMessage(String rid, String message) {
|
||||
Gson gsonBuilder = new GsonBuilder().create();
|
||||
|
||||
Map msgMap = new HashMap();
|
||||
msgMap.put("_id", getMessageId());
|
||||
msgMap.put("rid", rid);
|
||||
msgMap.put("msg", message);
|
||||
msgMap.put("tmid", null);
|
||||
|
||||
Map msg = new HashMap();
|
||||
msg.put("message", msgMap);
|
||||
|
||||
String json = gsonBuilder.toJson(msg);
|
||||
|
||||
return json;
|
||||
}
|
||||
|
||||
protected void onReplyFailed(NotificationManager notificationManager, int notId) {
|
||||
String CHANNEL_ID = "CHANNEL_ID_REPLY_FAILED";
|
||||
|
||||
final Resources res = mContext.getResources();
|
||||
String packageName = mContext.getPackageName();
|
||||
int smallIconResId = res.getIdentifier("ic_notification", "mipmap", packageName);
|
||||
|
||||
NotificationChannel channel = new NotificationChannel(CHANNEL_ID, CHANNEL_ID, NotificationManager.IMPORTANCE_LOW);
|
||||
notificationManager.createNotificationChannel(channel);
|
||||
Notification notification =
|
||||
new Notification.Builder(mContext, CHANNEL_ID)
|
||||
.setContentTitle("Failed to reply message.")
|
||||
.setSmallIcon(smallIconResId)
|
||||
.build();
|
||||
|
||||
notificationManager.notify(notId, notification);
|
||||
}
|
||||
|
||||
protected void onReplySuccess(NotificationManager notificationManager, int notId) {
|
||||
notificationManager.cancel(notId);
|
||||
}
|
||||
|
||||
private CharSequence getReplyMessage(Intent intent) {
|
||||
Bundle remoteInput = RemoteInput.getResultsFromIntent(intent);
|
||||
if (remoteInput != null) {
|
||||
return remoteInput.getCharSequence(CustomPushNotification.KEY_REPLY);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 638 B |
Before Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 797 B |
Before Width: | Height: | Size: 2.1 KiB |
Before Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 2.4 KiB |
Before Width: | Height: | Size: 498 B |
Before Width: | Height: | Size: 914 B |
Before Width: | Height: | Size: 995 B |
Before Width: | Height: | Size: 928 B |
Before Width: | Height: | Size: 595 B |
Before Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 774 B |
Before Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 775 B |
Before Width: | Height: | Size: 2.0 KiB |
Before Width: | Height: | Size: 2.2 KiB |
Before Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 998 B |
Before Width: | Height: | Size: 3.0 KiB |
Before Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 3.5 KiB |
Before Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 3.0 KiB |
Before Width: | Height: | Size: 3.4 KiB |
Before Width: | Height: | Size: 2.8 KiB |
Before Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 4.8 KiB |
Before Width: | Height: | Size: 2.5 KiB |
Before Width: | Height: | Size: 5.7 KiB |
Before Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 4.0 KiB |
Before Width: | Height: | Size: 4.6 KiB |
Before Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 6.9 KiB |
Before Width: | Height: | Size: 3.3 KiB |
Before Width: | Height: | Size: 7.8 KiB |
|
@ -1,9 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<network-security-config>
|
||||
<base-config cleartextTrafficPermitted="true">
|
||||
<network-security-config xmlns:tools="http://schemas.android.com/tools">
|
||||
<base-config cleartextTrafficPermitted="false">
|
||||
<trust-anchors>
|
||||
<certificates src="system" />
|
||||
<certificates src="user" />
|
||||
<certificates src="user"
|
||||
tools:ignore="AcceptsUserCertificates" />
|
||||
</trust-anchors>
|
||||
</base-config>
|
||||
</network-security-config>
|
|
@ -0,0 +1,75 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="chat.rocket.reactnative">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
|
||||
<application
|
||||
android:name=".MainApplication"
|
||||
android:allowBackup="true"
|
||||
android:label="@string/app_name"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:theme="@style/AppTheme"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
>
|
||||
<activity
|
||||
android:name="com.zoontek.rnbootsplash.RNBootSplashActivity"
|
||||
android:theme="@style/BootTheme">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:label="@string/app_name"
|
||||
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|uiMode"
|
||||
android:launchMode="singleTask"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.DOWNLOAD_COMPLETE"/>
|
||||
</intent-filter>
|
||||
<intent-filter android:label="@string/app_name">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="https" android:host="go.rocket.chat" />
|
||||
<data android:scheme="https" android:host="jitsi.rocket.chat" />
|
||||
<data android:scheme="rocketchat" android:host="room" />
|
||||
<data android:scheme="rocketchat" android:host="auth" />
|
||||
<data android:scheme="rocketchat" android:host="jitsi.rocket.chat" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />
|
||||
<receiver
|
||||
android:name=".ReplyBroadcast"
|
||||
android:enabled="true"
|
||||
android:exported="false" />
|
||||
<receiver
|
||||
android:name=".DismissNotification"
|
||||
android:enabled="true"
|
||||
android:exported="false" >
|
||||
</receiver>
|
||||
<activity
|
||||
android:noHistory="true"
|
||||
android:name=".share.ShareActivity"
|
||||
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
|
||||
android:label="@string/share_extension_name"
|
||||
android:screenOrientation="portrait"
|
||||
android:theme="@style/AppTheme" >
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<action android:name="android.intent.action.SEND_MULTIPLE" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="*/*" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<meta-data
|
||||
android:name="com.bugsnag.android.API_KEY"
|
||||
android:value="${BugsnagAPIKey}" />
|
||||
</application>
|
||||
|
||||
</manifest>
|
|
@ -1,6 +1,7 @@
|
|||
package chat.rocket.reactnative;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
public class Callback {
|
|
@ -0,0 +1,362 @@
|
|||
package chat.rocket.reactnative;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.app.RemoteInput;
|
||||
import android.content.Intent;
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.drawable.Icon;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.app.Person;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.bumptech.glide.load.resource.bitmap.RoundedCorners;
|
||||
import com.bumptech.glide.request.RequestOptions;
|
||||
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.lang.InterruptedException;
|
||||
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.wix.reactnativenotifications.core.AppLaunchHelper;
|
||||
import com.wix.reactnativenotifications.core.AppLifecycleFacade;
|
||||
import com.wix.reactnativenotifications.core.JsIOHelper;
|
||||
import com.wix.reactnativenotifications.core.notification.PushNotification;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.Iterator;
|
||||
|
||||
import static com.wix.reactnativenotifications.Defs.NOTIFICATION_RECEIVED_EVENT_NAME;
|
||||
|
||||
public class CustomPushNotification extends PushNotification {
|
||||
public static ReactApplicationContext reactApplicationContext;
|
||||
final NotificationManager notificationManager;
|
||||
|
||||
public CustomPushNotification(Context context, Bundle bundle, AppLifecycleFacade appLifecycleFacade, AppLaunchHelper appLaunchHelper, JsIOHelper jsIoHelper) {
|
||||
super(context, bundle, appLifecycleFacade, appLaunchHelper, jsIoHelper);
|
||||
reactApplicationContext = new ReactApplicationContext(context);
|
||||
notificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
}
|
||||
|
||||
private static Map<String, List<Bundle>> notificationMessages = new HashMap<String, List<Bundle>>();
|
||||
public static String KEY_REPLY = "KEY_REPLY";
|
||||
public static String NOTIFICATION_ID = "NOTIFICATION_ID";
|
||||
|
||||
public static void clearMessages(int notId) {
|
||||
notificationMessages.remove(Integer.toString(notId));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReceived() throws InvalidNotificationException {
|
||||
Bundle received = mNotificationProps.asBundle();
|
||||
Ejson receivedEjson = new Gson().fromJson(received.getString("ejson", "{}"), Ejson.class);
|
||||
|
||||
if (receivedEjson.notificationType != null && receivedEjson.notificationType.equals("message-id-only")) {
|
||||
notificationLoad(receivedEjson, new Callback() {
|
||||
@Override
|
||||
public void call(@Nullable Bundle bundle) {
|
||||
if (bundle != null) {
|
||||
mNotificationProps = createProps(bundle);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// We should re-read these values since that can be changed by notificationLoad
|
||||
Bundle bundle = mNotificationProps.asBundle();
|
||||
Ejson loadedEjson = new Gson().fromJson(bundle.getString("ejson", "{}"), Ejson.class);
|
||||
String notId = bundle.getString("notId", "1");
|
||||
|
||||
if (notificationMessages.get(notId) == null) {
|
||||
notificationMessages.put(notId, new ArrayList<Bundle>());
|
||||
}
|
||||
|
||||
boolean hasSender = loadedEjson.sender != null;
|
||||
String title = bundle.getString("title");
|
||||
|
||||
// If it has a encrypted message
|
||||
if (loadedEjson.msg != null) {
|
||||
// Override message with the decrypted content
|
||||
String decrypted = Encryption.shared.decryptMessage(loadedEjson, reactApplicationContext);
|
||||
if (decrypted != null) {
|
||||
bundle.putString("message", decrypted);
|
||||
}
|
||||
}
|
||||
|
||||
bundle.putLong("time", new Date().getTime());
|
||||
bundle.putString("username", hasSender ? loadedEjson.sender.username : title);
|
||||
bundle.putString("senderId", hasSender ? loadedEjson.sender._id : "1");
|
||||
bundle.putString("avatarUri", loadedEjson.getAvatarUri());
|
||||
|
||||
notificationMessages.get(notId).add(bundle);
|
||||
postNotification(Integer.parseInt(notId));
|
||||
notifyReceivedToJS();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onOpened() {
|
||||
Bundle bundle = mNotificationProps.asBundle();
|
||||
final String notId = bundle.getString("notId", "1");
|
||||
notificationMessages.remove(notId);
|
||||
digestNotification();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Notification.Builder getNotificationBuilder(PendingIntent intent) {
|
||||
final Notification.Builder notification = new Notification.Builder(mContext);
|
||||
|
||||
Bundle bundle = mNotificationProps.asBundle();
|
||||
String notId = bundle.getString("notId", "1");
|
||||
String title = bundle.getString("title");
|
||||
String message = bundle.getString("message");
|
||||
Boolean notificationLoaded = bundle.getBoolean("notificationLoaded", false);
|
||||
Ejson ejson = new Gson().fromJson(bundle.getString("ejson", "{}"), Ejson.class);
|
||||
|
||||
notification
|
||||
.setContentTitle(title)
|
||||
.setContentText(message)
|
||||
.setContentIntent(intent)
|
||||
.setPriority(Notification.PRIORITY_HIGH)
|
||||
.setDefaults(Notification.DEFAULT_ALL)
|
||||
.setAutoCancel(true);
|
||||
|
||||
Integer notificationId = Integer.parseInt(notId);
|
||||
notificationColor(notification);
|
||||
notificationChannel(notification);
|
||||
notificationIcons(notification, bundle);
|
||||
notificationDismiss(notification, notificationId);
|
||||
|
||||
// if notificationType is null (RC < 3.5) or notificationType is different of message-id-only or notification was loaded successfully
|
||||
if (ejson.notificationType == null || !ejson.notificationType.equals("message-id-only") || notificationLoaded) {
|
||||
notificationStyle(notification, notificationId, bundle);
|
||||
notificationReply(notification, notificationId, bundle);
|
||||
|
||||
// message couldn't be loaded from server (Fallback notification)
|
||||
} else {
|
||||
Gson gson = new Gson();
|
||||
// iterate over the current notification ids to dismiss fallback notifications from same server
|
||||
for (Map.Entry<String, List<Bundle>> bundleList : notificationMessages.entrySet()) {
|
||||
// iterate over the notifications with this id (same host + rid)
|
||||
Iterator iterator = bundleList.getValue().iterator();
|
||||
while (iterator.hasNext()) {
|
||||
Bundle not = (Bundle) iterator.next();
|
||||
// get the notification info
|
||||
Ejson notEjson = gson.fromJson(not.getString("ejson", "{}"), Ejson.class);
|
||||
// if already has a notification from same server
|
||||
if (ejson.serverURL().equals(notEjson.serverURL())) {
|
||||
String id = not.getString("notId");
|
||||
// cancel this notification
|
||||
notificationManager.cancel(Integer.parseInt(id));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return notification;
|
||||
}
|
||||
|
||||
private void notifyReceivedToJS() {
|
||||
mJsIOHelper.sendEventToJS(NOTIFICATION_RECEIVED_EVENT_NAME, mNotificationProps.asBundle(), mAppLifecycleFacade.getRunningReactContext());
|
||||
}
|
||||
|
||||
private Bitmap getAvatar(String uri) {
|
||||
try {
|
||||
return Glide.with(mContext)
|
||||
.asBitmap()
|
||||
.apply(RequestOptions.bitmapTransform(new RoundedCorners(10)))
|
||||
.load(uri)
|
||||
.submit(100, 100)
|
||||
.get();
|
||||
} catch (final ExecutionException | InterruptedException e) {
|
||||
return largeIcon();
|
||||
}
|
||||
}
|
||||
|
||||
private Bitmap largeIcon() {
|
||||
final Resources res = mContext.getResources();
|
||||
String packageName = mContext.getPackageName();
|
||||
int largeIconResId = res.getIdentifier("ic_launcher", "mipmap", packageName);
|
||||
Bitmap largeIconBitmap = BitmapFactory.decodeResource(res, largeIconResId);
|
||||
return largeIconBitmap;
|
||||
}
|
||||
|
||||
private void notificationIcons(Notification.Builder notification, Bundle bundle) {
|
||||
final Resources res = mContext.getResources();
|
||||
String packageName = mContext.getPackageName();
|
||||
|
||||
int smallIconResId = res.getIdentifier("ic_notification", "mipmap", packageName);
|
||||
|
||||
Gson gson = new Gson();
|
||||
Ejson ejson = gson.fromJson(bundle.getString("ejson", "{}"), Ejson.class);
|
||||
|
||||
notification.setSmallIcon(smallIconResId);
|
||||
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
|
||||
notification.setLargeIcon(getAvatar(ejson.getAvatarUri()));
|
||||
}
|
||||
}
|
||||
|
||||
private void notificationChannel(Notification.Builder notification) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
String CHANNEL_ID = "rocketchatrn_channel_01";
|
||||
String CHANNEL_NAME = "All";
|
||||
|
||||
NotificationChannel channel = new NotificationChannel(CHANNEL_ID,
|
||||
CHANNEL_NAME,
|
||||
NotificationManager.IMPORTANCE_DEFAULT);
|
||||
|
||||
final NotificationManager notificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
notificationManager.createNotificationChannel(channel);
|
||||
|
||||
notification.setChannelId(CHANNEL_ID);
|
||||
}
|
||||
}
|
||||
|
||||
private String extractMessage(String message, Ejson ejson) {
|
||||
if (ejson.type != null && !ejson.type.equals("d")) {
|
||||
int pos = message.indexOf(":");
|
||||
int start = pos == -1 ? 0 : pos + 2;
|
||||
return message.substring(start, message.length());
|
||||
}
|
||||
return message;
|
||||
}
|
||||
|
||||
private void notificationColor(Notification.Builder notification) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
notification.setColor(mContext.getColor(R.color.notification_text));
|
||||
}
|
||||
}
|
||||
|
||||
private void notificationStyle(Notification.Builder notification, int notId, Bundle bundle) {
|
||||
List<Bundle> bundles = notificationMessages.get(Integer.toString(notId));
|
||||
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
|
||||
Notification.InboxStyle messageStyle = new Notification.InboxStyle();
|
||||
if (bundles != null) {
|
||||
for (int i = 0; i < bundles.size(); i++) {
|
||||
Bundle data = bundles.get(i);
|
||||
String message = data.getString("message");
|
||||
|
||||
messageStyle.addLine(message);
|
||||
}
|
||||
}
|
||||
|
||||
notification.setStyle(messageStyle);
|
||||
} else {
|
||||
Notification.MessagingStyle messageStyle;
|
||||
|
||||
Gson gson = new Gson();
|
||||
Ejson ejson = gson.fromJson(bundle.getString("ejson", "{}"), Ejson.class);
|
||||
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
|
||||
messageStyle = new Notification.MessagingStyle("");
|
||||
} else {
|
||||
Person sender = new Person.Builder()
|
||||
.setKey("")
|
||||
.setName("")
|
||||
.build();
|
||||
messageStyle = new Notification.MessagingStyle(sender);
|
||||
}
|
||||
|
||||
String title = bundle.getString("title");
|
||||
messageStyle.setConversationTitle(title);
|
||||
|
||||
if (bundles != null) {
|
||||
for (int i = 0; i < bundles.size(); i++) {
|
||||
Bundle data = bundles.get(i);
|
||||
|
||||
long timestamp = data.getLong("time");
|
||||
String message = data.getString("message");
|
||||
String username = data.getString("username");
|
||||
String senderId = data.getString("senderId");
|
||||
String avatarUri = data.getString("avatarUri");
|
||||
|
||||
String m = extractMessage(message, ejson);
|
||||
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
|
||||
messageStyle.addMessage(m, timestamp, username);
|
||||
} else {
|
||||
Bitmap avatar = getAvatar(avatarUri);
|
||||
|
||||
String name = username;
|
||||
if (ejson.senderName != null) {
|
||||
name = ejson.senderName;
|
||||
}
|
||||
|
||||
Person.Builder sender = new Person.Builder()
|
||||
.setKey(senderId)
|
||||
.setName(name);
|
||||
|
||||
if (avatar != null) {
|
||||
sender.setIcon(Icon.createWithBitmap(avatar));
|
||||
}
|
||||
|
||||
Person person = sender.build();
|
||||
|
||||
messageStyle.addMessage(m, timestamp, person);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
notification.setStyle(messageStyle);
|
||||
}
|
||||
}
|
||||
|
||||
private void notificationReply(Notification.Builder notification, int notificationId, Bundle bundle) {
|
||||
String notId = bundle.getString("notId", "1");
|
||||
String ejson = bundle.getString("ejson", "{}");
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N || notId.equals("1") || ejson.equals("{}")) {
|
||||
return;
|
||||
}
|
||||
String label = "Reply";
|
||||
|
||||
final Resources res = mContext.getResources();
|
||||
String packageName = mContext.getPackageName();
|
||||
int smallIconResId = res.getIdentifier("ic_notification", "mipmap", packageName);
|
||||
|
||||
Intent replyIntent = new Intent(mContext, ReplyBroadcast.class);
|
||||
replyIntent.setAction(KEY_REPLY);
|
||||
replyIntent.putExtra("pushNotification", bundle);
|
||||
|
||||
PendingIntent replyPendingIntent = PendingIntent.getBroadcast(mContext, notificationId, replyIntent, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
|
||||
RemoteInput remoteInput = new RemoteInput.Builder(KEY_REPLY)
|
||||
.setLabel(label)
|
||||
.build();
|
||||
|
||||
CharSequence title = label;
|
||||
Notification.Action replyAction = new Notification.Action.Builder(smallIconResId, title, replyPendingIntent)
|
||||
.addRemoteInput(remoteInput)
|
||||
.setAllowGeneratedReplies(true)
|
||||
.build();
|
||||
|
||||
notification
|
||||
.setShowWhen(true)
|
||||
.addAction(replyAction);
|
||||
}
|
||||
|
||||
private void notificationDismiss(Notification.Builder notification, int notificationId) {
|
||||
Intent intent = new Intent(mContext, DismissNotification.class);
|
||||
intent.putExtra(NOTIFICATION_ID, notificationId);
|
||||
|
||||
PendingIntent dismissPendingIntent = PendingIntent.getBroadcast(mContext, notificationId, intent, 0);
|
||||
|
||||
notification.setDeleteIntent(dismissPendingIntent);
|
||||
}
|
||||
|
||||
private void notificationLoad(Ejson ejson, Callback callback) {
|
||||
LoadNotification.load(reactApplicationContext, ejson, callback);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
package chat.rocket.reactnative;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
|
||||
public class DismissNotification extends BroadcastReceiver {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
int notId = intent.getExtras().getInt(CustomPushNotification.NOTIFICATION_ID);
|
||||
CustomPushNotification.clearMessages(notId);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,107 @@
|
|||
package chat.rocket.reactnative;
|
||||
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.bridge.Callback;
|
||||
|
||||
import com.ammarahmed.mmkv.SecureKeystore;
|
||||
import com.tencent.mmkv.MMKV;
|
||||
|
||||
import java.math.BigInteger;
|
||||
|
||||
class RNCallback implements Callback {
|
||||
public void invoke(Object... args) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
class Utils {
|
||||
static public String toHex(String arg) {
|
||||
try {
|
||||
return String.format("%x", new BigInteger(1, arg.getBytes("UTF-8")));
|
||||
} catch (Exception e) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class Ejson {
|
||||
String host;
|
||||
String rid;
|
||||
String type;
|
||||
Sender sender;
|
||||
String messageId;
|
||||
String notificationType;
|
||||
String senderName;
|
||||
String msg;
|
||||
|
||||
private MMKV mmkv;
|
||||
|
||||
private String TOKEN_KEY = "reactnativemeteor_usertoken-";
|
||||
|
||||
public Ejson() {
|
||||
ReactApplicationContext reactApplicationContext = CustomPushNotification.reactApplicationContext;
|
||||
|
||||
// Start MMKV container
|
||||
MMKV.initialize(reactApplicationContext);
|
||||
SecureKeystore secureKeystore = new SecureKeystore(reactApplicationContext);
|
||||
|
||||
// https://github.com/ammarahm-ed/react-native-mmkv-storage/blob/master/src/loader.js#L31
|
||||
String alias = Utils.toHex("com.MMKV.default");
|
||||
|
||||
// Retrieve container password
|
||||
secureKeystore.getSecureKey(alias, new RNCallback() {
|
||||
@Override
|
||||
public void invoke(Object... args) {
|
||||
String error = (String) args[0];
|
||||
if (error == null) {
|
||||
String password = (String) args[1];
|
||||
mmkv = MMKV.mmkvWithID("default", MMKV.SINGLE_PROCESS_MODE, password);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public String getAvatarUri() {
|
||||
if (type == null) {
|
||||
return null;
|
||||
}
|
||||
return serverURL() + "/avatar/" + this.sender.username + "?rc_token=" + token() + "&rc_uid=" + userId();
|
||||
}
|
||||
|
||||
public String token() {
|
||||
String userId = userId();
|
||||
if (mmkv != null && userId != null) {
|
||||
return mmkv.decodeString(TOKEN_KEY.concat(userId));
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
public String userId() {
|
||||
String serverURL = serverURL();
|
||||
if (mmkv != null && serverURL != null) {
|
||||
return mmkv.decodeString(TOKEN_KEY.concat(serverURL));
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
public String privateKey() {
|
||||
String serverURL = serverURL();
|
||||
if (mmkv != null && serverURL != null) {
|
||||
return mmkv.decodeString(serverURL.concat("-RC_E2E_PRIVATE_KEY"));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public String serverURL() {
|
||||
String url = this.host;
|
||||
if (url != null && url.endsWith("/")) {
|
||||
url = url.substring(0, url.length() - 1);
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
public class Sender {
|
||||
String username;
|
||||
String _id;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,210 @@
|
|||
package chat.rocket.reactnative;
|
||||
|
||||
import android.util.Log;
|
||||
import android.util.Base64;
|
||||
import android.database.Cursor;
|
||||
|
||||
import com.pedrouid.crypto.RSA;
|
||||
import com.pedrouid.crypto.RCTAes;
|
||||
import com.pedrouid.crypto.RCTRsaUtils;
|
||||
import com.pedrouid.crypto.Util;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
|
||||
import com.facebook.react.bridge.Promise;
|
||||
import com.facebook.react.bridge.Arguments;
|
||||
import com.facebook.react.bridge.ReactMethod;
|
||||
import com.facebook.react.bridge.ReadableMap;
|
||||
import com.facebook.react.bridge.WritableMap;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
||||
|
||||
import com.nozbe.watermelondb.Database;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.security.SecureRandom;
|
||||
|
||||
class Message {
|
||||
String _id;
|
||||
String userId;
|
||||
String text;
|
||||
|
||||
Message(String id, String userId, String text) {
|
||||
this._id = id;
|
||||
this.userId = userId;
|
||||
this.text = text;
|
||||
}
|
||||
}
|
||||
|
||||
class PrivateKey {
|
||||
String d;
|
||||
String dp;
|
||||
String dq;
|
||||
String e;
|
||||
String n;
|
||||
String p;
|
||||
String q;
|
||||
String qi;
|
||||
}
|
||||
|
||||
class RoomKey {
|
||||
String k;
|
||||
}
|
||||
|
||||
class Room {
|
||||
String e2eKey;
|
||||
Boolean encrypted;
|
||||
|
||||
Room(String e2eKey, Boolean encrypted) {
|
||||
this.e2eKey = e2eKey;
|
||||
this.encrypted = encrypted;
|
||||
}
|
||||
}
|
||||
|
||||
class Encryption {
|
||||
private Gson gson = new Gson();
|
||||
private String E2ERoomKey;
|
||||
private String keyId;
|
||||
|
||||
public static Encryption shared = new Encryption();
|
||||
private ReactApplicationContext reactContext;
|
||||
|
||||
public Room readRoom(final Ejson ejson) {
|
||||
Database database = new Database(ejson.serverURL().replace("https://", "") + "-experimental.db", reactContext);
|
||||
String[] query = {ejson.rid};
|
||||
Cursor cursor = database.rawQuery("select * from subscriptions where id == ? limit 1", query);
|
||||
|
||||
// Room not found
|
||||
if (cursor.getCount() == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
cursor.moveToFirst();
|
||||
String e2eKey = cursor.getString(cursor.getColumnIndex("e2e_key"));
|
||||
Boolean encrypted = cursor.getInt(cursor.getColumnIndex("encrypted")) > 0;
|
||||
cursor.close();
|
||||
|
||||
return new Room(e2eKey, encrypted);
|
||||
}
|
||||
|
||||
public String readUserKey(final Ejson ejson) throws Exception {
|
||||
String privateKey = ejson.privateKey();
|
||||
if (privateKey == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
PrivateKey privKey = gson.fromJson(privateKey, PrivateKey.class);
|
||||
|
||||
WritableMap jwk = Arguments.createMap();
|
||||
jwk.putString("n", privKey.n);
|
||||
jwk.putString("e", privKey.e);
|
||||
jwk.putString("d", privKey.d);
|
||||
jwk.putString("p", privKey.p);
|
||||
jwk.putString("q", privKey.q);
|
||||
jwk.putString("dp", privKey.dp);
|
||||
jwk.putString("dq", privKey.dq);
|
||||
jwk.putString("qi", privKey.qi);
|
||||
|
||||
return new RCTRsaUtils().jwkToPrivatePkcs1(jwk);
|
||||
}
|
||||
|
||||
public String decryptRoomKey(final String e2eKey, final Ejson ejson) throws Exception {
|
||||
String key = e2eKey.substring(12, e2eKey.length());
|
||||
keyId = e2eKey.substring(0, 12);
|
||||
|
||||
String userKey = readUserKey(ejson);
|
||||
if (userKey == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
RSA rsa = new RSA();
|
||||
rsa.setPrivateKey(userKey);
|
||||
String decrypted = rsa.decrypt(key);
|
||||
|
||||
RoomKey roomKey = gson.fromJson(decrypted, RoomKey.class);
|
||||
byte[] decoded = Base64.decode(roomKey.k, Base64.NO_PADDING | Base64.NO_WRAP | Base64.URL_SAFE);
|
||||
|
||||
return Util.bytesToHex(decoded);
|
||||
}
|
||||
|
||||
public String decryptMessage(final Ejson ejson, final ReactApplicationContext reactContext) {
|
||||
try {
|
||||
this.reactContext = reactContext;
|
||||
|
||||
Room room = readRoom(ejson);
|
||||
if (room == null || room.e2eKey == null) {
|
||||
return null;
|
||||
}
|
||||
String e2eKey = decryptRoomKey(room.e2eKey, ejson);
|
||||
if (e2eKey == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String message = ejson.msg;
|
||||
String msg = message.substring(12, message.length());
|
||||
byte[] msgData = Base64.decode(msg, Base64.NO_WRAP);
|
||||
|
||||
String b64 = Base64.encodeToString(Arrays.copyOfRange(msgData, 16, msgData.length), Base64.DEFAULT);
|
||||
|
||||
String decrypted = RCTAes.decrypt(b64, e2eKey, Util.bytesToHex(Arrays.copyOfRange(msgData, 0, 16)));
|
||||
byte[] data = Base64.decode(decrypted, Base64.NO_WRAP);
|
||||
Message m = gson.fromJson(new String(data, "UTF-8"), Message.class);
|
||||
|
||||
return m.text;
|
||||
} catch (Exception e) {
|
||||
Log.d("[ROCKETCHAT][ENCRYPTION]", Log.getStackTraceString(e));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public String encryptMessage(final String message, final String id, final Ejson ejson) {
|
||||
try {
|
||||
Room room = readRoom(ejson);
|
||||
if (room == null || !room.encrypted || room.e2eKey == null) {
|
||||
return message;
|
||||
}
|
||||
|
||||
String e2eKey = decryptRoomKey(room.e2eKey, ejson);
|
||||
if (e2eKey == null) {
|
||||
return message;
|
||||
}
|
||||
|
||||
Message m = new Message(id, ejson.userId(), message);
|
||||
String cypher = gson.toJson(m);
|
||||
|
||||
SecureRandom random = new SecureRandom();
|
||||
byte[] bytes = new byte[16];
|
||||
random.nextBytes(bytes);
|
||||
|
||||
String encrypted = RCTAes.encrypt(Base64.encodeToString(cypher.getBytes("UTF-8"), Base64.NO_WRAP), e2eKey, Util.bytesToHex(bytes));
|
||||
byte[] data = Base64.decode(encrypted, Base64.NO_WRAP);
|
||||
|
||||
return keyId + Base64.encodeToString(concat(bytes, data), Base64.NO_WRAP);
|
||||
} catch (Exception e) {
|
||||
Log.d("[ROCKETCHAT][ENCRYPTION]", Log.getStackTraceString(e));
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
static byte[] concat(byte[]... arrays) {
|
||||
// Determine the length of the result array
|
||||
int totalLength = 0;
|
||||
for (int i = 0; i < arrays.length; i++) {
|
||||
totalLength += arrays[i].length;
|
||||
}
|
||||
|
||||
// create the result array
|
||||
byte[] result = new byte[totalLength];
|
||||
|
||||
// copy the source arrays into the result array
|
||||
int currentIndex = 0;
|
||||
for (int i = 0; i < arrays.length; i++) {
|
||||
System.arraycopy(arrays[i], 0, result, currentIndex, arrays[i].length);
|
||||
currentIndex += arrays[i].length;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,100 @@
|
|||
package chat.rocket.reactnative;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.content.Context;
|
||||
|
||||
import okhttp3.Call;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.HttpUrl;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
import okhttp3.Interceptor;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
|
||||
class JsonResponse {
|
||||
Data data;
|
||||
|
||||
class Data {
|
||||
Notification notification;
|
||||
|
||||
class Notification {
|
||||
String notId;
|
||||
String title;
|
||||
String text;
|
||||
Payload payload;
|
||||
|
||||
class Payload {
|
||||
String host;
|
||||
String rid;
|
||||
String type;
|
||||
Sender sender;
|
||||
String messageId;
|
||||
String notificationType;
|
||||
String name;
|
||||
String messageType;
|
||||
|
||||
class Sender {
|
||||
String _id;
|
||||
String username;
|
||||
String name;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class LoadNotification {
|
||||
private static int RETRY_COUNT = 0;
|
||||
private static int[] TIMEOUT = new int[]{0, 1, 3, 5, 10};
|
||||
private static String TOKEN_KEY = "reactnativemeteor_usertoken-";
|
||||
|
||||
public static void load(ReactApplicationContext reactApplicationContext, final Ejson ejson, Callback callback) {
|
||||
final OkHttpClient client = new OkHttpClient();
|
||||
HttpUrl.Builder url = HttpUrl.parse(ejson.serverURL().concat("/api/v1/push.get")).newBuilder();
|
||||
|
||||
Request request = new Request.Builder()
|
||||
.header("x-user-id", ejson.userId())
|
||||
.header("x-auth-token", ejson.token())
|
||||
.url(url.addQueryParameter("id", ejson.messageId).build())
|
||||
.build();
|
||||
|
||||
runRequest(client, request, callback);
|
||||
}
|
||||
|
||||
private static void runRequest(OkHttpClient client, Request request, Callback callback) {
|
||||
try {
|
||||
Thread.sleep(TIMEOUT[RETRY_COUNT] * 1000);
|
||||
|
||||
Response response = client.newCall(request).execute();
|
||||
String body = response.body().string();
|
||||
if (!response.isSuccessful()) {
|
||||
throw new Exception("Error");
|
||||
}
|
||||
|
||||
Gson gson = new Gson();
|
||||
JsonResponse json = gson.fromJson(body, JsonResponse.class);
|
||||
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putString("notId", json.data.notification.notId);
|
||||
bundle.putString("title", json.data.notification.title);
|
||||
bundle.putString("message", json.data.notification.text);
|
||||
bundle.putString("ejson", gson.toJson(json.data.notification.payload));
|
||||
bundle.putBoolean("notificationLoaded", true);
|
||||
|
||||
callback.call(bundle);
|
||||
|
||||
} catch (Exception e) {
|
||||
if (RETRY_COUNT <= TIMEOUT.length) {
|
||||
RETRY_COUNT++;
|
||||
runRequest(client, request, callback);
|
||||
} else {
|
||||
callback.call(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,165 @@
|
|||
package chat.rocket.reactnative;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.RemoteInput;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.res.Resources;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import okhttp3.Call;
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.Response;
|
||||
|
||||
import com.wix.reactnativenotifications.core.NotificationIntentAdapter;
|
||||
|
||||
public class ReplyBroadcast extends BroadcastReceiver {
|
||||
private Context mContext;
|
||||
private Bundle bundle;
|
||||
private NotificationManager notificationManager;
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
|
||||
final CharSequence message = getReplyMessage(intent);
|
||||
if (message == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
mContext = context;
|
||||
bundle = NotificationIntentAdapter.extractPendingNotificationDataFromIntent(intent);
|
||||
notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
|
||||
String notId = bundle.getString("notId");
|
||||
|
||||
Gson gson = new Gson();
|
||||
Ejson ejson = gson.fromJson(bundle.getString("ejson", "{}"), Ejson.class);
|
||||
|
||||
replyToMessage(ejson, Integer.parseInt(notId), message);
|
||||
}
|
||||
}
|
||||
|
||||
protected void replyToMessage(final Ejson ejson, final int notId, final CharSequence message) {
|
||||
String serverURL = ejson.serverURL();
|
||||
String rid = ejson.rid;
|
||||
|
||||
if (serverURL == null || rid == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final OkHttpClient client = new OkHttpClient();
|
||||
final MediaType JSON = MediaType.parse("application/json; charset=utf-8");
|
||||
|
||||
String json = buildMessage(rid, message.toString(), ejson);
|
||||
|
||||
CustomPushNotification.clearMessages(notId);
|
||||
|
||||
RequestBody body = RequestBody.create(JSON, json);
|
||||
Request request = new Request.Builder()
|
||||
.header("x-auth-token", ejson.token())
|
||||
.header("x-user-id", ejson.userId())
|
||||
.url(String.format("%s/api/v1/chat.sendMessage", serverURL))
|
||||
.post(body)
|
||||
.build();
|
||||
|
||||
client.newCall(request).enqueue(new okhttp3.Callback() {
|
||||
@Override
|
||||
public void onFailure(Call call, IOException e) {
|
||||
Log.i("RCNotification", String.format("Reply FAILED exception %s", e.getMessage()));
|
||||
onReplyFailed(notificationManager, notId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResponse(Call call, final Response response) throws IOException {
|
||||
if (response.isSuccessful()) {
|
||||
Log.d("RCNotification", "Reply SUCCESS");
|
||||
onReplySuccess(notificationManager, notId);
|
||||
} else {
|
||||
Log.i("RCNotification", String.format("Reply FAILED status %s BODY %s", response.code(), response.body().string()));
|
||||
onReplyFailed(notificationManager, notId);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private String getMessageId() {
|
||||
final String ALPHA_NUMERIC_STRING = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
int count = 17;
|
||||
StringBuilder builder = new StringBuilder();
|
||||
while (count-- != 0) {
|
||||
int character = (int) (Math.random() * ALPHA_NUMERIC_STRING.length());
|
||||
builder.append(ALPHA_NUMERIC_STRING.charAt(character));
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
protected String buildMessage(String rid, String message, Ejson ejson) {
|
||||
Gson gsonBuilder = new GsonBuilder().create();
|
||||
|
||||
String id = getMessageId();
|
||||
|
||||
String msg = Encryption.shared.encryptMessage(message, id, ejson);
|
||||
|
||||
Map msgMap = new HashMap();
|
||||
msgMap.put("_id", id);
|
||||
msgMap.put("rid", rid);
|
||||
msgMap.put("msg", msg);
|
||||
if (msg != message) {
|
||||
msgMap.put("t", "e2e");
|
||||
}
|
||||
msgMap.put("tmid", null);
|
||||
|
||||
Map m = new HashMap();
|
||||
m.put("message", msgMap);
|
||||
|
||||
String json = gsonBuilder.toJson(m);
|
||||
|
||||
return json;
|
||||
}
|
||||
|
||||
protected void onReplyFailed(NotificationManager notificationManager, int notId) {
|
||||
String CHANNEL_ID = "CHANNEL_ID_REPLY_FAILED";
|
||||
|
||||
final Resources res = mContext.getResources();
|
||||
String packageName = mContext.getPackageName();
|
||||
int smallIconResId = res.getIdentifier("ic_notification", "mipmap", packageName);
|
||||
|
||||
NotificationChannel channel = new NotificationChannel(CHANNEL_ID, CHANNEL_ID, NotificationManager.IMPORTANCE_LOW);
|
||||
notificationManager.createNotificationChannel(channel);
|
||||
Notification notification =
|
||||
new Notification.Builder(mContext, CHANNEL_ID)
|
||||
.setContentTitle("Failed to reply message.")
|
||||
.setSmallIcon(smallIconResId)
|
||||
.build();
|
||||
|
||||
notificationManager.notify(notId, notification);
|
||||
}
|
||||
|
||||
protected void onReplySuccess(NotificationManager notificationManager, int notId) {
|
||||
notificationManager.cancel(notId);
|
||||
}
|
||||
|
||||
private CharSequence getReplyMessage(Intent intent) {
|
||||
Bundle remoteInput = RemoteInput.getResultsFromIntent(intent);
|
||||
if (remoteInput != null) {
|
||||
return remoteInput.getCharSequence(CustomPushNotification.KEY_REPLY);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -17,15 +17,18 @@ buildscript {
|
|||
url 'https://maven.fabric.io/public'
|
||||
}
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:3.5.3'
|
||||
classpath 'com.google.gms:google-services:4.2.0'
|
||||
classpath 'com.google.firebase:firebase-crashlytics-gradle:2.0.0'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
classpath 'com.bugsnag:bugsnag-android-gradle-plugin:4.+'
|
||||
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
// in the individual module build.gradle files
|
||||
def taskRequests = getGradle().getStartParameter().getTaskRequests().toString().toLowerCase()
|
||||
def isPlay = !taskRequests.contains("foss")
|
||||
|
||||
dependencies {
|
||||
if (isPlay) {
|
||||
classpath 'com.google.gms:google-services:4.2.0'
|
||||
classpath 'com.google.firebase:firebase-crashlytics-gradle:2.0.0'
|
||||
classpath 'com.bugsnag:bugsnag-android-gradle-plugin:4.+'
|
||||
}
|
||||
classpath 'com.android.tools.build:gradle:3.5.3'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -16,21 +16,26 @@
|
|||
default_platform(:android)
|
||||
|
||||
platform :android do
|
||||
desc "Build App for development"
|
||||
lane :build do
|
||||
gradle(task: "assembleDebug")
|
||||
desc "Play build for development"
|
||||
lane :playBuild do
|
||||
gradle(task: "assemblePlayDebug")
|
||||
end
|
||||
|
||||
desc "Build App for release"
|
||||
lane :release do
|
||||
gradle(task: "bundleRelease")
|
||||
desc "Foss build for release"
|
||||
lane :fossRelease do
|
||||
gradle(task: "assembleFossRelease")
|
||||
end
|
||||
|
||||
desc "Upload App to Play store"
|
||||
lane :alpha do
|
||||
desc "Play build for release"
|
||||
lane :playRelease do
|
||||
gradle(task: "bundlePlayRelease")
|
||||
end
|
||||
|
||||
desc "Upload App to Play Store Internal"
|
||||
lane :beta do
|
||||
upload_to_play_store(
|
||||
track: 'alpha',
|
||||
aab: 'android/app/build/outputs/bundle/release/app-release.aab'
|
||||
track: 'internal',
|
||||
aab: 'android/app/build/outputs/bundle/playRelease/app-play-release.aab'
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -16,19 +16,24 @@ or alternatively using `brew cask install fastlane`
|
|||
|
||||
# Available Actions
|
||||
## Android
|
||||
### android build
|
||||
### android playBuild
|
||||
```
|
||||
fastlane android build
|
||||
fastlane android playBuild
|
||||
```
|
||||
Build App for development
|
||||
### android release
|
||||
Play build for development
|
||||
### android fossRelease
|
||||
```
|
||||
fastlane android release
|
||||
fastlane android fossRelease
|
||||
```
|
||||
Build App for release
|
||||
### android alpha
|
||||
Foss build for release
|
||||
### android playRelease
|
||||
```
|
||||
fastlane android alpha
|
||||
fastlane android playRelease
|
||||
```
|
||||
Play build for release
|
||||
### android playAlpha
|
||||
```
|
||||
fastlane android playAlpha
|
||||
```
|
||||
Upload App to Play store
|
||||
|
||||
|
|
|
@ -26,7 +26,7 @@ android.useAndroidX=true
|
|||
# Automatically convert third-party libraries to use AndroidX
|
||||
android.enableJetifier=true
|
||||
APPLICATIONID=chat.rocket.reactnative
|
||||
VERSIONNAME=4.9.0
|
||||
VERSIONNAME=4.11.0
|
||||
VERSIONCODE=1
|
||||
BugsnagAPIKey=
|
||||
KEYSTORE=my-upload-key.keystore
|
||||
|
|
|
@ -53,6 +53,7 @@ export const SNIPPETED_MESSAGES = createRequestTypes('SNIPPETED_MESSAGES', ['OPE
|
|||
export const DEEP_LINKING = createRequestTypes('DEEP_LINKING', ['OPEN']);
|
||||
export const SORT_PREFERENCES = createRequestTypes('SORT_PREFERENCES', ['SET_ALL', 'SET']);
|
||||
export const TOGGLE_CRASH_REPORT = 'TOGGLE_CRASH_REPORT';
|
||||
export const TOGGLE_ANALYTICS_EVENTS = 'TOGGLE_ANALYTICS_EVENTS';
|
||||
export const SET_CUSTOM_EMOJIS = 'SET_CUSTOM_EMOJIS';
|
||||
export const SET_ACTIVE_USERS = 'SET_ACTIVE_USERS';
|
||||
export const USERS_TYPING = createRequestTypes('USERS_TYPING', ['ADD', 'REMOVE', 'CLEAR']);
|
||||
|
@ -66,3 +67,5 @@ export const INVITE_LINKS = createRequestTypes('INVITE_LINKS', [
|
|||
]);
|
||||
export const SETTINGS = createRequestTypes('SETTINGS', ['CLEAR', 'ADD']);
|
||||
export const APP_STATE = createRequestTypes('APP_STATE', ['FOREGROUND', 'BACKGROUND']);
|
||||
export const ENTERPRISE_MODULES = createRequestTypes('ENTERPRISE_MODULES', ['CLEAR', 'SET']);
|
||||
export const ENCRYPTION = createRequestTypes('ENCRYPTION', ['INIT', 'STOP', 'DECODE_KEY', 'SET_BANNER']);
|
||||
|
|