Skip to content
Open
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 android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,10 @@
android:enabled="true"
android:exported="true" >
</receiver>
<receiver
android:name="chat.rocket.reactnative.notification.MarkAsReadBroadcast"
android:enabled="true"
android:exported="false" />
<receiver
android:name="chat.rocket.reactnative.notification.VideoConfBroadcast"
android:enabled="true"
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package chat.rocket.reactnative.notification;

import android.app.NotificationManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;

import com.google.gson.Gson;
import com.wix.reactnativenotifications.core.NotificationIntentAdapter;

import java.io.IOException;

import okhttp3.Call;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;

public class MarkAsReadBroadcast extends BroadcastReceiver {
private static final String TAG = "RocketChat.MarkAsRead";
public static final String KEY_MARK_AS_READ = "KEY_MARK_AS_READ";
private static final OkHttpClient client = new OkHttpClient();
private static final MediaType JSON = MediaType.parse("application/json; charset=utf-8");

@Override
public void onReceive(Context context, Intent intent) {
// Keep receiver alive for async network operation
final PendingResult pendingResult = goAsync();

Bundle bundle = NotificationIntentAdapter.extractPendingNotificationDataFromIntent(intent);
NotificationManager 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);

try {
int id = Integer.parseInt(notId);
markAsRead(ejson, id, notificationManager, pendingResult);
} catch (NumberFormatException e) {
Log.e(TAG, "Invalid notification ID: " + notId, e);
pendingResult.finish();
}
}

protected void markAsRead(final Ejson ejson, final int notId,
final NotificationManager notificationManager,
final PendingResult pendingResult) {
String serverURL = ejson.serverURL();
String rid = ejson.rid;

if (serverURL == null || rid == null) {
Log.e(TAG, "Missing serverURL or rid");
pendingResult.finish();
return;
}

String json = String.format("{\"rid\":\"%s\"}", rid);

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/subscriptions.read", serverURL))
.post(body)
.build();

client.newCall(request).enqueue(new okhttp3.Callback() {
@Override
public void onFailure(Call call, IOException e) {
Log.e(TAG, "Mark as read FAILED: " + e.getMessage());
pendingResult.finish();
}

@Override
public void onResponse(Call call, final Response response) throws IOException {
try {
if (response.isSuccessful()) {
Log.d(TAG, "Mark as read SUCCESS");
CustomPushNotification.clearMessages(notId);
notificationManager.cancel(notId);
} else {
Log.e(TAG, String.format("Mark as read FAILED status %s", response.code()));
}
} finally {
if (response.body() != null) {
response.body().close();
}
pendingResult.finish();
}
}
});
}
}
1 change: 1 addition & 0 deletions app/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -499,6 +499,7 @@
"Mark_as_unread_Info": "Display room as unread when there are unread messages",
"Mark_read": "Mark read",
"Mark_unread": "Mark unread",
"Mark_as_read": "Mark as read",
"Markdown_tools": "Markdown tools",
"Max_number_of_users_allowed_is_number": "Max number of users allowed is {{maxUsers}}",
"Max_number_of_uses": "Max number of uses",
Expand Down
9 changes: 8 additions & 1 deletion app/lib/notifications/push.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ const setupNotificationCategories = async (): Promise<void> => {
}

