Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Develop #882

Merged
merged 7 commits into from
Sep 7, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/backend/src/mobileBackendManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ import { ConnectionsManager } from './libp2p/connectionsManager'
import { Command } from 'commander'

export const runBackend = async (): Promise<any> => {
// Enable triggering push notifications
process.env['BACKEND'] = 'mobile'
process.env['CONNECTION_TIME'] = new Date().getTime().toString()

const program = new Command()

program
Expand Down
11 changes: 10 additions & 1 deletion packages/backend/src/storage/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ import {
DownloadProgress,
DownloadState,
imagesExtensions,
User
User,
BASE_NOTIFICATION_CHANNEL
} from '@quiet/state-manager'
import * as IPFS from 'ipfs-core'
import Libp2p from 'libp2p'
Expand Down Expand Up @@ -305,6 +306,14 @@ export class Storage {
this.io.loadMessages({
messages: [entry.payload.value]
})
// Display push notifications on mobile
if (process.env.BACKEND === 'mobile') {
const message = entry.payload.value
// Do not notify about old messages
if (parseInt(message.createdAt) < parseInt(process.env.CONNECTION_TIME)) return
const bridge = require('rn-bridge')
bridge.channel.send(BASE_NOTIFICATION_CHANNEL, JSON.stringify(message))
}
})
db.events.on('replicated', async address => {
log('Replicated.', address)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
package com.zbaymobile;

import android.app.Application;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.content.Context;
import android.os.Build;

import com.facebook.react.PackageList;
import com.facebook.react.ReactApplication;
import com.facebook.react.ReactInstanceManager;
import com.facebook.react.ReactNativeHost;
import com.facebook.react.ReactPackage;
import com.facebook.soloader.SoLoader;
import com.zbaymobile.Utils.Const;

import java.lang.reflect.InvocationTargetException;
import java.util.List;

Expand All @@ -29,6 +35,7 @@ protected List<ReactPackage> getPackages() {
// Packages that cannot be autolinked yet can be added manually here, for example:
// packages.add(new MyReactNativePackage());
packages.add(new TorModulePackage());
packages.add(new NotificationModulePackage());
return packages;
}

Expand All @@ -49,6 +56,7 @@ public void onCreate() {
SoLoader.init(this, /* native exopackage */ false);
initializeFlipper(this, getReactNativeHost().getReactInstanceManager());
PACKAGE_NAME = getApplicationContext().getPackageName();
createNotificationChannel();
}

/**
Expand Down Expand Up @@ -81,4 +89,20 @@ private static void initializeFlipper(
}
}
}

private void createNotificationChannel() {
// Create the NotificationChannel, but only on API 26+ because
// the NotificationChannel class is new and not in the support library
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
CharSequence name = getString(R.string.notification_channel_name);
String description = getString(R.string.notification_channel_description);
int importance = NotificationManager.IMPORTANCE_HIGH;
NotificationChannel channel = new NotificationChannel(Const.INCOMING_MESSAGES_CHANNEL_ID, name, importance);
channel.setDescription(description);
// Register the channel with the system; you can't change the importance
// or other notification behaviors after this
NotificationManager notificationManager = getSystemService(NotificationManager.class);
notificationManager.createNotificationChannel(channel);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package com.zbaymobile;

import android.app.ActivityManager;
import android.content.Context;
import android.util.Log;

import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.zbaymobile.Utils.Const;

import androidx.annotation.NonNull;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.util.List;
import java.util.concurrent.ThreadLocalRandom;

public class NotificationModule extends ReactContextBaseJavaModule {

private static final String SYSTEM_CHANNEL = "_SYSTEM_";
private static final String BASE_NOTIFICATION_CHANNEL = "_BASE_NOTIFICATION_";
private static final String RICH_NOTIFICATION_CHANNEL = "_RICH_NOTIFICATION_";

private static ReactApplicationContext reactContext;

@NonNull
@Override
public String getName() {
return "NotificationModule";
}

public NotificationModule(ReactApplicationContext reactContext) {
super(reactContext);
this.reactContext = reactContext;
}

@ReactMethod
public static void notify(String channelName, String message) {
if (channelName.equals(SYSTEM_CHANNEL)) return; // Ignore system messages
if (!channelName.equals(RICH_NOTIFICATION_CHANNEL) && isAppOnForeground()) return; // Only RICH_NOTIFICATION can be shown in foreground

JSONObject json; // Message payload

String title = "Quiet";
String text = "";

try {
json = new JSONObject(message);
} catch (JSONException e) {
Log.e("NOTIFICATION", "unexpected JSON exception", e);
return;
}

if (channelName.equals(BASE_NOTIFICATION_CHANNEL)) {
try {
JSONArray payload = new JSONArray(json.getString("payload"));
JSONObject data = new JSONObject(payload.getString(0));

String channelAddress = data.getString("channelAddress");
title = "Quiet";
text = String.format("You have a message in #%s", channelAddress);
} catch (JSONException e) {
Log.e("NOTIFICATION", "incorrect BASE_NOTIFICATION payload", e);
return;
}
}

if(channelName.equals(RICH_NOTIFICATION_CHANNEL)) {
try {
JSONObject data = json;

String channelAddress = data.getString("channelAddress");
String messageContent = data.getString("message");
title = String.format("#%s", channelAddress);
text = truncate(messageContent, 32);
} catch (JSONException e) {
Log.e("NOTIFICATION", "incorrect RICH_NOTIFICATION payload", e);
return;
}
}

NotificationCompat.Builder builder = new NotificationCompat.Builder(reactContext.getApplicationContext(), Const.INCOMING_MESSAGES_CHANNEL_ID)
.setSmallIcon(R.drawable.ic_notification)
.setContentTitle(title)
.setContentText(text)
.setDefaults(NotificationCompat.DEFAULT_ALL)
.setPriority(NotificationCompat.PRIORITY_HIGH);

Integer notificationId = ThreadLocalRandom.current().nextInt(0, 9000 + 1);

NotificationManagerCompat notificationManager = NotificationManagerCompat.from(reactContext.getApplicationContext());
notificationManager.notify(notificationId, builder.build());
}

private static boolean isAppOnForeground() {
ActivityManager activityManager = (ActivityManager) reactContext.getSystemService(Context.ACTIVITY_SERVICE);
List<ActivityManager.RunningAppProcessInfo> appProcesses = activityManager.getRunningAppProcesses();
if (appProcesses == null) {
return false;
}
final String packageName = reactContext.getPackageName();
for (ActivityManager.RunningAppProcessInfo appProcess : appProcesses) {
if (appProcess.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND && appProcess.processName.equals(packageName)) {
return true;
}
}
return false;
}

private static String truncate(String message, Integer length) {
if (message.length() > length) {
return String.format("%s...", message.substring(0, length));
} else {
return message;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.zbaymobile;

import java.util.Arrays;
import java.util.Collections;
import java.util.List;

import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;
import com.facebook.react.bridge.JavaScriptModule;

public class NotificationModulePackage implements ReactPackage {
@Override
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
return Arrays.<NativeModule>asList(new NotificationModule(reactContext));
}

// Deprecated from RN 0.47
public List<Class<? extends JavaScriptModule>> createJSModules() {
return Collections.emptyList();
}

@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
return Collections.emptyList();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,7 @@ object Const {
const val TAG_TOR = "TOR"
const val TAG_TOR_ERR = "TOR_ERR"

const val INCOMING_MESSAGES_CHANNEL_ID = "incoming.messages.channel"

val SERVICE_ACTION_EXECUTE = "$PACKAGE_NAME.service.execute"
}
4 changes: 4 additions & 0 deletions packages/mobile/android/app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,8 @@

<!-- Universal -->
<string name="close">close</string>

<!-- Notification -->
<string name="notification_channel_name">Incoming messages</string>
<string name="notification_channel_description">Inform the user about incoming messages</string>
</resources>
14 changes: 14 additions & 0 deletions packages/mobile/enable-notifications.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
--- node_modules/nodejs-mobile-react-native/android/src/main/cpp/native-lib.cpp.backup 2022-08-31 11:49:47.331103861 +0200
+++ node_modules/nodejs-mobile-react-native/android/src/main/cpp/native-lib.cpp 2022-08-31 11:50:07.430981315 +0200
@@ -67,9 +67,9 @@
void rcv_message(const char* channel_name, const char* msg) {
JNIEnv *env=cacheEnvPointer;
if(!env) return;
- jclass cls2 = env->FindClass("com/janeasystems/rn_nodejs_mobile/RNNodeJsMobileModule"); // try to find the class
+ jclass cls2 = env->FindClass("com/zbaymobile/NotificationModule"); // try to find the class
if(cls2 != nullptr) {
- jmethodID m_sendMessage = env->GetStaticMethodID(cls2, "sendMessageToApplication", "(Ljava/lang/String;Ljava/lang/String;)V"); // find method
+ jmethodID m_sendMessage = env->GetStaticMethodID(cls2, "notify", "(Ljava/lang/String;Ljava/lang/String;)V"); // find method
if(m_sendMessage != nullptr) {
jstring java_channel_name=env->NewStringUTF(channel_name);
jstring java_msg=env->NewStringUTF(msg);
3 changes: 2 additions & 1 deletion packages/mobile/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@
"provide-backend-assets": "rm -rf ./nodejs-assets/nodejs-project/* && rf-lerna zipbundle @quiet/backend --target ./nodejs-assets/nodejs-project/backend.zip && cd ./nodejs-assets/nodejs-project/ && unzip ./backend.zip && rm -rf ./backend.zip && cp ../parse-duration.patch ./ && patch -p0 --forward --binary < ./parse-duration.patch || true && cp ../libp2p.patch ./ && patch -p0 --forward --binary < ./libp2p.patch || true && cp ../orbit-db-keystore.patch ./ && patch -p0 --forward --binary < ./orbit-db-keystore.patch || true && cp ../orbit-db-storage-adapter.patch ./ && patch -p0 --forward --binary < ./orbit-db-storage-adapter.patch || true && rm -rf ./node_modules/electron && rm -rf ./node_modules/wrtc",
"enable-backend-logging-android": "node -e \"if (process.env.NODE_ENV !== 'production'){process.exit(1)} \" || patch -p0 --forward --binary < ./enable-backend-logs-android.patch || true",
"enable-backend-logging-ios": "node -e \"if (process.env.NODE_ENV !== 'production'){process.exit(1)} \" || patch -p0 --forward --binary < ./enable-backend-logs-ios.patch || true",
"enable-notifications": "patch -p0 --forward --binary < ./enable-notifications.patch || true",
"patch-dependencies": "node -e \"if (process.env.NODE_ENV !== 'production'){process.exit(1)} \" || patch -d ../state-manager -p0 < ./factory-girl.patch || true && patch -d ../state-manager -p0 < ./state-manager.patch || true",
"prepare": "npm run provide-backend-assets && npm run enable-backend-logging-android && npm run enable-backend-logging-ios && npm run exclude-test-assets && npm run patch-dependencies && npm run build",
"prepare": "npm run provide-backend-assets && npm run enable-backend-logging-android && npm run enable-backend-logging-ios && npm run enable-notifications && npm run exclude-test-assets && npm run patch-dependencies && npm run build",
"postversion": "react-native-version"
},
"dependencies": {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { expectSaga } from 'redux-saga-test-plan'
import { showNotificationSaga } from './showNotification.saga'
import { MarkUnreadChannelPayload, publicChannels, RICH_NOTIFICATION_CHANNEL } from '@quiet/state-manager'
import { call } from 'redux-saga-test-plan/matchers'
import { NativeModules } from 'react-native'

describe('showNotificationSaga', () => {
test('show notification for new messages', async () => {
NativeModules.NotificationModule = {
notify: jest.fn()
}

const unreadMessage: MarkUnreadChannelPayload = {
channelAddress: 'channelAddress',
message: {
channelAddress: 'address',
createdAt: 1000000,
id: 'id',
message: 'message',
pubKey: 'pubKey',
signature: 'signature',
type: 1
}
}
const mockJSONMessage = 'messageJsonObject'

await expectSaga(
showNotificationSaga,
publicChannels.actions.markUnreadChannel(unreadMessage)
)
.provide([
[call.fn(JSON.stringify), mockJSONMessage],
[call.fn(NativeModules.NotificationModule.notify), null]
])
.call(JSON.stringify, unreadMessage.message)
.call(NativeModules.NotificationModule.notify, RICH_NOTIFICATION_CHANNEL, mockJSONMessage)
.run()
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { publicChannels, RICH_NOTIFICATION_CHANNEL } from '@quiet/state-manager'
import { PayloadAction } from '@reduxjs/toolkit'
import { NativeModules } from 'react-native'
import { call } from 'typed-redux-saga'

export function* showNotificationSaga(
action: PayloadAction<ReturnType<typeof publicChannels.actions.markUnreadChannel>['payload']>
): Generator {
const stringChannelMessage = yield* call(JSON.stringify, action.payload.message)

yield* call(NativeModules.NotificationModule.notify, RICH_NOTIFICATION_CHANNEL, stringChannelMessage)
}
5 changes: 4 additions & 1 deletion packages/mobile/src/store/root.saga.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@ import { nativeServicesMasterSaga } from './nativeServices/nativeServices.master
import { initMasterSaga } from './init/init.master.saga'
import { initActions } from './init/init.slice'
import { setupCryptoSaga } from './init/setupCrypto/setupCrypto.saga'
import { publicChannels } from '@quiet/state-manager'
import { showNotificationSaga } from './nativeServices/showNotification/showNotification.saga'

export function* rootSaga(): Generator {
yield all([
takeEvery(initActions.setStoreReady.type, nativeServicesMasterSaga),
takeEvery(initActions.setStoreReady.type, initMasterSaga),
takeEvery(initActions.setStoreReady.type, setupCryptoSaga)
takeEvery(initActions.setStoreReady.type, setupCryptoSaga),
takeEvery(publicChannels.actions.markUnreadChannel.type, showNotificationSaga),
])
}
4 changes: 4 additions & 0 deletions packages/state-manager/src/constants.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
export const MAIN_CHANNEL = 'general'

export const AUTODOWNLOAD_SIZE_LIMIT = 20971520 // 20 MB

export const BASE_NOTIFICATION_CHANNEL = '_BASE_NOTIFICATION_'
export const RICH_NOTIFICATION_CHANNEL = '_RICH_NOTIFICATION_'
2 changes: 1 addition & 1 deletion packages/state-manager/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ export { PublicChannelsTransform } from './sagas/publicChannels/publicChannels.t
export { MessagesTransform } from './sagas/messages/messages.transform'
export { FilesTransform } from './sagas/files/files.transform'

export { AUTODOWNLOAD_SIZE_LIMIT } from './constants'
export { AUTODOWNLOAD_SIZE_LIMIT, BASE_NOTIFICATION_CHANNEL, RICH_NOTIFICATION_CHANNEL } from './constants'

export { parseName } from './utils/functions/naming/naming'

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,17 +112,20 @@ describe('markUnreadChannelsSaga', () => {
.withState(store.getState())
.put(
publicChannelsActions.markUnreadChannel({
channelAddress: 'memes'
channelAddress: 'memes',
message: messages[1]
})
)
.not.put(
publicChannelsActions.markUnreadChannel({
channelAddress: 'enya'
channelAddress: 'enya',
message: messages[2]
})
)
.put(
publicChannelsActions.markUnreadChannel({
channelAddress: 'travels'
channelAddress: 'travels',
message: messages[3]
})
)
.run()
Expand Down
Loading