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
This commit is contained in:
Diego Mello 2020-10-05 14:21:38 -03:00
commit 5e633e550e
19878 changed files with 3212523 additions and 3145 deletions

View File

@ -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

View File

@ -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. -->

View File

@ -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.

View File

@ -0,0 +1,3 @@
export default {
BuildConfigs: null
};

View File

@ -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 [

View File

@ -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

View File

@ -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'
}

View File

@ -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>

View File

@ -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>

View File

@ -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);
}
}

View File

@ -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) {
}
}

View File

@ -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;
}
}

View File

@ -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) {
}
}

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 367 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 233 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 508 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 897 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<item name="splashBackground" type="color">#000000</item>
</resources>

View File

@ -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>

View File

@ -0,0 +1,4 @@
<resources>
<string name="app_name">Rocket.Chat</string>
<string name="share_extension_name">Rocket.Chat</string>
</resources>

View File

@ -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>

View File

@ -67,9 +67,6 @@
<data android:mimeType="*/*" />
</intent-filter>
</activity>
<meta-data
android:name="com.bugsnag.android.API_KEY"
android:value="${BugsnagAPIKey}" />
</application>
</manifest>

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}
}
}

View File

@ -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);
}
}
/**

View File

@ -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));

View File

@ -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;
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 638 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 797 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 498 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 914 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 995 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 928 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 595 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 774 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 775 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 998 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

View File

@ -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>

View File

@ -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>

View File

@ -1,6 +1,7 @@
package chat.rocket.reactnative;
import android.os.Bundle;
import androidx.annotation.Nullable;
public class Callback {

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}
}
}

View File

@ -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;
}
}

View File

@ -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"
}
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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']);

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