try {
// Message category with Reply action
// Message category with Reply and Mark as Read actions
await Notifications.setNotificationCategoryAsync('MESSAGE', [
{
identifier: 'REPLY_ACTION',
Expand All @@ -100,6 +100,13 @@ const setupNotificationCategories = async (): Promise<void> => {
options: {
opensAppToForeground: false
}
},
{
identifier: 'MARK_AS_READ_ACTION',
buttonTitle: I18n.t('Mark_as_read'),
options: {
opensAppToForeground: false
}
}
]);

Expand Down
56 changes: 42 additions & 14 deletions ios/ReplyNotification.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,20 @@ import UserNotifications
class ReplyNotification: NSObject, UNUserNotificationCenterDelegate {
private static var shared: ReplyNotification?
private weak var originalDelegate: UNUserNotificationCenterDelegate?

@objc
public static func configure() {
let instance = ReplyNotification()
shared = instance

// Store the original delegate (expo-notifications) and set ourselves as the delegate
let center = UNUserNotificationCenter.current()
instance.originalDelegate = center.delegate
center.delegate = instance
}

// MARK: - UNUserNotificationCenterDelegate

func userNotificationCenter(
_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse,
Expand All @@ -40,15 +40,21 @@ class ReplyNotification: NSObject, UNUserNotificationCenterDelegate {
handleReplyAction(response: response, completionHandler: completionHandler)
return
}


// Handle MARK_AS_READ_ACTION natively
if response.actionIdentifier == "MARK_AS_READ_ACTION" {
handleMarkAsReadAction(response: response, completionHandler: completionHandler)
return
}

// Forward to original delegate (expo-notifications)
if let originalDelegate = originalDelegate {
originalDelegate.userNotificationCenter?(center, didReceive: response, withCompletionHandler: completionHandler)
} else {
completionHandler()
}
}

func userNotificationCenter(
_ center: UNUserNotificationCenter,
willPresent notification: UNNotification,
Expand All @@ -61,7 +67,7 @@ class ReplyNotification: NSObject, UNUserNotificationCenterDelegate {
completionHandler([])
}
}

func userNotificationCenter(
_ center: UNUserNotificationCenter,
openSettingsFor notification: UNNotification?
Expand All @@ -73,37 +79,37 @@ class ReplyNotification: NSObject, UNUserNotificationCenterDelegate {
}
}
}

// MARK: - Reply Handling

private func handleReplyAction(response: UNNotificationResponse, completionHandler: @escaping () -> Void) {
guard let textResponse = response as? UNTextInputNotificationResponse else {
completionHandler()
return
}

let userInfo = response.notification.request.content.userInfo

guard let ejsonString = userInfo["ejson"] as? String,
let ejsonData = ejsonString.data(using: .utf8),
let payload = try? JSONDecoder().decode(Payload.self, from: ejsonData),
let rid = payload.rid else {
completionHandler()
return
}

let message = textResponse.userText
let rocketchat = RocketChat(server: payload.host.removeTrailingSlash())
let backgroundTask = UIApplication.shared.beginBackgroundTask(expirationHandler: nil)

rocketchat.sendMessage(rid: rid, message: message, threadIdentifier: payload.tmid) { response in
// Ensure we're on the main thread for UI operations
DispatchQueue.main.async {
defer {
UIApplication.shared.endBackgroundTask(backgroundTask)
completionHandler()
}

guard let response = response, response.success else {
// Show failure notification
let content = UNMutableNotificationContent()
Expand All @@ -115,4 +121,26 @@ class ReplyNotification: NSObject, UNUserNotificationCenterDelegate {
}
}
}

private func handleMarkAsReadAction(response: UNNotificationResponse, completionHandler: @escaping () -> Void) {
let userInfo = response.notification.request.content.userInfo

guard let ejsonString = userInfo["ejson"] as? String,
let ejsonData = ejsonString.data(using: .utf8),
let payload = try? JSONDecoder().decode(Payload.self, from: ejsonData),
let rid = payload.rid else {
completionHandler()
return
}

let rocketchat = RocketChat(server: payload.host.removeTrailingSlash())
let backgroundTask = UIApplication.shared.beginBackgroundTask(expirationHandler: nil)

rocketchat.markAsRead(rid: rid) { response in
DispatchQueue.main.async {
UIApplication.shared.endBackgroundTask(backgroundTask)
completionHandler()
}
}
}
}
34 changes: 34 additions & 0 deletions ios/Shared/RocketChat/API/Requests/MarkAsRead.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
//
// MarkAsRead.swift
// RocketChatRN
//
// Created for Mark as Read notification action
// Copyright © 2025 Rocket.Chat. All rights reserved.
//

import Foundation

struct MarkAsReadBody: Codable {
let rid: String
}

struct MarkAsReadResponse: Response {
var success: Bool
}

final class MarkAsReadRequest: Request {
typealias ResponseType = MarkAsReadResponse

let method: HTTPMethod = .post
let path = "/api/v1/subscriptions.read"

let rid: String

init(rid: String) {
self.rid = rid
}

func body() -> Data? {
return try? JSONEncoder().encode(MarkAsReadBody(rid: rid))
}
}
Loading