[Gekidou] refactor clean notifications (#6566)

This commit is contained in:
Elias Nahum 2022-08-19 16:29:15 -04:00 committed by GitHub
parent 25ae8fdb88
commit c2f5092678
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
42 changed files with 1217 additions and 933 deletions

View file

@ -71,23 +71,19 @@ public class CustomPushNotificationHelper {
}
long timestamp = new Date().getTime();
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
messagingStyle.addMessage(message, timestamp, senderName);
} else {
Person.Builder sender = new Person.Builder()
.setKey(senderId)
.setName(senderName);
Person.Builder sender = new Person.Builder()
.setKey(senderId)
.setName(senderName);
if (serverUrl != null) {
try {
sender.setIcon(IconCompat.createWithBitmap(Objects.requireNonNull(userAvatar(context, serverUrl, senderId))));
} catch (IOException e) {
e.printStackTrace();
}
if (serverUrl != null) {
try {
sender.setIcon(IconCompat.createWithBitmap(Objects.requireNonNull(userAvatar(context, serverUrl, senderId))));
} catch (IOException e) {
e.printStackTrace();
}
messagingStyle.addMessage(message, timestamp, sender.build());
}
messagingStyle.addMessage(message, timestamp, sender.build());
}
private static void addNotificationExtras(NotificationCompat.Builder notification, Bundle bundle) {
@ -111,6 +107,16 @@ public class CustomPushNotificationHelper {
userInfoBundle.putString("root_id", rootId);
}
String crtEnabled = bundle.getString("is_crt_enabled");
if (crtEnabled != null) {
userInfoBundle.putString("is_crt_enabled", crtEnabled);
}
String serverUrl = bundle.getString("server_url");
if (serverUrl != null) {
userInfoBundle.putString("server_url", serverUrl);
}
notification.addExtras(userInfoBundle);
}
@ -166,7 +172,7 @@ public class CustomPushNotificationHelper {
String rootId = bundle.getString("root_id");
int notificationId = postId != null ? postId.hashCode() : MESSAGE_NOTIFICATION_ID;
Boolean is_crt_enabled = bundle.getString("is_crt_enabled") != null && bundle.getString("is_crt_enabled").equals("true");
boolean is_crt_enabled = bundle.containsKey("is_crt_enabled") && bundle.getString("is_crt_enabled").equals("true");
String groupId = is_crt_enabled && !android.text.TextUtils.isEmpty(rootId) ? rootId : channelId;
addNotificationExtras(notification, bundle);
@ -256,24 +262,20 @@ public class CustomPushNotificationHelper {
String senderId = "me";
final String serverUrl = bundle.getString("server_url");
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
messagingStyle = new NotificationCompat.MessagingStyle("Me");
} else {
Person.Builder sender = new Person.Builder()
.setKey(senderId)
.setName("Me");
Person.Builder sender = new Person.Builder()
.setKey(senderId)
.setName("Me");
if (serverUrl != null) {
try {
sender.setIcon(IconCompat.createWithBitmap(Objects.requireNonNull(userAvatar(context, serverUrl, "me"))));
} catch (IOException e) {
e.printStackTrace();
}
if (serverUrl != null) {
try {
sender.setIcon(IconCompat.createWithBitmap(Objects.requireNonNull(userAvatar(context, serverUrl, "me"))));
} catch (IOException e) {
e.printStackTrace();
}
messagingStyle = new NotificationCompat.MessagingStyle(sender.build());
}
messagingStyle = new NotificationCompat.MessagingStyle(sender.build());
String conversationTitle = getConversationTitle(bundle);
setMessagingStyleConversationTitle(messagingStyle, conversationTitle, bundle);
addMessagingStyleMessages(context, messagingStyle, conversationTitle, bundle);

View file

@ -16,8 +16,7 @@ import java.lang.Exception
import java.util.*
class DatabaseHelper {
var defaultDatabase: Database? = null
private set
private var defaultDatabase: Database? = null
val onlyServerUrl: String?
get() {
@ -83,12 +82,15 @@ class DatabaseHelper {
fun queryIds(db: Database, tableName: String, ids: Array<String>): List<String> {
val list: MutableList<String> = ArrayList()
val args = TextUtils.join(",", Arrays.stream(ids).map { value: String? -> "?" }.toArray())
val args = TextUtils.join(",", Arrays.stream(ids).map { "?" }.toArray())
try {
db.rawQuery("select distinct id from $tableName where id IN ($args)", ids as Array<Any?>).use { cursor ->
if (cursor.count > 0) {
while (cursor.moveToNext()) {
list.add(cursor.getString(cursor.getColumnIndex("id")))
val index = cursor.getColumnIndex("id")
if (index >= 0) {
list.add(cursor.getString(index))
}
}
}
}
@ -100,12 +102,15 @@ class DatabaseHelper {
fun queryByColumn(db: Database, tableName: String, columnName: String, values: Array<Any?>): List<String> {
val list: MutableList<String> = ArrayList()
val args = TextUtils.join(",", Arrays.stream(values).map { value: Any? -> "?" }.toArray())
val args = TextUtils.join(",", Arrays.stream(values).map { "?" }.toArray())
try {
db.rawQuery("select distinct $columnName from $tableName where $columnName IN ($args)", values).use { cursor ->
if (cursor.count > 0) {
while (cursor.moveToNext()) {
list.add(cursor.getString(cursor.getColumnIndex(columnName)))
val index = cursor.getColumnIndex(columnName)
if (index >= 0) {
list.add(cursor.getString(index))
}
}
}
}
@ -120,7 +125,7 @@ class DatabaseHelper {
return result.getString("value")
}
fun queryLastPostCreateAt(db: Database?, channelId: String): Double? {
private fun queryLastPostCreateAt(db: Database?, channelId: String): Double? {
if (db != null) {
val postsInChannelQuery = "SELECT earliest, latest FROM PostsInChannel WHERE channel_id=? ORDER BY latest DESC LIMIT 1"
val cursor1 = db.rawQuery(postsInChannelQuery, arrayOf(channelId))
@ -175,7 +180,7 @@ class DatabaseHelper {
val firstId = ordered.first()
val lastId = ordered.last()
lastFetchedAt = postList.fold(0.0) { acc, next ->
val post = next.second as Map<String, Any?>
val post = next.second as Map<*, *>
val createAt = post["create_at"] as Double
val updateAt = post["update_at"] as Double
val deleteAt = post["delete_at"] as Double
@ -186,38 +191,38 @@ class DatabaseHelper {
var prevPostId = ""
val sortedPosts = postList.sortedBy { (_, value) ->
((value as Map<*, *>).get("create_at") as Double)
((value as Map<*, *>)["create_at"] as Double)
}
sortedPosts.forEachIndexed { index, it ->
val key = it.first
if (it.second != null) {
val post = (it.second as MutableMap<String, Any?>)
val post = it.second as MutableMap<String, Any?>
if (index == 0) {
post.putIfAbsent("prev_post_id", previousPostId)
} else if (!prevPostId.isNullOrEmpty()) {
} else if (prevPostId.isNotEmpty()) {
post.putIfAbsent("prev_post_id", prevPostId)
}
if (lastId == key) {
earliest = post.get("create_at") as Double
earliest = post["create_at"] as Double
}
if (firstId == key) {
latest = post.get("create_at") as Double
latest = post["create_at"] as Double
}
val jsonPost = JSONObject(post)
val rootId = post.get("root_id") as? String
val rootId = post["root_id"] as? String
if (!rootId.isNullOrEmpty()) {
var thread = postsInThread.get(rootId)?.toMutableList()
var thread = postsInThread[rootId]?.toMutableList()
if (thread == null) {
thread = mutableListOf()
}
thread.add(jsonPost)
postsInThread.put(rootId, thread.toList())
postsInThread[rootId] = thread.toList()
}
if (find(db, "Post", key) == null) {
@ -308,27 +313,6 @@ class DatabaseHelper {
}
}
fun getServerVersion(db: Database): String? {
val config = getSystemConfig(db)
if (config != null) {
return config.getString("Version")
}
return null
}
private fun getSystemConfig(db: Database): JSONObject? {
val configRecord = find(db, "System", "config")
if (configRecord != null) {
val value = configRecord.getString("value");
try {
return JSONObject(value)
} catch(e: JSONException) {
return null
}
}
return null
}
private fun setDefaultDatabase(context: Context) {
val databaseName = "app.db"
val databasePath = Uri.fromFile(context.filesDir).toString() + "/" + databaseName
@ -336,7 +320,7 @@ class DatabaseHelper {
}
private fun insertPost(db: Database, post: JSONObject) {
var metadata: JSONObject? = null
var metadata: JSONObject?
var reactions: JSONArray? = null
var customEmojis: JSONArray? = null
var files: JSONArray? = null
@ -390,7 +374,7 @@ class DatabaseHelper {
}
private fun updatePost(db: Database, post: JSONObject) {
var metadata: JSONObject? = null
var metadata: JSONObject?
var reactions: JSONArray? = null
var customEmojis: JSONArray? = null
@ -441,10 +425,12 @@ class DatabaseHelper {
private fun insertThread(db: Database, thread: ReadableMap) {
// These fields are not present when we extract threads from posts
val isFollowing = try { thread.getBoolean("is_following") } catch (e: NoSuchKeyException) { false };
val lastViewedAt = try { thread.getDouble("last_viewed_at") } catch (e: NoSuchKeyException) { 0 };
val unreadReplies = try { thread.getInt("unread_replies") } catch (e: NoSuchKeyException) { 0 };
val unreadMentions = try { thread.getInt("unread_mentions") } catch (e: NoSuchKeyException) { 0 };
val isFollowing = try { thread.getBoolean("is_following") } catch (e: NoSuchKeyException) { false }
val lastViewedAt = try { thread.getDouble("last_viewed_at") } catch (e: NoSuchKeyException) { 0 }
val unreadReplies = try { thread.getInt("unread_replies") } catch (e: NoSuchKeyException) { 0 }
val unreadMentions = try { thread.getInt("unread_mentions") } catch (e: NoSuchKeyException) { 0 }
val lastReplyAt = try { thread.getDouble("last_reply_at") } catch (e: NoSuchKeyException) { 0 }
val replyCount = try { thread.getInt("reply_count") } catch (e: NoSuchKeyException) { 0 }
db.execute(
"insert into Thread " +
@ -452,9 +438,9 @@ class DatabaseHelper {
" values (?, ?, 0, ?, ?, ?, ?, ?, 'created')",
arrayOf(
thread.getString("id"),
thread.getDouble("last_reply_at") ?: 0,
lastReplyAt,
lastViewedAt,
thread.getInt("reply_count") ?: 0,
replyCount,
isFollowing,
unreadReplies,
unreadMentions
@ -464,17 +450,19 @@ class DatabaseHelper {
private fun updateThread(db: Database, thread: ReadableMap, existingRecord: ReadableMap) {
// These fields are not present when we extract threads from posts
val isFollowing = try { thread.getBoolean("is_following") } catch (e: NoSuchKeyException) { existingRecord.getInt("is_following") == 1 };
val lastViewedAt = try { thread.getDouble("last_viewed_at") } catch (e: NoSuchKeyException) { existingRecord.getDouble("last_viewed_at") };
val unreadReplies = try { thread.getInt("unread_replies") } catch (e: NoSuchKeyException) { existingRecord.getInt("unread_replies") };
val unreadMentions = try { thread.getInt("unread_mentions") } catch (e: NoSuchKeyException) { existingRecord.getInt("unread_mentions") };
val isFollowing = try { thread.getBoolean("is_following") } catch (e: NoSuchKeyException) { existingRecord.getInt("is_following") == 1 }
val lastViewedAt = try { thread.getDouble("last_viewed_at") } catch (e: NoSuchKeyException) { existingRecord.getDouble("last_viewed_at") }
val unreadReplies = try { thread.getInt("unread_replies") } catch (e: NoSuchKeyException) { existingRecord.getInt("unread_replies") }
val unreadMentions = try { thread.getInt("unread_mentions") } catch (e: NoSuchKeyException) { existingRecord.getInt("unread_mentions") }
val lastReplyAt = try { thread.getDouble("last_reply_at") } catch (e: NoSuchKeyException) { 0 }
val replyCount = try { thread.getInt("reply_count") } catch (e: NoSuchKeyException) { 0 }
db.execute(
"update Thread SET last_reply_at = ?, last_viewed_at = ?, reply_count = ?, is_following = ?, unread_replies = ?, unread_mentions = ?, _status = 'updated' where id = ?",
arrayOf(
thread.getDouble("last_reply_at") ?: 0,
lastReplyAt,
lastViewedAt,
thread.getInt("reply_count") ?: 0,
replyCount,
isFollowing,
unreadReplies,
unreadMentions,
@ -518,9 +506,9 @@ class DatabaseHelper {
private fun insertFiles(db: Database, files: JSONArray) {
for (i in 0 until files.length()) {
val file = files.getJSONObject(i)
val miniPreview = try { file.getString("mini_preview") } catch (e: JSONException) { "" };
val height = try { file.getInt("height") } catch (e: JSONException) { 0 };
val width = try { file.getInt("width") } catch (e: JSONException) { 0 };
val miniPreview = try { file.getString("mini_preview") } catch (e: JSONException) { "" }
val height = try { file.getInt("height") } catch (e: JSONException) { 0 }
val width = try { file.getInt("width") } catch (e: JSONException) { 0 }
db.execute(
"insert into File (id, extension, height, image_thumbnail, local_path, mime_type, name, post_id, size, width, _status) " +
"values (?, ?, ?, ?, '', ?, ?, ?, ?, ?, 'created')",
@ -604,11 +592,11 @@ class DatabaseHelper {
for (i in 0 until chunks.size()) {
val chunk = chunks.getMap(i)
if (earliest >= chunk.getDouble("earliest") || latest <= chunk.getDouble("latest")) {
return chunk;
return chunk
}
}
return null;
return null
}
private fun insertPostInChannel(db: Database, channelId: String, earliest: Double, latest: Double): ReadableMap {
@ -666,7 +654,7 @@ class DatabaseHelper {
}
}
private fun JSONObject.toMap(): Map<String, *> = keys().asSequence().associateWith {
private fun JSONObject.toMap(): Map<String, *> = keys().asSequence().associateWith { it ->
when (val value = this[it])
{
is JSONArray ->

View file

@ -1,68 +0,0 @@
package com.mattermost.helpers;
import com.facebook.react.bridge.Dynamic;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.ReadableType;
import java.util.ArrayList;
/**
* KeysReadableArray: Helper class that abstracts boilerplate
*/
public class KeysReadableArray implements ReadableArray {
@Override
public int size() {
return 0;
}
@Override
public boolean isNull(int index) {
return false;
}
@Override
public boolean getBoolean(int index) {
return false;
}
@Override
public double getDouble(int index) {
return 0;
}
@Override
public int getInt(int index) {
return 0;
}
@Override
public String getString(int index) {
return null;
}
@Override
public ReadableArray getArray(int index) {
return null;
}
@Override
public ReadableMap getMap(int index) {
return null;
}
@Override
public Dynamic getDynamic(int index) {
return null;
}
@Override
public ReadableType getType(int index) {
return null;
}
@Override
public ArrayList<Object> toArrayList() {
return null;
}
}

View file

@ -0,0 +1,312 @@
package com.mattermost.helpers;
import android.app.Notification;
import android.app.NotificationManager;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.pm.PackageInfo;
import android.os.Bundle;
import android.service.notification.StatusBarNotification;
import androidx.core.app.NotificationManagerCompat;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Objects;
public class NotificationHelper {
public static final String PUSH_NOTIFICATIONS = "PUSH_NOTIFICATIONS";
public static final String NOTIFICATIONS_IN_GROUP = "notificationsInGroup";
private static final String VERSION_PREFERENCE = "VERSION_PREFERENCE";
public static void cleanNotificationPreferencesIfNeeded(Context context) {
try {
PackageInfo pInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
String version = String.valueOf(pInfo.versionCode);
String storedVersion = null;
SharedPreferences pSharedPref = context.getSharedPreferences(VERSION_PREFERENCE, Context.MODE_PRIVATE);
if (pSharedPref != null) {
storedVersion = pSharedPref.getString("Version", "");
}
if (!version.equals(storedVersion)) {
if (pSharedPref != null) {
SharedPreferences.Editor editor = pSharedPref.edit();
editor.putString("Version", version);
editor.apply();
}
Map<String, JSONObject> inputMap = new HashMap<>();
saveMap(context, inputMap);
}
} catch (Exception e) {
e.printStackTrace();
}
}
public static int getNotificationId(Bundle notification) {
final String postId = notification.getString("post_id");
final String channelId = notification.getString("channel_id");
int notificationId = CustomPushNotificationHelper.MESSAGE_NOTIFICATION_ID;
if (postId != null) {
notificationId = postId.hashCode();
} else if (channelId != null) {
notificationId = channelId.hashCode();
}
return notificationId;
}
public static StatusBarNotification[] getDeliveredNotifications(Context context) {
final NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
return notificationManager.getActiveNotifications();
}
public static boolean addNotificationToPreferences(Context context, int notificationId, Bundle notification) {
try {
boolean createSummary = true;
final String serverUrl = notification.getString("server_url");
final String channelId = notification.getString("channel_id");
final String rootId = notification.getString("root_id");
final boolean isCRTEnabled = notification.containsKey("is_crt_enabled") && notification.getString("is_crt_enabled").equals("true");
final boolean isThreadNotification = isCRTEnabled && !android.text.TextUtils.isEmpty(rootId);
final String groupId = isThreadNotification ? rootId : channelId;
Map<String, JSONObject> notificationsPerServer = loadMap(context);
JSONObject notificationsInServer = notificationsPerServer.get(serverUrl);
if (notificationsInServer == null) {
notificationsInServer = new JSONObject();
}
JSONObject notificationsInGroup = notificationsInServer.optJSONObject(groupId);
if (notificationsInGroup == null) {
notificationsInGroup = new JSONObject();
}
if (notificationsInGroup.length() > 0) {
createSummary = false;
}
notificationsInGroup.put(String.valueOf(notificationId), false);
if (createSummary) {
// Add the summary notification id as well
notificationsInGroup.put(String.valueOf(notificationId + 1), true);
}
notificationsInServer.put(groupId, notificationsInGroup);
notificationsPerServer.put(serverUrl, notificationsInServer);
saveMap(context, notificationsPerServer);
return createSummary;
} catch(Exception e) {
e.printStackTrace();
return false;
}
}
public static void dismissNotification(Context context, Bundle notification) {
final boolean isCRTEnabled = notification.containsKey("is_crt_enabled") && notification.getString("is_crt_enabled").equals("true");
final String serverUrl = notification.getString("server_url");
final String channelId = notification.getString("channel_id");
final String rootId = notification.getString("root_id");
int notificationId = getNotificationId(notification);
if (!android.text.TextUtils.isEmpty(serverUrl) && !android.text.TextUtils.isEmpty(channelId)) {
boolean isThreadNotification = isCRTEnabled && !android.text.TextUtils.isEmpty(rootId);
String notificationIdStr = String.valueOf(notificationId);
String groupId = isThreadNotification ? rootId : channelId;
Map<String, JSONObject> notificationsPerServer = loadMap(context);
JSONObject notificationsInServer = notificationsPerServer.get(serverUrl);
if (notificationsInServer == null) {
return;
}
JSONObject notificationsInGroup = notificationsInServer.optJSONObject(groupId);
if (notificationsInGroup == null) {
return;
}
boolean isSummary = notificationsInGroup.optBoolean(notificationIdStr);
notificationsInGroup.remove(notificationIdStr);
NotificationManager notificationManager = context.getSystemService(NotificationManager.class);
notificationManager.cancel(notificationId);
StatusBarNotification[] statusNotifications = getDeliveredNotifications(context);
boolean hasMore = false;
for (final StatusBarNotification status : statusNotifications) {
Bundle bundle = status.getNotification().extras;
if (isThreadNotification) {
hasMore = bundle.getString("root_id").equals(rootId);
} else {
hasMore = bundle.getString("channel_id").equals(channelId);
}
if (hasMore) break;
}
if (!hasMore || isSummary) {
notificationsInServer.remove(groupId);
} else {
try {
notificationsInServer.put(groupId, notificationsInGroup);
} catch (JSONException e) {
e.printStackTrace();
}
}
notificationsPerServer.put(serverUrl, notificationsInServer);
saveMap(context, notificationsPerServer);
}
}
public static void removeChannelNotifications(Context context, String serverUrl, String channelId) {
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
Map<String, JSONObject> notificationsPerServer = loadMap(context);
JSONObject notificationsInServer = notificationsPerServer.get(serverUrl);
if (notificationsInServer != null) {
notificationsInServer.remove(channelId);
notificationsPerServer.put(serverUrl, notificationsInServer);
saveMap(context, notificationsPerServer);
}
StatusBarNotification[] notifications = getDeliveredNotifications(context);
for (StatusBarNotification sbn:notifications) {
Notification n = sbn.getNotification();
Bundle bundle = n.extras;
String cId = bundle.getString("channel_id");
String rootId = bundle.getString("root_id");
boolean isCRTEnabled = bundle.containsKey("is_crt_enabled") && bundle.getString("is_crt_enabled").equals("true");
boolean skipThreadNotification = isCRTEnabled && !android.text.TextUtils.isEmpty(rootId);
if (Objects.equals(cId, channelId) && !skipThreadNotification) {
notificationManager.cancel(sbn.getId());
}
}
}
public static void removeThreadNotifications(Context context, String serverUrl, String threadId) {
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
Map<String, JSONObject> notificationsPerServer = loadMap(context);
JSONObject notificationsInServer = notificationsPerServer.get(serverUrl);
StatusBarNotification[] notifications = getDeliveredNotifications(context);
for (StatusBarNotification sbn:notifications) {
Notification n = sbn.getNotification();
Bundle bundle = n.extras;
String rootId = bundle.getString("root_id");
String postId = bundle.getString("post_id");
if (Objects.equals(rootId, threadId)) {
notificationManager.cancel(sbn.getId());
}
if (Objects.equals(postId, threadId)) {
String channelId = bundle.getString("channel_id");
int id = sbn.getId();
if (notificationsInServer != null && channelId != null) {
JSONObject notificationsInChannel = notificationsInServer.optJSONObject(channelId);
if (notificationsInChannel != null) {
notificationsInChannel.remove(String.valueOf(id));
try {
notificationsInServer.put(channelId, notificationsInChannel);
} catch (JSONException e) {
e.printStackTrace();
}
}
}
notificationManager.cancel(id);
}
}
if (notificationsInServer != null) {
notificationsInServer.remove(threadId);
notificationsPerServer.put(serverUrl, notificationsInServer);
saveMap(context, notificationsPerServer);
}
}
public static void removeServerNotifications(Context context, String serverUrl) {
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
Map<String, JSONObject> notificationsPerServer = loadMap(context);
notificationsPerServer.remove(serverUrl);
saveMap(context, notificationsPerServer);
StatusBarNotification[] notifications = getDeliveredNotifications(context);
for (StatusBarNotification sbn:notifications) {
Notification n = sbn.getNotification();
Bundle bundle = n.extras;
String url = bundle.getString("server_url");
if (Objects.equals(url, serverUrl)) {
notificationManager.cancel(sbn.getId());
}
}
}
public static void clearChannelOrThreadNotifications(Context context, Bundle notification) {
final String serverUrl = notification.getString("server_url");
final String channelId = notification.getString("channel_id");
final String rootId = notification.getString("root_id");
if (channelId != null) {
final boolean isCRTEnabled = notification.containsKey("is_crt_enabled") && notification.getString("is_crt_enabled").equals("true");
// rootId is available only when CRT is enabled & clearing the thread
final boolean isClearThread = isCRTEnabled && !android.text.TextUtils.isEmpty(rootId);
if (isClearThread) {
removeThreadNotifications(context, serverUrl, rootId);
} else {
removeChannelNotifications(context, serverUrl, channelId);
}
}
}
/**
* Map Structure
*
* { serverUrl: { groupId: { notification1: true, notification2: false } } }
* summary notification has a value of true
*
*/
private static void saveMap(Context context, Map<String, JSONObject> inputMap) {
SharedPreferences pSharedPref = context.getSharedPreferences(PUSH_NOTIFICATIONS, Context.MODE_PRIVATE);
if (pSharedPref != null) {
JSONObject json = new JSONObject(inputMap);
String jsonString = json.toString();
SharedPreferences.Editor editor = pSharedPref.edit();
editor.remove(NOTIFICATIONS_IN_GROUP).apply();
editor.putString(NOTIFICATIONS_IN_GROUP, jsonString);
editor.apply();
}
}
private static Map<String, JSONObject> loadMap(Context context) {
Map<String, JSONObject> outputMap = new HashMap<>();
if (context != null) {
SharedPreferences pSharedPref = context.getSharedPreferences(PUSH_NOTIFICATIONS, Context.MODE_PRIVATE);
try {
if (pSharedPref != null) {
String jsonString = pSharedPref.getString(NOTIFICATIONS_IN_GROUP, (new JSONObject()).toString());
JSONObject json = new JSONObject(jsonString);
Iterator<String> servers = json.keys();
while (servers.hasNext()) {
String serverUrl = servers.next();
JSONObject notificationGroup = json.getJSONObject(serverUrl);
outputMap.put(serverUrl, notificationGroup);
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
return outputMap;
}
}

View file

@ -2,6 +2,7 @@ package com.mattermost.helpers
import android.content.Context
import android.os.Bundle
import android.util.Log
import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.ReadableArray
import com.facebook.react.bridge.ReadableMap
@ -25,7 +26,8 @@ class PushNotificationDataHelper(private val context: Context) {
class PushNotificationDataRunnable {
companion object {
private val specialMentions = listOf<String>("all", "here", "channel")
private val specialMentions = listOf("all", "here", "channel")
@Synchronized
suspend fun start(context: Context, initialData: Bundle) {
try {
@ -34,6 +36,7 @@ class PushNotificationDataRunnable {
val rootId = initialData.getString("root_id")
val isCRTEnabled = initialData.getString("is_crt_enabled") == "true"
val db = DatabaseHelper.instance!!.getDatabaseForServer(context, serverUrl)
Log.i("ReactNative", "Start fetching notification data in server="+serverUrl+" for channel="+channelId)
if (db != null) {
var postData: ReadableMap?
@ -90,6 +93,7 @@ class PushNotificationDataRunnable {
}
db.close()
Log.i("ReactNative", "Done processing push notification="+serverUrl+" for channel="+channelId)
}
} catch (e: Exception) {
e.printStackTrace()
@ -108,14 +112,13 @@ class PushNotificationDataRunnable {
additionalParams = "&collapsedThreads=true&collapsedThreadsExtended=true"
}
var endpoint: String
val receivingThreads = isCRTEnabled && !rootId.isNullOrEmpty()
if (receivingThreads) {
var queryParams = "?skipFetchThreads=false&perPage=60&fromCreatedAt=0&direction=up"
endpoint = "/api/v4/posts/$rootId/thread$queryParams$additionalParams"
val endpoint = if (receivingThreads) {
val queryParams = "?skipFetchThreads=false&perPage=60&fromCreatedAt=0&direction=up"
"/api/v4/posts/$rootId/thread$queryParams$additionalParams"
} else {
var queryParams = if (since == null) "?page=0&per_page=60" else "?since=${since.toLong()}"
endpoint = "/api/v4/channels/$channelId/posts$queryParams$additionalParams"
val queryParams = if (since == null) "?page=0&per_page=60" else "?since=${since.toLong()}"
"/api/v4/channels/$channelId/posts$queryParams$additionalParams"
}
val postsResponse = fetch(serverUrl, endpoint)
@ -124,11 +127,11 @@ class PushNotificationDataRunnable {
if (postsResponse != null) {
val data = ReadableMapUtils.toMap(postsResponse)
results.putMap("posts", postsResponse)
val postsData = data.get("data") as? Map<*, *>
val postsData = data["data"] as? Map<*, *>
if (postsData != null) {
val postsMap = postsData.get("posts")
val postsMap = postsData["posts"]
if (postsMap != null) {
val posts = ReadableMapUtils.toWritableMap(postsMap as? Map<String, Object>)
val posts = ReadableMapUtils.toWritableMap(postsMap as? Map<String, Any>)
val iterator = posts.keySetIterator()
val userIds = mutableListOf<String>()
val usernames = mutableListOf<String>()
@ -158,8 +161,8 @@ class PushNotificationDataRunnable {
if (isCRTEnabled) {
// Add root post as a thread
val rootId = post?.getString("root_id")
if (rootId.isNullOrEmpty()) {
val threadId = post?.getString("root_id")
if (threadId.isNullOrEmpty()) {
threads.pushMap(post!!)
}
@ -169,14 +172,14 @@ class PushNotificationDataRunnable {
for (i in 0 until participants.size()) {
val participant = participants.getMap(i)
val userId = participant.getString("id")
if (userId != currentUserId && userId != null) {
if (!threadParticipantUserIds.contains(userId)) {
threadParticipantUserIds.add(userId)
val participantId = participant.getString("id")
if (participantId != currentUserId && participantId != null) {
if (!threadParticipantUserIds.contains(participantId)) {
threadParticipantUserIds.add(participantId)
}
if (!threadParticipantUsers.containsKey(userId)) {
threadParticipantUsers[userId!!] = participant
if (!threadParticipantUsers.containsKey(participantId)) {
threadParticipantUsers[participantId] = participant
}
}
@ -236,14 +239,14 @@ class PushNotificationDataRunnable {
val endpoint = "api/v4/users/ids"
val options = Arguments.createMap()
options.putArray("body", ReadableArrayUtils.toWritableArray(ReadableArrayUtils.toArray(userIds)))
return fetchWithPost(serverUrl, endpoint, options);
return fetchWithPost(serverUrl, endpoint, options)
}
private suspend fun fetchUsersByUsernames(serverUrl: String, usernames: ReadableArray): ReadableMap? {
val endpoint = "api/v4/users/usernames"
val options = Arguments.createMap()
options.putArray("body", ReadableArrayUtils.toWritableArray(ReadableArrayUtils.toArray(usernames)))
return fetchWithPost(serverUrl, endpoint, options);
return fetchWithPost(serverUrl, endpoint, options)
}
private suspend fun fetch(serverUrl: String, endpoint: String): ReadableMap? {

View file

@ -5,15 +5,15 @@ import kotlin.math.floor
class RandomId {
companion object {
private const val alphabet = "0123456789abcdefghijklmnopqrstuvwxyz"
private const val alphabetLenght = alphabet.length
private const val idLenght = 16
private const val alphabetLength = alphabet.length
private const val idLength = 16
fun generate(): String {
var id = ""
for (i in 1.rangeTo((idLenght / 2))) {
val random = floor(Math.random() * alphabetLenght * alphabetLenght)
id += alphabet[floor(random / alphabetLenght).toInt()]
id += alphabet[(random % alphabetLenght).toInt()]
for (i in 1.rangeTo((idLength / 2))) {
val random = floor(Math.random() * alphabetLength * alphabetLength)
id += alphabet[floor(random / alphabetLength).toInt()]
id += alphabet[(random % alphabetLength).toInt()]
}
return id

View file

@ -99,23 +99,17 @@ public class ReadableArrayUtils {
for (Object value : array) {
if (value == null) {
writableArray.pushNull();
}
if (value instanceof Boolean) {
} else if (value instanceof Boolean) {
writableArray.pushBoolean((Boolean) value);
}
if (value instanceof Double) {
} else if (value instanceof Double) {
writableArray.pushDouble((Double) value);
}
if (value instanceof Integer) {
} else if (value instanceof Integer) {
writableArray.pushInt((Integer) value);
}
if (value instanceof String) {
} else if (value instanceof String) {
writableArray.pushString((String) value);
}
if (value instanceof Map) {
} else if (value instanceof Map) {
writableArray.pushMap(ReadableMapUtils.toWritableMap((Map<String, Object>) value));
}
if (value.getClass().isArray()) {
} else if (value.getClass().isArray()) {
writableArray.pushArray(ReadableArrayUtils.toWritableArray((Object[]) value));
}
}

View file

@ -1,6 +1,7 @@
package com.mattermost.helpers;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.ReadableMapKeySetIterator;
import com.facebook.react.bridge.ReadableType;
@ -38,10 +39,16 @@ public class ReadableMapUtils {
jsonObject.put(key, readableMap.getString(key));
break;
case Map:
jsonObject.put(key, ReadableMapUtils.toJSONObject(readableMap.getMap(key)));
ReadableMap map = readableMap.getMap(key);
if (map != null) {
jsonObject.put(key, ReadableMapUtils.toJSONObject(map));
}
break;
case Array:
jsonObject.put(key, ReadableArrayUtils.toJSONArray(readableMap.getArray(key)));
ReadableArray array = readableMap.getArray(key);
if (array != null) {
jsonObject.put(key, ReadableArrayUtils.toJSONArray(array));
}
break;
}
}
@ -92,10 +99,16 @@ public class ReadableMapUtils {
map.put(key, readableMap.getString(key));
break;
case Map:
map.put(key, ReadableMapUtils.toMap(readableMap.getMap(key)));
ReadableMap obj = readableMap.getMap(key);
if (obj != null) {
map.put(key, ReadableMapUtils.toMap(obj));
}
break;
case Array:
map.put(key, ReadableArrayUtils.toArray(readableMap.getArray(key)));
ReadableArray array = readableMap.getArray(key);
if (array != null) {
map.put(key, ReadableArrayUtils.toArray(array));
}
break;
}
}
@ -105,26 +118,26 @@ public class ReadableMapUtils {
public static WritableMap toWritableMap(Map<String, Object> map) {
WritableMap writableMap = Arguments.createMap();
Iterator iterator = map.entrySet().iterator();
Iterator<Map.Entry<String, Object>> iterator = map.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry pair = (Map.Entry)iterator.next();
Map.Entry<String, Object> pair = iterator.next();
Object value = pair.getValue();
if (value == null) {
writableMap.putNull((String) pair.getKey());
writableMap.putNull(pair.getKey());
} else if (value instanceof Boolean) {
writableMap.putBoolean((String) pair.getKey(), (Boolean) value);
writableMap.putBoolean(pair.getKey(), (Boolean) value);
} else if (value instanceof Double) {
writableMap.putDouble((String) pair.getKey(), (Double) value);
writableMap.putDouble(pair.getKey(), (Double) value);
} else if (value instanceof Integer) {
writableMap.putInt((String) pair.getKey(), (Integer) value);
writableMap.putInt(pair.getKey(), (Integer) value);
} else if (value instanceof String) {
writableMap.putString((String) pair.getKey(), (String) value);
} else if (value instanceof Map) {
writableMap.putMap((String) pair.getKey(), ReadableMapUtils.toWritableMap((Map<String, Object>) value));
} else if (value.getClass() != null && value.getClass().isArray()) {
writableMap.putArray((String) pair.getKey(), ReadableArrayUtils.toWritableArray((Object[]) value));
writableMap.putString(pair.getKey(), (String) value);
} else if (value instanceof Map)
writableMap.putMap(pair.getKey(), ReadableMapUtils.toWritableMap((Map<String, Object>) value));
else if (value.getClass().isArray()) {
writableMap.putArray(pair.getKey(), ReadableArrayUtils.toWritableArray((Object[]) value));
}
iterator.remove();

View file

@ -3,11 +3,9 @@ package com.mattermost.helpers;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.provider.DocumentsContract;
import android.provider.MediaStore;
import android.provider.OpenableColumns;
import android.content.ContentResolver;
import android.os.Environment;
import android.webkit.MimeTypeMap;
import android.util.Log;
@ -18,16 +16,14 @@ import android.os.ParcelFileDescriptor;
import java.io.*;
import java.nio.channels.FileChannel;
// Class based on the steveevers DocumentHelper https://gist.github.com/steveevers/a5af24c226f44bb8fdc3
// Class based on DocumentHelper https://gist.github.com/steveevers/a5af24c226f44bb8fdc3
public class RealPathUtil {
public static final String CACHE_DIR_NAME = "mmShare";
public static String getRealPathFromURI(final Context context, final Uri uri) {
final boolean isKitKatOrNewer = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;
// DocumentProvider
if (isKitKatOrNewer && DocumentsContract.isDocumentUri(context, uri)) {
if (DocumentsContract.isDocumentUri(context, uri)) {
// ExternalStorageProvider
if (isExternalStorageDocument(uri)) {
final String docId = DocumentsContract.getDocumentId(uri);
@ -48,7 +44,7 @@ public class RealPathUtil {
try {
return getPathFromSavingTempFile(context, uri);
} catch (NumberFormatException e) {
Log.e("ReactNative", "DownloadsProvider unexpected uri " + uri.toString());
Log.e("ReactNative", "DownloadsProvider unexpected uri " + uri);
return null;
}
}
@ -100,7 +96,7 @@ public class RealPathUtil {
public static String getPathFromSavingTempFile(Context context, final Uri uri) {
File tmpFile;
String fileName = null;
String fileName = "";
if (uri == null || uri.isRelative()) {
return null;
@ -113,13 +109,14 @@ public class RealPathUtil {
int nameIndex = returnCursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
returnCursor.moveToFirst();
fileName = sanitizeFilename(returnCursor.getString(nameIndex));
returnCursor.close();
} catch (Exception e) {
// just continue to get the filename with the last segment of the path
}
try {
if (TextUtils.isEmpty(fileName)) {
fileName = sanitizeFilename(uri.getLastPathSegment().toString().trim());
fileName = sanitizeFilename(uri.getLastPathSegment().trim());
}
@ -128,7 +125,6 @@ public class RealPathUtil {
cacheDir.mkdirs();
}
String mimeType = getMimeType(uri.getPath());
tmpFile = new File(cacheDir, fileName);
tmpFile.createNewFile();
@ -214,15 +210,6 @@ public class RealPathUtil {
return getMimeType(file);
}
public static String getMimeTypeFromUri(final Context context, final Uri uri) {
try {
ContentResolver cR = context.getContentResolver();
return cR.getType(uri);
} catch (Exception e) {
return "application/octet-stream";
}
}
public static void deleteTempFiles(final File dir) {
try {
if (dir.isDirectory()) {
@ -234,9 +221,13 @@ public class RealPathUtil {
}
private static void deleteRecursive(File fileOrDirectory) {
if (fileOrDirectory.isDirectory())
for (File child : fileOrDirectory.listFiles())
deleteRecursive(child);
if (fileOrDirectory.isDirectory()) {
File[] files = fileOrDirectory.listFiles();
if (files != null) {
for (File child : files)
deleteRecursive(child);
}
}
fileOrDirectory.delete();
}

View file

@ -1,5 +1,7 @@
package com.mattermost.helpers;
import androidx.annotation.NonNull;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.WritableMap;
@ -18,7 +20,7 @@ public class ResolvePromise implements Promise {
}
@Override
public void reject(String code, WritableMap map) {
public void reject(String code, @NonNull WritableMap map) {
}
@ -48,7 +50,7 @@ public class ResolvePromise implements Promise {
}
@Override
public void reject(String code, String message, WritableMap map) {
public void reject(String code, String message, @NonNull WritableMap map) {
}

View file

@ -1,30 +1,21 @@
package com.mattermost.rnbeta;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.service.notification.StatusBarNotification;
import android.text.TextUtils;
import android.util.Log;
import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Objects;
import com.mattermost.helpers.CustomPushNotificationHelper;
import com.mattermost.helpers.DatabaseHelper;
import com.mattermost.helpers.Network;
import com.mattermost.helpers.NotificationHelper;
import com.mattermost.helpers.PushNotificationDataHelper;
import com.mattermost.helpers.ResolvePromise;
import com.wix.reactnativenotifications.core.NotificationIntentAdapter;
@ -34,15 +25,10 @@ import com.wix.reactnativenotifications.core.AppLifecycleFacade;
import com.wix.reactnativenotifications.core.JsIOHelper;
import static com.wix.reactnativenotifications.Defs.NOTIFICATION_RECEIVED_EVENT_NAME;
import org.json.JSONObject;
public class CustomPushNotification extends PushNotification {
private static final String PUSH_NOTIFICATIONS = "PUSH_NOTIFICATIONS";
private static final String VERSION_PREFERENCE = "VERSION_PREFERENCE";
private static final String PUSH_TYPE_MESSAGE = "message";
private static final String PUSH_TYPE_CLEAR = "clear";
private static final String PUSH_TYPE_SESSION = "session";
private static final String NOTIFICATIONS_IN_CHANNEL = "notificationsInChannel";
private final PushNotificationDataHelper dataHelper;
public CustomPushNotification(Context context, Bundle bundle, AppLifecycleFacade appLifecycleFacade, AppLaunchHelper appLaunchHelper, JsIOHelper jsIoHelper) {
@ -53,101 +39,12 @@ public class CustomPushNotification extends PushNotification {
try {
Objects.requireNonNull(DatabaseHelper.Companion.getInstance()).init(context);
Network.init(context);
PackageInfo pInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
String version = String.valueOf(pInfo.versionCode);
String storedVersion = null;
SharedPreferences pSharedPref = context.getSharedPreferences(VERSION_PREFERENCE, Context.MODE_PRIVATE);
if (pSharedPref != null) {
storedVersion = pSharedPref.getString("Version", "");
}
if (!version.equals(storedVersion)) {
if (pSharedPref != null) {
SharedPreferences.Editor editor = pSharedPref.edit();
editor.putString("Version", version);
editor.apply();
}
Map<String, Map<String, JSONObject>> inputMap = new HashMap<>();
saveNotificationsMap(context, inputMap);
}
} catch (PackageManager.NameNotFoundException e) {
NotificationHelper.cleanNotificationPreferencesIfNeeded(context);
} catch (Exception e) {
e.printStackTrace();
}
}
public static void cancelNotification(Context context, String channelId, String rootId, Integer notificationId, Boolean isCRTEnabled) {
if (!android.text.TextUtils.isEmpty(channelId)) {
final String notificationIdStr = notificationId.toString();
final Boolean isThreadNotification = isCRTEnabled && !android.text.TextUtils.isEmpty(rootId);
final String groupId = isThreadNotification ? rootId : channelId;
Map<String, Map<String, JSONObject>> notificationsInChannel = loadNotificationsMap(context);
Map<String, JSONObject> notifications = notificationsInChannel.get(groupId);
if (notifications == null) {
return;
}
final NotificationManager notificationManager = context.getSystemService(NotificationManager.class);
notificationManager.cancel(notificationId);
notifications.remove(notificationIdStr);
final StatusBarNotification[] statusNotifications = notificationManager.getActiveNotifications();
boolean hasMore = false;
for (final StatusBarNotification status : statusNotifications) {
Bundle bundle = status.getNotification().extras;
if (isThreadNotification) {
hasMore = bundle.getString("root_id").equals(rootId);
} else {
hasMore = bundle.getString("channel_id").equals(channelId);
}
if (hasMore) {
break;
}
}
if (!hasMore) {
notificationsInChannel.remove(groupId);
} else {
notificationsInChannel.put(groupId, notifications);
}
saveNotificationsMap(context, notificationsInChannel);
}
}
public static void clearChannelNotifications(Context context, String channelId, String rootId, Boolean isCRTEnabled) {
if (!android.text.TextUtils.isEmpty(channelId)) {
final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
// rootId is available only when CRT is enabled & clearing the thread
final boolean isClearThread = isCRTEnabled && !android.text.TextUtils.isEmpty(rootId);
Map<String, Map<String, JSONObject>> notificationsInChannel = loadNotificationsMap(context);
String groupId = isClearThread ? rootId : channelId;
Map<String, JSONObject> notifications = notificationsInChannel.get(groupId);
if (notifications == null) {
return;
}
notificationsInChannel.remove(groupId);
saveNotificationsMap(context, notificationsInChannel);
notifications.forEach(
(notificationIdStr, post) -> notificationManager.cancel(Integer.valueOf(notificationIdStr))
);
}
}
public static void clearAllNotifications(Context context) {
if (context != null) {
Map<String, Map<String, JSONObject>> notificationsInChannel = loadNotificationsMap(context);
notificationsInChannel.clear();
saveNotificationsMap(context, notificationsInChannel);
final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
notificationManager.cancelAll();
}
}
@Override
public void onReceived() {
final Bundle initialData = mNotificationProps.asBundle();
@ -155,15 +52,8 @@ public class CustomPushNotification extends PushNotification {
final String ackId = initialData.getString("ack_id");
final String postId = initialData.getString("post_id");
final String channelId = initialData.getString("channel_id");
final String rootId = initialData.getString("root_id");
final boolean isCRTEnabled = initialData.getString("is_crt_enabled") != null && initialData.getString("is_crt_enabled").equals("true");
final boolean isIdLoaded = initialData.getString("id_loaded") != null && initialData.getString("id_loaded").equals("true");
int notificationId = CustomPushNotificationHelper.MESSAGE_NOTIFICATION_ID;
if (postId != null) {
notificationId = postId.hashCode();
} else if (channelId != null) {
notificationId = channelId.hashCode();
}
int notificationId = NotificationHelper.getNotificationId(initialData);
String serverUrl = addServerUrlToBundle(initialData);
boolean isReactInit = mAppLifecycleFacade.isReactInitialized();
@ -176,7 +66,9 @@ public class CustomPushNotification extends PushNotification {
Bundle response = (Bundle) value;
if (value != null) {
response.putString("server_url", serverUrl);
mNotificationProps = createProps(response);
Bundle current = mNotificationProps.asBundle();
current.putAll(response);
mNotificationProps = createProps(current);
}
}
}
@ -191,52 +83,24 @@ public class CustomPushNotification extends PushNotification {
switch (type) {
case PUSH_TYPE_MESSAGE:
case PUSH_TYPE_SESSION:
boolean createSummary = type.equals(PUSH_TYPE_MESSAGE);
if (!mAppLifecycleFacade.isAppVisible()) {
boolean createSummary = type.equals(PUSH_TYPE_MESSAGE);
if (type.equals(PUSH_TYPE_MESSAGE)) {
if (channelId != null) {
Bundle notificationBundle = mNotificationProps.asBundle();
if (serverUrl != null && !isReactInit) {
// We will only fetch the data related to the notification on the native side
// as updating the data directly to the db removes the wal & shm files needed
// by watermelonDB, if the DB is updated while WDB is running it causes WDB to
// detect the database as malformed, thus the app stop working and a restart is required.
// Data will be fetch from within the JS context instead.
dataHelper.fetchAndStoreDataForPushNotification(mNotificationProps.asBundle());
}
try {
JSONObject post = new JSONObject();
if (!android.text.TextUtils.isEmpty(rootId)) {
post.put("root_id", rootId);
}
if (!android.text.TextUtils.isEmpty(postId)) {
post.put("post_id", postId);
}
final Boolean isThreadNotification = isCRTEnabled && post.has("root_id");
final String groupId = isThreadNotification ? rootId : channelId;
Map<String, Map<String, JSONObject>> notificationsInChannel = loadNotificationsMap(mContext);
Map<String, JSONObject> notifications = notificationsInChannel.get(groupId);
if (notifications == null) {
notifications = Collections.synchronizedMap(new HashMap<String, JSONObject>());
}
if (notifications.size() > 0) {
createSummary = false;
}
notifications.put(String.valueOf(notificationId), post);
if (createSummary) {
// Add the summary notification id as well
notifications.put(String.valueOf(notificationId + 1), new JSONObject());
}
notificationsInChannel.put(groupId, notifications);
saveNotificationsMap(mContext, notificationsInChannel);
} catch(Exception e) {
e.printStackTrace();
dataHelper.fetchAndStoreDataForPushNotification(notificationBundle);
}
createSummary = NotificationHelper.addNotificationToPreferences(
mContext,
notificationId,
notificationBundle
);
}
}
@ -244,7 +108,7 @@ public class CustomPushNotification extends PushNotification {
}
break;
case PUSH_TYPE_CLEAR:
clearChannelNotifications(mContext, channelId, rootId, isCRTEnabled);
NotificationHelper.clearChannelOrThreadNotifications(mContext, mNotificationProps.asBundle());
break;
}
@ -255,15 +119,11 @@ public class CustomPushNotification extends PushNotification {
@Override
public void onOpened() {
digestNotification();
if (mNotificationProps != null) {
digestNotification();
Bundle data = mNotificationProps.asBundle();
final String channelId = data.getString("channel_id");
final String rootId = data.getString("root_id");
final Boolean isCRTEnabled = data.getBoolean("is_crt_enabled");
if (channelId != null) {
clearChannelNotifications(mContext, channelId, rootId, isCRTEnabled);
Bundle data = mNotificationProps.asBundle();
NotificationHelper.clearChannelOrThreadNotifications(mContext, data);
}
}
@ -312,61 +172,4 @@ public class CustomPushNotification extends PushNotification {
return serverUrl;
}
private static void saveNotificationsMap(Context context, Map<String, Map<String, JSONObject>> inputMap) {
SharedPreferences pSharedPref = context.getSharedPreferences(PUSH_NOTIFICATIONS, Context.MODE_PRIVATE);
if (pSharedPref != null) {
JSONObject json = new JSONObject(inputMap);
String jsonString = json.toString();
SharedPreferences.Editor editor = pSharedPref.edit();
editor.remove(NOTIFICATIONS_IN_CHANNEL).apply();
editor.putString(NOTIFICATIONS_IN_CHANNEL, jsonString);
editor.apply();
}
}
/**
* Map Structure
*
* {
* channel_id1 | thread_id1: {
* notification_id1: {
* post_id: 'p1',
* root_id: 'r1',
* }
* }
* }
*
*/
private static Map<String, Map<String, JSONObject>> loadNotificationsMap(Context context) {
Map<String, Map<String, JSONObject>> outputMap = new HashMap<>();
if (context != null) {
SharedPreferences pSharedPref = context.getSharedPreferences(PUSH_NOTIFICATIONS, Context.MODE_PRIVATE);
try {
if (pSharedPref != null) {
String jsonString = pSharedPref.getString(NOTIFICATIONS_IN_CHANNEL, (new JSONObject()).toString());
JSONObject json = new JSONObject(jsonString);
// Can be a channel_id or thread_id
Iterator<String> groupIdsItr = json.keys();
while (groupIdsItr.hasNext()) {
String groupId = groupIdsItr.next();
JSONObject notificationsJSONObj = json.getJSONObject(groupId);
Map<String, JSONObject> notifications = new HashMap<>();
Iterator<String> notificationIdKeys = notificationsJSONObj.keys();
while(notificationIdKeys.hasNext()) {
String notificationId = notificationIdKeys.next();
JSONObject post = notificationsJSONObj.getJSONObject(notificationId);
notifications.put(notificationId, post);
}
outputMap.put(groupId, notifications);
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
return outputMap;
}
}

View file

@ -1,35 +0,0 @@
package com.mattermost.rnbeta;
import android.content.Context;
import com.wix.reactnativenotifications.core.AppLaunchHelper;
import com.wix.reactnativenotifications.core.notificationdrawer.PushNotificationsDrawer;
public class CustomPushNotificationDrawer extends PushNotificationsDrawer {
final protected Context mContext;
final protected AppLaunchHelper mAppLaunchHelper;
protected CustomPushNotificationDrawer(Context context, AppLaunchHelper appLaunchHelper) {
super(context, appLaunchHelper);
mContext = context;
mAppLaunchHelper = appLaunchHelper;
}
@Override
public void onAppInit() {
}
@Override
public void onAppVisible() {
}
@Override
public void onNotificationOpened() {
}
@Override
public void onCancelAllLocalNotifications() {
CustomPushNotification.clearAllNotifications(mContext);
cancelAllScheduledNotifications();
}
}

View file

@ -1,13 +1,12 @@
package com.mattermost.rnbeta;
import com.facebook.react.bridge.JSIModuleSpec;
import com.facebook.react.bridge.JavaScriptContextHolder;
import android.content.Context;
import android.os.Bundle;
import android.util.Log;
import java.io.File;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@ -40,10 +39,9 @@ import com.facebook.soloader.SoLoader;
import com.mattermost.networkclient.RCTOkHttpClientFactory;
import com.mattermost.newarchitecture.MainApplicationReactNativeHost;
import com.swmansion.reanimated.ReanimatedJSIModulePackage;
import com.nozbe.watermelondb.jsi.WatermelonDBJSIPackage;
public class MainApplication extends NavigationApplication implements INotificationsApplication, INotificationsDrawerApplication {
public class MainApplication extends NavigationApplication implements INotificationsApplication {
public static MainApplication instance;
public Boolean sharedExtensionIsOpened = false;
@ -70,8 +68,8 @@ public class MainApplication extends NavigationApplication implements INotificat
switch (name) {
case "MattermostManaged":
return MattermostManagedModule.getInstance(reactContext);
case "NotificationPreferences":
return NotificationPreferencesModule.getInstance(instance, reactContext);
case "Notifications":
return NotificationsModule.getInstance(instance, reactContext);
default:
throw new IllegalArgumentException("Could not find module " + name);
}
@ -82,7 +80,7 @@ public class MainApplication extends NavigationApplication implements INotificat
return () -> {
Map<String, ReactModuleInfo> map = new HashMap<>();
map.put("MattermostManaged", new ReactModuleInfo("MattermostManaged", "com.mattermost.rnbeta.MattermostManagedModule", false, false, false, false, false));
map.put("NotificationPreferences", new ReactModuleInfo("NotificationPreferences", "com.mattermost.rnbeta.NotificationPreferencesModule", false, false, false, false, false));
map.put("Notifications", new ReactModuleInfo("Notifications", "com.mattermost.rnbeta.NotificationsModule", false, false, false, false, false));
return map;
};
}
@ -94,18 +92,11 @@ public class MainApplication extends NavigationApplication implements INotificat
@Override
protected JSIModulePackage getJSIModulePackage() {
return new JSIModulePackage() {
@Override
public List<JSIModuleSpec> getJSIModules(
final ReactApplicationContext reactApplicationContext,
final JavaScriptContextHolder jsContext
) {
List<JSIModuleSpec> modules = Arrays.asList();
modules.addAll(new WatermelonDBJSIPackage().getJSIModules(reactApplicationContext, jsContext));
modules.addAll(new ReanimatedJSIModulePackage().getJSIModules(reactApplicationContext, jsContext));
return (reactApplicationContext, jsContext) -> {
List<JSIModuleSpec> modules = Collections.emptyList();
modules.addAll(new WatermelonDBJSIPackage().getJSIModules(reactApplicationContext, jsContext));
return modules;
}
return modules;
};
}
@ -161,11 +152,6 @@ public class MainApplication extends NavigationApplication implements INotificat
);
}
@Override
public IPushNotificationsDrawer getPushNotificationsDrawer(Context context, AppLaunchHelper defaultAppLaunchHelper) {
return new CustomPushNotificationDrawer(context, defaultAppLaunchHelper);
}
/**
* Loads Flipper in React Native templates. Call this in the onCreate method with something like
* initializeFlipper(this, getReactNativeHost().getReactInstanceManager());

View file

@ -6,7 +6,7 @@ import android.app.IntentService;
import android.os.Bundle;
import android.util.Log;
import com.mattermost.helpers.CustomPushNotificationHelper;
import com.mattermost.helpers.NotificationHelper;
import com.wix.reactnativenotifications.core.NotificationIntentAdapter;
public class NotificationDismissService extends IntentService {
@ -18,19 +18,8 @@ public class NotificationDismissService extends IntentService {
protected void onHandleIntent(Intent intent) {
final Context context = getApplicationContext();
final Bundle bundle = NotificationIntentAdapter.extractPendingNotificationDataFromIntent(intent);
final String channelId = bundle.getString("channel_id");
final String postId = bundle.getString("post_id");
final String rootId = bundle.getString("root_id");
final Boolean isCRTEnabled = bundle.getString("is_crt_enabled") != null && bundle.getString("is_crt_enabled").equals("true");
int notificationId = CustomPushNotificationHelper.MESSAGE_NOTIFICATION_ID;
if (postId != null) {
notificationId = postId.hashCode();
} else if (channelId != null) {
notificationId = channelId.hashCode();
}
CustomPushNotification.cancelNotification(context, channelId, rootId, notificationId, isCRTEnabled);
NotificationHelper.dismissNotification(context, bundle);
Log.i("ReactNative", "Dismiss notification");
}
}

View file

@ -1,70 +0,0 @@
package com.mattermost.rnbeta;
import android.app.Notification;
import android.app.NotificationManager;
import android.content.Context;
import android.os.Bundle;
import android.service.notification.StatusBarNotification;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.WritableArray;
import com.facebook.react.bridge.WritableMap;
public class NotificationPreferencesModule extends ReactContextBaseJavaModule {
private static NotificationPreferencesModule instance;
private final MainApplication mApplication;
private NotificationPreferencesModule(MainApplication application, ReactApplicationContext reactContext) {
super(reactContext);
mApplication = application;
Context context = mApplication.getApplicationContext();
}
public static NotificationPreferencesModule getInstance(MainApplication application, ReactApplicationContext reactContext) {
if (instance == null) {
instance = new NotificationPreferencesModule(application, reactContext);
}
return instance;
}
public static NotificationPreferencesModule getInstance() {
return instance;
}
@Override
public String getName() {
return "NotificationPreferences";
}
@ReactMethod
public void getDeliveredNotifications(final Promise promise) {
final Context context = mApplication.getApplicationContext();
final NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
StatusBarNotification[] statusBarNotifications = notificationManager.getActiveNotifications();
WritableArray result = Arguments.createArray();
for (StatusBarNotification sbn:statusBarNotifications) {
WritableMap map = Arguments.createMap();
Notification n = sbn.getNotification();
Bundle bundle = n.extras;
String postId = bundle.getString("post_id");
map.putString("post_id", postId);
String rootId = bundle.getString("root_id");
map.putString("root_id", rootId);
String channelId = bundle.getString("channel_id");
map.putString("channel_id", channelId);
result.pushMap(map);
}
promise.resolve(result);
}
@ReactMethod
public void removeDeliveredNotifications(String channelId, String rootId, Boolean isCRTEnabled) {
Context context = mApplication.getApplicationContext();
CustomPushNotification.clearChannelNotifications(context, channelId, rootId, isCRTEnabled);
}
}

View file

@ -0,0 +1,80 @@
package com.mattermost.rnbeta;
import android.app.Notification;
import android.app.NotificationManager;
import android.content.Context;
import android.os.Bundle;
import android.service.notification.StatusBarNotification;
import androidx.annotation.NonNull;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.WritableArray;
import com.facebook.react.bridge.WritableMap;
import com.mattermost.helpers.NotificationHelper;
import java.util.Set;
public class NotificationsModule extends ReactContextBaseJavaModule {
private static NotificationsModule instance;
private final MainApplication mApplication;
private NotificationsModule(MainApplication application, ReactApplicationContext reactContext) {
super(reactContext);
mApplication = application;
}
public static NotificationsModule getInstance(MainApplication application, ReactApplicationContext reactContext) {
if (instance == null) {
instance = new NotificationsModule(application, reactContext);
}
return instance;
}
@NonNull
@Override
public String getName() {
return "Notifications";
}
@ReactMethod
public void getDeliveredNotifications(final Promise promise) {
Context context = mApplication.getApplicationContext();
StatusBarNotification[] notifications = NotificationHelper.getDeliveredNotifications(context);
WritableArray result = Arguments.createArray();
for (StatusBarNotification sbn:notifications) {
WritableMap map = Arguments.createMap();
Notification n = sbn.getNotification();
Bundle bundle = n.extras;
Set<String> keys = bundle.keySet();
for (String key: keys) {
map.putString(key, bundle.getString(key));
}
result.pushMap(map);
}
promise.resolve(result);
}
@ReactMethod
public void removeChannelNotifications(String serverUrl, String channelId) {
Context context = mApplication.getApplicationContext();
NotificationHelper.removeChannelNotifications(context, serverUrl, channelId);
}
@ReactMethod
public void removeThreadNotifications(String serverUrl, String threadId) {
Context context = mApplication.getApplicationContext();
NotificationHelper.removeThreadNotifications(context, serverUrl, threadId);
}
@ReactMethod
public void removeServerNotifications(String serverUrl) {
Context context = mApplication.getApplicationContext();
NotificationHelper.removeServerNotifications(context, serverUrl);
}
}

View file

@ -17,7 +17,6 @@ import {
import {queryPreferencesByCategoryAndName} from '@queries/servers/preference';
import {prepareCommonSystemValues, PrepareCommonSystemValuesArgs, getCommonSystemValues, getCurrentTeamId, setCurrentChannelId, getCurrentUserId} from '@queries/servers/system';
import {addChannelToTeamHistory, addTeamToTeamHistory, getTeamById, removeChannelFromTeamHistory} from '@queries/servers/team';
import {getIsCRTEnabled} from '@queries/servers/thread';
import {getCurrentUser, queryUsersById} from '@queries/servers/user';
import {dismissAllModalsAndPopToRoot, dismissAllModalsAndPopToScreen} from '@screens/navigation';
import EphemeralStore from '@store/ephemeral_store';
@ -176,8 +175,7 @@ export async function markChannelAsViewed(serverUrl: string, channelId: string,
m.viewedAt = member.lastViewedAt;
m.lastViewedAt = Date.now();
});
const isCRTEnabled = await getIsCRTEnabled(database);
PushNotifications.cancelChannelNotifications(channelId, undefined, isCRTEnabled);
PushNotifications.removeChannelNotifications(serverUrl, channelId);
if (!prepareRecordsOnly) {
await operator.batchRecords([member]);
}

View file

@ -111,6 +111,8 @@ const restNotificationEntry = async (serverUrl: string, teamId: string, channelI
const isCRTEnabled = await getIsCRTEnabled(database);
const isThreadNotification = isCRTEnabled && Boolean(rootId);
await operator.batchRecords(models);
let switchedToScreen = false;
let switchedToChannel = false;
if (myChannel && myTeam) {
@ -129,15 +131,15 @@ const restNotificationEntry = async (serverUrl: string, teamId: string, channelI
// Make switch again to get the missing data and make sure the team is the correct one
switchedToScreen = true;
if (isThreadNotification) {
fetchAndSwitchToThread(serverUrl, rootId, true);
await fetchAndSwitchToThread(serverUrl, rootId, true);
} else {
switchedToChannel = true;
switchToChannelById(serverUrl, selectedChannelId, selectedTeamId);
await switchToChannelById(serverUrl, selectedChannelId, selectedTeamId);
}
} else if (selectedTeamId !== teamId || selectedChannelId !== channelId) {
// If in the end the selected team or channel is different than the one from the notification
// we switch again
setCurrentTeamAndChannelId(operator, selectedTeamId, selectedChannelId);
await setCurrentTeamAndChannelId(operator, selectedTeamId, selectedChannelId);
}
}
@ -147,13 +149,19 @@ const restNotificationEntry = async (serverUrl: string, teamId: string, channelI
emitNotificationError('Channel');
}
await operator.batchRecords(models);
const {id: currentUserId, locale: currentUserLocale} = (await getCurrentUser(operator.database))!;
const {config, license} = await getCommonSystemValues(operator.database);
const lastDisconnectedAt = await getWebSocketLastDisconnected(database);
await deferredAppEntryActions(serverUrl, lastDisconnectedAt, currentUserId, currentUserLocale, prefData.preferences, config, license, teamData, chData, selectedTeamId, switchedToChannel ? selectedChannelId : undefined);
// Waiting for the screen to display fixes a race condition when fetching and storing data
if (switchedToChannel) {
await NavigationStore.waitUntilScreenHasLoaded(Screens.CHANNEL);
} else if (switchedToScreen && isThreadNotification) {
await NavigationStore.waitUntilScreenHasLoaded(Screens.THREAD);
}
await deferredAppEntryActions(serverUrl, lastDisconnectedAt, currentUserId, currentUserLocale, prefData.preferences, config, license, teamData, chData, selectedTeamId, selectedChannelId);
return {userId: currentUserId};
};

View file

@ -5,6 +5,7 @@ import {Platform} from 'react-native';
import {updatePostSinceCache, updatePostsInThreadsSinceCache} from '@actions/local/notification';
import {fetchDirectChannelsInfo, fetchMyChannel, switchToChannelById} from '@actions/remote/channel';
import {fetchPostsForChannel, fetchPostThread} from '@actions/remote/post';
import {forceLogoutIfNecessary} from '@actions/remote/session';
import {fetchMyTeam} from '@actions/remote/team';
import {fetchAndSwitchToThread} from '@actions/remote/thread';
@ -73,6 +74,18 @@ const fetchNotificationData = async (serverUrl: string, notification: Notificati
}
}
if (Platform.OS === 'android') {
// on Android we only fetched the post data on the native side
// when the RN context is not running, thus we need to fetch the
// data here as well
const isCRTEnabled = await getIsCRTEnabled(database);
const isThreadNotification = isCRTEnabled && Boolean(notification.payload?.root_id);
if (isThreadNotification) {
fetchPostThread(serverUrl, notification.payload!.root_id!);
} else {
fetchPostsForChannel(serverUrl, channelId);
}
}
return {};
} catch (error) {
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);

View file

@ -54,7 +54,7 @@ export const fetchAndSwitchToThread = async (serverUrl: string, rootId: string,
}
}
switchToThread(serverUrl, rootId, isFromNotification);
await switchToThread(serverUrl, rootId, isFromNotification);
return {};
};
@ -186,7 +186,11 @@ export const markThreadAsRead = async (serverUrl: string, teamId: string | undef
const isCRTEnabled = await getIsCRTEnabled(database);
const post = await getPostById(database, threadId);
if (post) {
PushNotifications.cancelChannelNotifications(post.channelId, threadId, isCRTEnabled);
if (isCRTEnabled) {
PushNotifications.removeThreadNotifications(serverUrl, threadId);
} else {
PushNotifications.removeChannelNotifications(serverUrl, post.channelId);
}
}
return {data};

View file

@ -35,12 +35,13 @@ export const subscribeServerUnreadAndMentions = (serverUrl: string, observer: Un
let subscription: Subscription|undefined;
if (server?.database) {
subscription = server.database.get<MyChannelModel>(MY_CHANNEL).
subscription = server.database.
get<MyChannelModel>(MY_CHANNEL).
query(Q.on(CHANNEL, Q.where('delete_at', Q.eq(0)))).
observeWithColumns(['is_unread', 'mentions_count']).
pipe(
combineLatestWith(observeAllMyChannelNotifyProps(server.database)),
combineLatestWith(observeUnreadsAndMentionsInTeam(server.database, undefined, false)),
combineLatestWith(observeUnreadsAndMentionsInTeam(server.database, undefined, true)),
map$(([[myChannels, settings], {unreads, mentions}]) => ({myChannels, settings, threadUnreads: unreads, threadMentionCount: mentions})),
).
subscribe(observer);
@ -59,7 +60,7 @@ export const subscribeMentionsByServer = (serverUrl: string, observer: ServerUnr
query(Q.on(CHANNEL, Q.where('delete_at', Q.eq(0)))).
observeWithColumns(['mentions_count']).
pipe(
combineLatestWith(observeThreadMentionCount(server.database, undefined, false)),
combineLatestWith(observeThreadMentionCount(server.database, undefined, true)),
map$(([myChannels, threadMentionCount]) => ({myChannels, threadMentionCount})),
).
subscribe(observer.bind(undefined, serverUrl));
@ -78,7 +79,7 @@ export const subscribeUnreadAndMentionsByServer = (serverUrl: string, observer:
observeWithColumns(['mentions_count', 'is_unread']).
pipe(
combineLatestWith(observeAllMyChannelNotifyProps(server.database)),
combineLatestWith(observeUnreadsAndMentionsInTeam(server.database, undefined, false)),
combineLatestWith(observeUnreadsAndMentionsInTeam(server.database, undefined, true)),
map$(([[myChannels, settings], {unreads, mentions}]) => ({myChannels, settings, threadUnreads: unreads, threadMentionCount: mentions})),
).
subscribe(observer.bind(undefined, serverUrl));

View file

@ -20,7 +20,6 @@ import {backgroundNotification, openNotification} from '@actions/remote/notifica
import {markThreadAsRead} from '@actions/remote/thread';
import {Device, Events, Navigation, Screens} from '@constants';
import DatabaseManager from '@database/manager';
import {getTotalMentionsForServer} from '@database/subscription/unreads';
import {DEFAULT_LOCALE, getLocalizedMessage, t} from '@i18n';
import NativeNotifications from '@notifications';
import {queryServerName} from '@queries/app/servers';
@ -52,60 +51,6 @@ class PushNotifications {
Notifications.events().registerNotificationReceivedForeground(this.onNotificationReceivedForeground);
}
cancelAllLocalNotifications = () => {
Notifications.cancelAllLocalNotifications();
};
cancelChannelNotifications = async (channelId: string, rootId?: string, isCRTEnabled?: boolean) => {
const notifications = await NativeNotifications.getDeliveredNotifications();
this.cancelNotificationsForChannel(notifications, channelId, rootId, isCRTEnabled);
};
cancelChannelsNotifications = async (channelIds: string[]) => {
const notifications = await NativeNotifications.getDeliveredNotifications();
for (const channelId of channelIds) {
this.cancelNotificationsForChannel(notifications, channelId);
}
};
cancelNotificationsForChannel = (notifications: NotificationWithChannel[], channelId: string, rootId?: string, isCRTEnabled?: boolean) => {
if (Platform.OS === 'android') {
NativeNotifications.removeDeliveredNotifications(channelId, rootId, isCRTEnabled);
} else {
const ids: string[] = [];
const clearThreads = Boolean(rootId);
for (const notification of notifications) {
if (notification.channel_id === channelId) {
let doesNotificationMatch = true;
if (clearThreads) {
doesNotificationMatch = notification.thread === rootId;
} else if (isCRTEnabled) {
// Do not match when CRT is enabled BUT post is not a root post
doesNotificationMatch = !notification.root_id;
}
if (doesNotificationMatch) {
ids.push(notification.identifier);
}
}
}
if (ids.length) {
NativeNotifications.removeDeliveredNotifications(ids);
}
let badgeCount = notifications.length - ids.length;
const serversUrl = Object.keys(DatabaseManager.serverDatabases);
const mentionPromises = serversUrl.map((url) => getTotalMentionsForServer(url));
Promise.all(mentionPromises).then((result) => {
badgeCount += result.reduce((acc, count) => (acc + count), 0);
badgeCount = badgeCount <= 0 ? 0 : badgeCount;
Notifications.ios.setBadgeCount(badgeCount);
});
}
};
createReplyCategory = () => {
const replyTitle = getLocalizedMessage(DEFAULT_LOCALE, t('mobile.push_notification_reply.title'));
const replyButton = getLocalizedMessage(DEFAULT_LOCALE, t('mobile.push_notification_reply.button'));
@ -288,6 +233,18 @@ class PushNotifications {
return null;
};
removeChannelNotifications = async (serverUrl: string, channelId: string) => {
NativeNotifications.removeChannelNotifications(serverUrl, channelId);
};
removeServerNotifications = (serverUrl: string) => {
NativeNotifications.removeServerNotifications(serverUrl);
};
removeThreadNotifications = async (serverUrl: string, threadId: string) => {
NativeNotifications.removeThreadNotifications(serverUrl, threadId);
};
requestNotificationReplyPermissions = () => {
if (Platform.OS === 'ios') {
const replyCategory = this.createReplyCategory();

View file

@ -5,7 +5,6 @@ import CookieManager, {Cookie} from '@react-native-cookies/cookies';
import {Alert, DeviceEventEmitter, Linking, Platform} from 'react-native';
import semver from 'semver';
import {selectAllMyChannelIds} from '@actions/local/channel';
import LocalConfig from '@assets/config.json';
import {Events, Sso, Launch} from '@constants';
import DatabaseManager from '@database/manager';
@ -92,8 +91,7 @@ class GlobalEventHandler {
onLogout = async ({serverUrl, removeServer}: LogoutCallbackArg) => {
await removeServerCredentials(serverUrl);
const channelIds = await selectAllMyChannelIds(serverUrl);
PushNotifications.cancelChannelsNotifications(channelIds);
PushNotifications.removeServerNotifications(serverUrl);
NetworkManager.invalidateClient(serverUrl);
WebsocketManager.invalidateClient(serverUrl);

View file

@ -1,5 +1,15 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// @ts-expect-error platform specific
export {default} from './notifications';
import {NativeModules} from 'react-native';
const {Notifications} = NativeModules;
const nativeNotification: NativeNotification = {
getDeliveredNotifications: Notifications.getDeliveredNotifications,
removeChannelNotifications: Notifications.removeChannelNotifications,
removeThreadNotifications: Notifications.removeThreadNotifications,
removeServerNotifications: Notifications.removeServerNotifications,
};
export default nativeNotification;

View file

@ -1,42 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {NativeModules, PermissionsAndroid} from 'react-native';
const {NotificationPreferences} = NativeModules;
const defaultPreferences: NativeNotificationPreferences = {
sounds: [],
shouldBlink: false,
shouldVibrate: true,
};
const nativeNotification: NativeNotification = {
getDeliveredNotifications: NotificationPreferences.getDeliveredNotifications,
getPreferences: async () => {
try {
const hasPermission = await PermissionsAndroid.check(PermissionsAndroid.PERMISSIONS.READ_EXTERNAL_STORAGE);
let granted;
if (!hasPermission) {
granted = await PermissionsAndroid.request(
PermissionsAndroid.PERMISSIONS.READ_EXTERNAL_STORAGE,
);
}
if (hasPermission || granted === PermissionsAndroid.RESULTS.GRANTED) {
return await NotificationPreferences.getPreferences();
}
return defaultPreferences;
} catch (error) {
return defaultPreferences;
}
},
play: NotificationPreferences.previewSound,
removeDeliveredNotifications: NotificationPreferences.removeDeliveredNotifications,
setNotificationSound: NotificationPreferences.setNotificationSound,
setShouldBlink: NotificationPreferences.setShouldBlink,
setShouldVibrate: NotificationPreferences.setShouldVibrate,
};
export default nativeNotification;

View file

@ -1,18 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Notifications} from 'react-native-notifications';
import {emptyFunction} from '@utils/general';
const nativeNotification: NativeNotification = {
getDeliveredNotifications: async () => Notifications.ios.getDeliveredNotifications(),
getPreferences: async () => null,
play: (soundUri: string) => emptyFunction(soundUri),
removeDeliveredNotifications: async (ids: string[]) => Notifications.ios.removeDeliveredNotifications(ids),
setNotificationSound: () => emptyFunction(),
setShouldBlink: (shouldBlink: boolean) => emptyFunction(shouldBlink),
setShouldVibrate: (shouldVibrate: boolean) => emptyFunction(shouldVibrate),
};
export default nativeNotification;

View file

@ -2,13 +2,15 @@
// See LICENSE.txt for license information.
import React, {useEffect, useState} from 'react';
import {StyleSheet, View} from 'react-native';
import {Platform, StyleSheet, View} from 'react-native';
import {Notifications} from 'react-native-notifications';
import Badge from '@components/badge';
import CompassIcon from '@components/compass_icon';
import {BOTTOM_TAB_ICON_SIZE} from '@constants/view';
import {subscribeAllServers} from '@database/subscription/servers';
import {subscribeUnreadAndMentionsByServer, UnreadObserverArgs} from '@database/subscription/unreads';
import NativeNotification from '@notifications';
import {changeOpacity} from '@utils/theme';
import type ServersModel from '@typings/database/models/app/servers';
@ -48,6 +50,16 @@ const Home = ({isFocused, theme}: Props) => {
mentions += value.mentions;
});
setTotal({mentions, unread});
if (Platform.OS === 'ios') {
NativeNotification.getDeliveredNotifications().then((delivered) => {
if (mentions === 0 && delivered.length > 0) {
return;
}
Notifications.ios.setBadgeCount(mentions);
});
}
};
const unreadsSubscription = (serverUrl: string, {myChannels, settings, threadMentionCount}: UnreadObserverArgs) => {

View file

@ -0,0 +1,64 @@
import Foundation
public struct ChannelMemberData: Codable {
let channel_id: String
let mention_count: Int
let mention_count_root: Int
let user_id: String
let roles: String
let last_viewed_at: Int64
let last_update_at: Int64
public enum ChannelMemberKeys: String, CodingKey {
case channel_id, mention_count, mention_count_root, user_id, roles, last_viewed_at, last_update_at
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: ChannelMemberKeys.self)
channel_id = try container.decode(String.self, forKey: .channel_id)
mention_count = try container.decode(Int.self, forKey: .mention_count)
let mentions_root = try? container.decode(Int?.self, forKey: .mention_count_root) ?? 0
mention_count_root = mentions_root!
user_id = try container.decode(String.self, forKey: .user_id)
roles = try container.decode(String.self, forKey: .roles)
last_update_at = try container.decode(Int64.self, forKey: .last_update_at)
last_viewed_at = try container.decode(Int64.self, forKey: .last_viewed_at)
}
}
public struct ThreadData: Codable {
let id: String
let last_reply_at: Int64
let last_viewed_at: Int64
let reply_count: Int
let unread_replies: Int
let unread_mentions: Int
public enum ThreadKeys: String, CodingKey {
case id, last_reply_at, last_viewed_at, reply_count, unread_replies, unread_mentions
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: ThreadKeys.self)
id = try container.decode(String.self, forKey: .id)
last_viewed_at = try container.decode(Int64.self, forKey: .last_viewed_at)
last_reply_at = try container.decode(Int64.self, forKey: .last_reply_at)
reply_count = try container.decode(Int.self, forKey: .reply_count)
unread_replies = try container.decode(Int.self, forKey: .unread_replies)
unread_mentions = try container.decode(Int.self, forKey: .unread_mentions)
}
}
extension Network {
public func fetchChannelMentions(channelId: String, withServerUrl serverUrl: String, completionHandler: @escaping ResponseHandler) {
let endpoint = "/channels/\(channelId)/members/me"
let url = buildApiUrl(serverUrl, endpoint)
return request(url, withMethod: "GET", withServerUrl: serverUrl, completionHandler: completionHandler)
}
public func fetchThreadMentions(teamId: String, threadId: String, withServerUrl serverUrl: String, completionHandler: @escaping ResponseHandler) {
let endpoint = "/users/me/teams/\(teamId)/threads/\(threadId)"
let url = buildApiUrl(serverUrl, endpoint)
return request(url, withMethod: "GET", withServerUrl: serverUrl, completionHandler: completionHandler)
}
}

View file

@ -108,7 +108,6 @@ extension Network {
}
public func fetchAndStoreDataForPushNotification(_ notification: UNMutableNotificationContent, withContentHandler contentHandler: ((UNNotificationContent) -> Void)?) {
let operation = BlockOperation {
let group = DispatchGroup()
@ -121,11 +120,38 @@ extension Network {
let currentUsername = currentUser?[Expression<String>("username")]
var postData: PostData? = nil
var myChannelData: ChannelMemberData? = nil
var threadData: ThreadData? = nil
var threads: [Post] = []
var userIdsToLoad: Set<String> = Set()
var usernamesToLoad: Set<String> = Set()
var users: Set<User> = Set()
if isCRTEnabled && !rootId.isEmpty {
// Fetch the thread mentions
let teamId = Gekidou.Database.default.queryTeamIdForChannel(withId: channelId, withServerUrl: serverUrl) ?? ""
if !teamId.isEmpty {
group.enter()
self.fetchThreadMentions(teamId: teamId, threadId: rootId, withServerUrl: serverUrl, completionHandler: {data, response, error in
if self.responseOK(response), let data = data {
threadData = try? JSONDecoder().decode(ThreadData.self, from: data)
}
group.leave()
})
}
} else {
// Fetch the channel mentions
group.enter()
self.fetchChannelMentions(channelId: channelId, withServerUrl: serverUrl, completionHandler: { data, response, error in
if self.responseOK(response), let data = data {
myChannelData = try? JSONDecoder().decode(ChannelMemberData.self, from: data)
}
group.leave()
})
}
group.enter()
let since = try? Database.default.queryPostsSinceForChannel(withId: channelId, withServerUrl: serverUrl)
self.fetchPostsForChannel(withId: channelId, withSince: since, withServerUrl: serverUrl, withIsCRTEnabled: isCRTEnabled, withRootId: rootId) { data, response, error in
@ -242,18 +268,30 @@ extension Network {
let receivingThreads = isCRTEnabled && !rootId.isEmpty
try? db.transaction {
try? Database.default.handlePostData(db, postData!, channelId, since != nil, receivingThreads)
if (threads.count > 0) {
if threads.count > 0 {
try? Database.default.handleThreads(db, threads)
}
if (users.count > 0) {
if users.count > 0 {
try? Database.default.insertUsers(db, users)
}
if myChannelData != nil {
try? Database.default.handleMyChannelMentions(db, myChannelData!, withCRTEnabled: isCRTEnabled)
}
if threadData != nil {
try? Database.default.handleThreadMentions(db, threadData!)
}
}
}
}
group.leave()
if let contentHandler = contentHandler {
// Get the total mentions from all databases and set the badge icon
notification.badge = Gekidou.Database.default.getTotalMentions() as NSNumber
contentHandler(notification)
}
}

View file

@ -147,7 +147,7 @@ extension Database {
let onlyDMs = term.starts(with: "@") ? "AND c.type = 'D'" : ""
var username: String = ""
var displayName: String = ""
var searchTerm = term.removePrefix("@").lowercased()
let searchTerm = term.removePrefix("@").lowercased()
var bindings: [String] = []
if matchStart {
username = "u.username LIKE ?"

View file

@ -0,0 +1,159 @@
import Foundation
import SQLite
extension Database {
public func hasMyChannel(_ db: Connection, channelId: String) -> Bool {
let idCol = Expression<String>("id")
let query = myChannelTable.where(idCol == channelId)
if let _ = try? db.pluck(query) {
return true
}
return false
}
public func hasThread(_ db: Connection, threadId: String) -> Bool {
let idCol = Expression<String>("id")
let query = threadTable.where(idCol == threadId)
if let _ = try? db.pluck(query) {
return true
}
return false
}
public func getTotalMentions() -> Int {
let serverUrls = getAllActiveServerUrls()
var mentions = 0
for serverUrl in serverUrls {
if let db = try? getDatabaseForServer(serverUrl) {
mentions += (getChannelMentions(db) + getThreadMentions(db))
}
}
return mentions
}
public func getChannelMentions(_ db: Connection) -> Int {
let mentionsCol = Expression<Int?>("mentions_count")
let mentions = try? db.scalar(myChannelTable.select(mentionsCol.total))
return Int(mentions ?? 0)
}
public func getThreadMentions(_ db: Connection) -> Int {
let mentionsCol = Expression<Int?>("unread_mentions")
let mentions = try? db.scalar(threadTable.select(mentionsCol.total))
return Int(mentions ?? 0)
}
public func handleMyChannelMentions(_ db: Connection, _ channelMemberData: ChannelMemberData, withCRTEnabled crtEnabled: Bool) throws {
let idCol = Expression<String>("id")
let mentionsCol = Expression<Int>("mentions_count")
let isUnreadCol = Expression<Bool>("is_unread")
let mentions = crtEnabled ? channelMemberData.mention_count_root : channelMemberData.mention_count
if hasMyChannel(db, channelId: channelMemberData.channel_id) {
let updateQuery = myChannelTable
.where(idCol == channelMemberData.channel_id)
.update(mentionsCol <- mentions,
isUnreadCol <- true
)
let _ = try db.run(updateQuery)
} else {
let msgCol = Expression<Int>("message_count")
let lastPostAtCol = Expression<Int64>("last_post_at")
let lastViewedAtCol = Expression<Int64>("last_viewed_at")
let viewedAtCol = Expression<Int64>("viewed_at")
let lastFetchedAtCol = Expression<Int64>("last_fetched_at")
let manuallyUnreadCol = Expression<Bool>("manually_unread")
let rolesCol = Expression<String>("roles")
let statusCol = Expression<String>("status")
let setters: [Setter] = [
idCol <- channelMemberData.channel_id,
mentionsCol <- mentions,
msgCol <- mentions,
lastPostAtCol <- channelMemberData.last_update_at,
lastViewedAtCol <- channelMemberData.last_viewed_at,
viewedAtCol <- 0,
lastFetchedAtCol <- 0,
isUnreadCol <- true,
manuallyUnreadCol <- false,
rolesCol <- channelMemberData.roles,
statusCol <- "created"
]
let insertQuery = myChannelTable.insert(setters)
let _ = try db.run(insertQuery)
}
}
public func handleThreadMentions(_ db: Connection, _ threadData: ThreadData) throws {
let idCol = Expression<String>("id")
let unreadMentionsCol = Expression<Int>("unread_mentions")
if hasThread(db, threadId: threadData.id) {
let updateQuery = threadTable
.where(idCol == threadData.id)
.update(unreadMentionsCol <- threadData.unread_mentions)
let _ = try db.run(updateQuery)
} else {
let lastReplyAtCol = Expression<Int64>("last_reply_at")
let lastViewedAtCol = Expression<Int64>("last_viewed_at")
let viewedAtCol = Expression<Int64>("viewed_at")
let lastFetchedAtCol = Expression<Int64>("last_fetched_at")
let isFollowingCol = Expression<Bool>("is_following")
let unreadRepliesCol = Expression<Int>("unread_replies")
let replyCountCol = Expression<Int>("reply_count")
let statusCol = Expression<String>("status")
let setters: [Setter] = [
idCol <- threadData.id,
unreadMentionsCol <- threadData.unread_mentions,
lastReplyAtCol <- threadData.last_reply_at,
lastViewedAtCol <- threadData.last_viewed_at,
viewedAtCol <- 0,
lastFetchedAtCol <- 0,
isFollowingCol <- true,
unreadRepliesCol <- threadData.unread_replies,
replyCountCol <- threadData.reply_count,
statusCol <- "created"
]
let insertQuery = threadTable.insert(setters)
let _ = try db.run(insertQuery)
}
}
public func resetMyChannelMentions(_ serverUrl: String, _ channelId: String) throws {
if let db = try? getDatabaseForServer(serverUrl) {
let idCol = Expression<String>("id")
let mentionsCol = Expression<Int>("mentions_count")
let msgCol = Expression<Int>("message_count")
let isUnreadCol = Expression<Bool>("is_unread")
if hasMyChannel(db, channelId: channelId) {
let updateQuery = myChannelTable
.where(idCol == channelId)
.update(mentionsCol <- 0,
msgCol <- 0,
isUnreadCol <- false
)
let _ = try db.run(updateQuery)
}
}
}
public func resetThreadMentions(_ serverUrl: String, _ rootId: String) throws {
if let db = try? getDatabaseForServer(serverUrl) {
let idCol = Expression<String>("id")
let mentionsCol = Expression<Int>("unread_mentions")
let msgCol = Expression<Int>("unread_replies")
if hasThread(db, threadId: rootId) {
let updateQuery = threadTable
.where(idCol == rootId)
.update(mentionsCol <- 0, msgCol <- 0)
let _ = try db.run(updateQuery)
}
}
}
}

View file

@ -0,0 +1,36 @@
import Foundation
import SQLite
extension Database {
internal func queryCurrentTeamId(_ serverUrl: String) -> String? {
if let db = try? getDatabaseForServer(serverUrl) {
let idCol = Expression<String>("id")
let valueCol = Expression<String>("value")
let query = systemTable.where(idCol == "currentTeamId")
if let result = try? db.pluck(query) {
return try? result.get(valueCol).replacingOccurrences(of: "\"", with: "")
}
}
return nil
}
public func queryTeamIdForChannel(withId channelId: String, withServerUrl serverUrl: String) -> String? {
if let db = try? getDatabaseForServer(serverUrl) {
let idCol = Expression<String>("id")
let teamIdCol = Expression<String?>("team_id")
let query = channelTable.where(idCol == channelId)
if let result = try? db.pluck(query) {
var teamId = result[teamIdCol]
if teamId != nil || teamId!.isEmpty {
teamId = queryCurrentTeamId(serverUrl)
}
return teamId
}
}
return nil
}
}

View file

@ -153,6 +153,24 @@ public class Database: NSObject {
}
}
public func getAllActiveServerUrls() -> [String] {
guard let db = try? Connection(DEFAULT_DB_PATH) else {return []}
let lastActiveAt = Expression<Int64>("last_active_at")
let identifier = Expression<String>("identifier")
let url = Expression<String>("url")
let query = serversTable.filter(lastActiveAt > 0 && identifier != "").order(lastActiveAt.desc)
do {
let rows = try db.prepare(query)
let servers: [String] = try rows.map { row in
return try row.get(url)
}
return servers
} catch {
return []
}
}
public func getCurrentServerDatabase<T: Codable>() -> T? {
guard let db = try? Connection(DEFAULT_DB_PATH) else {return nil}
do {

View file

@ -24,6 +24,7 @@
7F4288042865D340006B48E1 /* Gekidou in Frameworks */ = {isa = PBXBuildFile; productRef = 7F4288032865D340006B48E1 /* Gekidou */; };
7F42880A286672F6006B48E1 /* ServerService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F428809286672F6006B48E1 /* ServerService.swift */; };
7F42880C2866A9C0006B48E1 /* ChannelService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F42880B2866A9C0006B48E1 /* ChannelService.swift */; };
7F537B2928A517580086D6B3 /* NotificationHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F537B2828A517580086D6B3 /* NotificationHelper.swift */; };
7F581D35221ED5C60099E66B /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F581D34221ED5C60099E66B /* NotificationService.swift */; };
7F581D39221ED5C60099E66B /* NotificationService.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 7F581D32221ED5C60099E66B /* NotificationService.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
7F7E9F462864E6C60064BFAF /* Color.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F7E9F442864E6C60064BFAF /* Color.swift */; };
@ -97,6 +98,7 @@
7FD482682864DC5900A5B18B /* OpenSans-SemiBold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = E5C16B14E1CE4868886A1A00 /* OpenSans-SemiBold.ttf */; };
7FD482692864DC5900A5B18B /* OpenSans-SemiBoldItalic.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 0DB14DFDF6E04FA69FE769DC /* OpenSans-SemiBoldItalic.ttf */; };
7FEB109D1F61019C0039A015 /* MattermostManaged.m in Sources */ = {isa = PBXBuildFile; fileRef = 7FEB109A1F61019C0039A015 /* MattermostManaged.m */; };
7FEC870128A4325D00DE96CB /* NotificationsModule.m in Sources */ = {isa = PBXBuildFile; fileRef = 7FEC870028A4325D00DE96CB /* NotificationsModule.m */; };
A94508A396424B2DB778AFE9 /* OpenSans-SemiBold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = E5C16B14E1CE4868886A1A00 /* OpenSans-SemiBold.ttf */; };
/* End PBXBuildFile section */
@ -184,6 +186,7 @@
7F428809286672F6006B48E1 /* ServerService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerService.swift; sourceTree = "<group>"; };
7F42880B2866A9C0006B48E1 /* ChannelService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelService.swift; sourceTree = "<group>"; };
7F43D6051F6BF9EB001FC614 /* libPods-Mattermost.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = "libPods-Mattermost.a"; path = "../../../../../../../Library/Developer/Xcode/DerivedData/Mattermost-czlinsdviifujheezzjvmisotjrm/Build/Products/Debug-iphonesimulator/libPods-Mattermost.a"; sourceTree = "<group>"; };
7F537B2828A517580086D6B3 /* NotificationHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = NotificationHelper.swift; path = Mattermost/NotificationHelper.swift; sourceTree = "<group>"; };
7F581D32221ED5C60099E66B /* NotificationService.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = NotificationService.appex; sourceTree = BUILT_PRODUCTS_DIR; };
7F581D34221ED5C60099E66B /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = "<group>"; };
7F581D36221ED5C60099E66B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
@ -237,6 +240,8 @@
7FD482282864D69700A5B18B /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
7FEB10991F61019C0039A015 /* MattermostManaged.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = MattermostManaged.h; path = Mattermost/MattermostManaged.h; sourceTree = "<group>"; };
7FEB109A1F61019C0039A015 /* MattermostManaged.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = MattermostManaged.m; path = Mattermost/MattermostManaged.m; sourceTree = "<group>"; };
7FEC870028A4325D00DE96CB /* NotificationsModule.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = NotificationsModule.m; path = Mattermost/NotificationsModule.m; sourceTree = "<group>"; };
7FEC870428A44A7B00DE96CB /* NotificationsModule.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = NotificationsModule.h; path = Mattermost/NotificationsModule.h; sourceTree = "<group>"; };
7FFE32B51FD9CCAA0038C7A0 /* FLAnimatedImage.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = FLAnimatedImage.framework; sourceTree = BUILT_PRODUCTS_DIR; };
7FFE32B61FD9CCAA0038C7A0 /* KSCrash.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = KSCrash.framework; sourceTree = BUILT_PRODUCTS_DIR; };
7FFE32B71FD9CCAA0038C7A0 /* KSCrash.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = KSCrash.framework; sourceTree = BUILT_PRODUCTS_DIR; };
@ -330,22 +335,19 @@
13B07FAE1A68108700A75B9A /* Mattermost */ = {
isa = PBXGroup;
children = (
536CC6C223E79287002C478C /* RNNotificationEventHandler+HandleReplyAction.h */,
536CC6C123E79287002C478C /* RNNotificationEventHandler+HandleReplyAction.m */,
7F537B2128A514CB0086D6B3 /* Extensions */,
7F537B2728A517430086D6B3 /* Helpers */,
7F537B2428A515730086D6B3 /* Modules */,
7F537B2328A515600086D6B3 /* Utils */,
7F537B2228A515400086D6B3 /* Wrappers */,
13B07FAF1A68108700A75B9A /* AppDelegate.h */,
13B07FB01A68108700A75B9A /* AppDelegate.mm */,
7F151D3D221B062700FAD8F3 /* RuntimeUtils.swift */,
13B07FB51A68108700A75B9A /* Images.xcassets */,
13B07FB61A68108700A75B9A /* Info.plist */,
13B07FB71A68108700A75B9A /* main.m */,
7F63D2C21E6DD98A001FAE12 /* Mattermost.entitlements */,
7F151D43221B082A00FAD8F3 /* Mattermost-Bridging-Header.h */,
7FEB10991F61019C0039A015 /* MattermostManaged.h */,
7FEB109A1F61019C0039A015 /* MattermostManaged.m */,
7F0F4B0924BA173900E14C60 /* LaunchScreen.storyboard */,
7FCEFB9126B7934F006DC1DE /* SDWebImageDownloaderOperation+Swizzle.h */,
7FCEFB9226B7934F006DC1DE /* SDWebImageDownloaderOperation+Swizzle.m */,
7F1EB88427FDE361002E7EEC /* GekidouWrapper.swift */,
);
name = Mattermost;
sourceTree = "<group>";
@ -414,6 +416,52 @@
path = ViewModels;
sourceTree = "<group>";
};
7F537B2128A514CB0086D6B3 /* Extensions */ = {
isa = PBXGroup;
children = (
536CC6C223E79287002C478C /* RNNotificationEventHandler+HandleReplyAction.h */,
536CC6C123E79287002C478C /* RNNotificationEventHandler+HandleReplyAction.m */,
7FCEFB9126B7934F006DC1DE /* SDWebImageDownloaderOperation+Swizzle.h */,
7FCEFB9226B7934F006DC1DE /* SDWebImageDownloaderOperation+Swizzle.m */,
);
name = Extensions;
sourceTree = "<group>";
};
7F537B2228A515400086D6B3 /* Wrappers */ = {
isa = PBXGroup;
children = (
7F1EB88427FDE361002E7EEC /* GekidouWrapper.swift */,
);
name = Wrappers;
sourceTree = "<group>";
};
7F537B2328A515600086D6B3 /* Utils */ = {
isa = PBXGroup;
children = (
7F151D3D221B062700FAD8F3 /* RuntimeUtils.swift */,
);
name = Utils;
sourceTree = "<group>";
};
7F537B2428A515730086D6B3 /* Modules */ = {
isa = PBXGroup;
children = (
7FEB10991F61019C0039A015 /* MattermostManaged.h */,
7FEB109A1F61019C0039A015 /* MattermostManaged.m */,
7FEC870428A44A7B00DE96CB /* NotificationsModule.h */,
7FEC870028A4325D00DE96CB /* NotificationsModule.m */,
);
name = Modules;
sourceTree = "<group>";
};
7F537B2728A517430086D6B3 /* Helpers */ = {
isa = PBXGroup;
children = (
7F537B2828A517580086D6B3 /* NotificationHelper.swift */,
);
name = Helpers;
sourceTree = "<group>";
};
7F581D33221ED5C60099E66B /* NotificationService */ = {
isa = PBXGroup;
children = (
@ -938,10 +986,12 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
7FEC870128A4325D00DE96CB /* NotificationsModule.m in Sources */,
7F1EB88527FDE361002E7EEC /* GekidouWrapper.swift in Sources */,
7FCEFB9326B7934F006DC1DE /* SDWebImageDownloaderOperation+Swizzle.m in Sources */,
13B07FBC1A68108700A75B9A /* AppDelegate.mm in Sources */,
7F151D3E221B062700FAD8F3 /* RuntimeUtils.swift in Sources */,
7F537B2928A517580086D6B3 /* NotificationHelper.swift in Sources */,
13B07FC11A68108700A75B9A /* main.m in Sources */,
7FEB109D1F61019C0039A015 /* MattermostManaged.m in Sources */,
536CC6C323E79287002C478C /* RNNotificationEventHandler+HandleReplyAction.m in Sources */,

View file

@ -120,9 +120,6 @@ NSString* const NOTIFICATION_TEST_ACTION = @"test";
-(void)application:(UIApplication *)application didReceiveRemoteNotification:(nonnull NSDictionary *)userInfo fetchCompletionHandler:(nonnull void (^)(UIBackgroundFetchResult))completionHandler {
UIApplicationState state = [UIApplication sharedApplication].applicationState;
NSString* action = [userInfo objectForKey:@"type"];
NSString* channelId = [userInfo objectForKey:@"channel_id"];
NSString* rootId = [userInfo objectForKey:@"root_id"];
BOOL isCRTEnabled = [userInfo objectForKey:@"is_crt_enabled"];
BOOL isClearAction = (action && [action isEqualToString: NOTIFICATION_CLEAR_ACTION]);
BOOL isTestAction = (action && [action isEqualToString: NOTIFICATION_TEST_ACTION]);
@ -136,7 +133,7 @@ NSString* const NOTIFICATION_TEST_ACTION = @"test";
// If received a notification that a channel was read, remove all notifications from that channel (only with app in foreground/background)
// When CRT is ON:
// When rootId is nil, clear channel's root post notifications or else clear all thread notifications
[self cleanNotificationsFromChannel:channelId :rootId :isCRTEnabled];
[[NotificationHelper default] clearChannelOrThreadNotificationsWithUserInfo:userInfo];
[[GekidouWrapper default] postNotificationReceipt:userInfo];
}
@ -197,42 +194,6 @@ NSString* const NOTIFICATION_TEST_ACTION = @"test";
return extraModules;
}
-(void)cleanNotificationsFromChannel:(NSString *)channelId :(NSString *)rootId :(BOOL)isCRTEnabled {
if ([UNUserNotificationCenter class]) {
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
[center getDeliveredNotificationsWithCompletionHandler:^(NSArray<UNNotification *> * _Nonnull notifications) {
NSMutableArray<NSString *> *notificationIds = [NSMutableArray new];
for (UNNotification *prevNotification in notifications) {
UNNotificationRequest *notificationRequest = [prevNotification request];
UNNotificationContent *notificationContent = [notificationRequest content];
NSString *identifier = [notificationRequest identifier];
NSString* cId = [[notificationContent userInfo] objectForKey:@"channel_id"];
NSString* pId = [[notificationContent userInfo] objectForKey:@"post_id"];
NSString* rId = [[notificationContent userInfo] objectForKey:@"root_id"];
if ([cId isEqualToString: channelId]) {
BOOL doesNotificationMatch = true;
if (isCRTEnabled) {
// Check if it is a thread notification
if (rootId != nil) {
doesNotificationMatch = [pId isEqualToString: rootId] || [rId isEqualToString: rootId];
} else {
// With CRT ON, remove notifications without rootId
doesNotificationMatch = rId == nil;
}
}
if (doesNotificationMatch) {
[notificationIds addObject:identifier];
}
}
}
[center removeDeliveredNotificationsWithIdentifiers:notificationIds];
}];
}
}
/*
https://mattermost.atlassian.net/browse/MM-10601
Required by react-native-hw-keyboard-event

View file

@ -0,0 +1,96 @@
import Foundation
import Gekidou
import UserNotifications
import UIKit
@objc class NotificationHelper: NSObject {
@objc public static let `default` = NotificationHelper()
private let notificationCenter = UNUserNotificationCenter.current()
@objc func getDeliveredNotifications(completionHandler: @escaping ([UNNotification]) -> Void) {
notificationCenter.getDeliveredNotifications(completionHandler: completionHandler)
}
@objc func clearChannelOrThreadNotifications(userInfo: NSDictionary) {
let channelId = userInfo["channel_id"] as? String
let rootId = userInfo["root_id"] as? String ?? ""
let crtEnabled = userInfo["is_crt_enabled"] as? Bool ?? false
let serverId = userInfo["server_id"] as? String ?? ""
let skipThreadNotification = !rootId.isEmpty && crtEnabled
guard let serverUrl = try? Gekidou.Database.default.getServerUrlForServer(serverId)
else { return }
if !skipThreadNotification && channelId != nil {
removeChannelNotifications(serverUrl: serverUrl, channelId: channelId!)
try? Gekidou.Database.default.resetMyChannelMentions(serverUrl, channelId!)
} else if !rootId.isEmpty {
removeThreadNotifications(serverUrl: serverUrl, threadId: rootId)
try? Gekidou.Database.default.resetThreadMentions(serverUrl, rootId)
}
let mentions = Gekidou.Database.default.getTotalMentions()
UIApplication.shared.applicationIconBadgeNumber = mentions
}
@objc func removeChannelNotifications(serverUrl: String, channelId: String) {
getDeliveredNotifications(completionHandler: {notifications in
var notificationIds = [String]()
for notification in notifications {
let request = notification.request
let content = request.content
let identifier = request.identifier
let cId = content.userInfo["channel_id"] as? String
let rootId = content.userInfo["root_id"] as? String ?? ""
let crtEnabled = content.userInfo["is_crt_enabled"] as? Bool ?? false
let skipThreadNotification = !rootId.isEmpty && crtEnabled
if cId == channelId && !skipThreadNotification {
notificationIds.append(identifier)
}
}
self.notificationCenter.removeDeliveredNotifications(withIdentifiers: notificationIds)
})
}
@objc func removeThreadNotifications(serverUrl: String, threadId: String) {
getDeliveredNotifications(completionHandler: {notifications in
var notificationIds = [String]()
for notification in notifications {
let request = notification.request
let content = request.content
let identifier = request.identifier
let postId = content.userInfo["post_id"] as? String
let rootId = content.userInfo["root_id"] as? String
if rootId == threadId || postId == threadId {
notificationIds.append(identifier)
}
}
self.notificationCenter.removeDeliveredNotifications(withIdentifiers: notificationIds)
})
}
@objc func removeServerNotifications(serverUrl: String) {
getDeliveredNotifications(completionHandler: {notifications in
var notificationIds = [String]()
for notification in notifications {
let request = notification.request
let content = request.content
let identifier = request.identifier
let url = content.userInfo["server_url"] as? String
if url == serverUrl {
notificationIds.append(identifier)
}
}
self.notificationCenter.removeDeliveredNotifications(withIdentifiers: notificationIds)
})
}
}

View file

@ -0,0 +1,6 @@
#import <React/RCTBridgeModule.h>
#import "RCTConvert+RNNotifications.h"
@interface NotificationsModule : NSObject <RCTBridgeModule>
@end

View file

@ -0,0 +1,32 @@
#import "Mattermost-Swift.h"
#import "NotificationsModule.h"
@implementation NotificationsModule
RCT_EXPORT_MODULE(Notifications)
RCT_EXPORT_METHOD(getDeliveredNotifications:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) {
[[NotificationHelper default] getDeliveredNotificationsWithCompletionHandler:^(NSArray<UNNotification *> * _Nonnull notifications) {
NSMutableArray<NSDictionary *> *formattedNotifications = [NSMutableArray new];
for (UNNotification *notification in notifications) {
[formattedNotifications addObject:[RCTConvert UNNotificationPayload:notification]];
}
resolve(formattedNotifications);
}];
}
RCT_EXPORT_METHOD(removeChannelNotifications:(NSString *) serverUrl channelId:(NSString*) channelId) {
[[NotificationHelper default] removeChannelNotificationsWithServerUrl:serverUrl channelId:channelId];
}
RCT_EXPORT_METHOD(removeThreadNotifications:(NSString *) serverUrl threadId:(NSString*) threadId) {
[[NotificationHelper default] removeThreadNotificationsWithServerUrl:serverUrl threadId:threadId];
}
RCT_EXPORT_METHOD(removeServerNotifications:(NSString *) serverUrl) {
[[NotificationHelper default] removeServerNotificationsWithServerUrl:serverUrl];
}
@end

View file

@ -3,6 +3,7 @@ import UserNotifications
class NotificationService: UNNotificationServiceExtension {
let preferences = Gekidou.Preferences.default
let fibonacciBackoffsInSeconds = [1.0, 2.0, 3.0, 5.0, 8.0]
var contentHandler: ((UNNotificationContent) -> Void)?
var bestAttemptContent: UNMutableNotificationContent?
@ -11,38 +12,6 @@ class NotificationService: UNNotificationServiceExtension {
override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
self.contentHandler = contentHandler
let fibonacciBackoffsInSeconds = [1.0, 2.0, 3.0, 5.0, 8.0]
func fetchReceipt(_ ackNotification: AckNotification) -> Void {
if (self.retryIndex >= fibonacciBackoffsInSeconds.count) {
contentHandler(self.bestAttemptContent!)
return
}
Network.default.postNotificationReceipt(ackNotification) { data, response, error in
if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode != 200 {
contentHandler(self.bestAttemptContent!)
return
}
guard let data = data, error == nil else {
if (ackNotification.isIdLoaded) {
// Receipt retrieval failed. Kick off retries.
let backoffInSeconds = fibonacciBackoffsInSeconds[self.retryIndex]
DispatchQueue.main.asyncAfter(deadline: .now() + backoffInSeconds, execute: {
fetchReceipt(ackNotification)
})
self.retryIndex += 1
}
return
}
self.processResponse(serverUrl: ackNotification.serverUrl, data: data, bestAttemptContent: self.bestAttemptContent!, contentHandler: contentHandler)
}
}
bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)
if let bestAttemptContent = bestAttemptContent,
let jsonData = try? JSONSerialization.data(withJSONObject: bestAttemptContent.userInfo),
@ -87,6 +56,37 @@ class NotificationService: UNNotificationServiceExtension {
contentHandler(bestAttemptContent)
}
}
func fetchReceipt(_ ackNotification: AckNotification) -> Void {
if (self.retryIndex >= self.fibonacciBackoffsInSeconds.count) {
self.contentHandler?(self.bestAttemptContent!)
return
}
Network.default.postNotificationReceipt(ackNotification) { data, response, error in
if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode != 200 {
self.contentHandler?(self.bestAttemptContent!)
return
}
guard let data = data, error == nil else {
if (ackNotification.isIdLoaded) {
// Receipt retrieval failed. Kick off retries.
let backoffInSeconds = self.fibonacciBackoffsInSeconds[self.retryIndex]
DispatchQueue.main.asyncAfter(deadline: .now() + backoffInSeconds, execute: {
self.fetchReceipt(ackNotification)
})
self.retryIndex += 1
}
return
}
self.processResponse(serverUrl: ackNotification.serverUrl, data: data, bestAttemptContent: self.bestAttemptContent!, contentHandler: self.contentHandler)
}
}
}
extension Date {

View file

@ -21,10 +21,10 @@ index 24cd226..4bfacba 100644
</manifest>
diff --git a/node_modules/react-native-notifications/lib/android/app/src/main/java/com/wix/reactnativenotifications/RNNotificationsModule.java b/node_modules/react-native-notifications/lib/android/app/src/main/java/com/wix/reactnativenotifications/RNNotificationsModule.java
index db9eaba..af65d0e 100644
index 90969b2..778cb29 100644
--- a/node_modules/react-native-notifications/lib/android/app/src/main/java/com/wix/reactnativenotifications/RNNotificationsModule.java
+++ b/node_modules/react-native-notifications/lib/android/app/src/main/java/com/wix/reactnativenotifications/RNNotificationsModule.java
@@ -100,7 +100,12 @@ public class RNNotificationsModule extends ReactContextBaseJavaModule implements
@@ -102,7 +102,12 @@ public class RNNotificationsModule extends ReactContextBaseJavaModule implements
if(BuildConfig.DEBUG) Log.d(LOGTAG, "Native method invocation: postLocalNotification");
final Bundle notificationProps = Arguments.toBundle(notificationPropsMap);
final IPushNotification pushNotification = PushNotification.get(getReactApplicationContext().getApplicationContext(), notificationProps);
@ -38,19 +38,6 @@ index db9eaba..af65d0e 100644
}
@ReactMethod
@@ -109,6 +114,12 @@ public class RNNotificationsModule extends ReactContextBaseJavaModule implements
notificationsDrawer.onNotificationClearRequest(notificationId);
}
+ @ReactMethod
+ public void cancelAllLocalNotifications() {
+ IPushNotificationsDrawer notificationDrawer = PushNotificationsDrawer.get(getReactApplicationContext().getApplicationContext());
+ notificationDrawer.onCancelAllLocalNotifications();
+ }
+
@ReactMethod
public void setCategories(ReadableArray categories) {
diff --git a/node_modules/react-native-notifications/lib/android/app/src/main/java/com/wix/reactnativenotifications/core/helpers/ScheduleNotificationHelper.java b/node_modules/react-native-notifications/lib/android/app/src/main/java/com/wix/reactnativenotifications/core/helpers/ScheduleNotificationHelper.java
new file mode 100644
index 0000000..dde4a2c
@ -173,7 +160,7 @@ index 0d70024..b9e6c88 100644
PushNotificationProps asProps();
}
diff --git a/node_modules/react-native-notifications/lib/android/app/src/main/java/com/wix/reactnativenotifications/core/notification/PushNotification.java b/node_modules/react-native-notifications/lib/android/app/src/main/java/com/wix/reactnativenotifications/core/notification/PushNotification.java
index b93f762..400e086 100644
index 54a5fb8..f38881e 100644
--- a/node_modules/react-native-notifications/lib/android/app/src/main/java/com/wix/reactnativenotifications/core/notification/PushNotification.java
+++ b/node_modules/react-native-notifications/lib/android/app/src/main/java/com/wix/reactnativenotifications/core/notification/PushNotification.java
@@ -8,6 +8,10 @@ import android.content.Context;
@ -338,18 +325,8 @@ index 0000000..58ff887
+ pushNotification.onPostScheduledRequest(notificationId);
+ }
+}
diff --git a/node_modules/react-native-notifications/lib/android/app/src/main/java/com/wix/reactnativenotifications/core/notificationdrawer/IPushNotificationsDrawer.java b/node_modules/react-native-notifications/lib/android/app/src/main/java/com/wix/reactnativenotifications/core/notificationdrawer/IPushNotificationsDrawer.java
index e22cd62..48aa1cd 100644
--- a/node_modules/react-native-notifications/lib/android/app/src/main/java/com/wix/reactnativenotifications/core/notificationdrawer/IPushNotificationsDrawer.java
+++ b/node_modules/react-native-notifications/lib/android/app/src/main/java/com/wix/reactnativenotifications/core/notificationdrawer/IPushNotificationsDrawer.java
@@ -11,4 +11,5 @@ public interface IPushNotificationsDrawer {
void onNotificationClearRequest(int id);
void onNotificationClearRequest(String tag, int id);
void onAllNotificationsClearRequest();
+ void onCancelAllLocalNotifications();
}
diff --git a/node_modules/react-native-notifications/lib/android/app/src/main/java/com/wix/reactnativenotifications/core/notificationdrawer/PushNotificationsDrawer.java b/node_modules/react-native-notifications/lib/android/app/src/main/java/com/wix/reactnativenotifications/core/notificationdrawer/PushNotificationsDrawer.java
index a14089f..1262e6d 100644
index a14089f..a339934 100644
--- a/node_modules/react-native-notifications/lib/android/app/src/main/java/com/wix/reactnativenotifications/core/notificationdrawer/PushNotificationsDrawer.java
+++ b/node_modules/react-native-notifications/lib/android/app/src/main/java/com/wix/reactnativenotifications/core/notificationdrawer/PushNotificationsDrawer.java
@@ -2,9 +2,15 @@ package com.wix.reactnativenotifications.core.notificationdrawer;
@ -368,17 +345,11 @@ index a14089f..1262e6d 100644
public class PushNotificationsDrawer implements IPushNotificationsDrawer {
@@ -62,4 +68,37 @@ public class PushNotificationsDrawer implements IPushNotificationsDrawer {
@@ -62,4 +68,31 @@ public class PushNotificationsDrawer implements IPushNotificationsDrawer {
final NotificationManager notificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.cancelAll();
}
+
+ @Override
+ public void onCancelAllLocalNotifications() {
+ onAllNotificationsClearRequest();
+ cancelAllScheduledNotifications();
+ }
+
+ protected void cancelAllScheduledNotifications() {
+ Log.i(LOGTAG, "Cancelling all scheduled notifications");
+ ScheduleNotificationHelper helper = ScheduleNotificationHelper.getInstance(mContext);
@ -428,43 +399,11 @@ index ad7fc1a..a04ec6b 100644
}
get title() {
return this.payload.title;
diff --git a/node_modules/react-native-notifications/lib/dist/Notifications.d.ts b/node_modules/react-native-notifications/lib/dist/Notifications.d.ts
index 6e49fd4..1d94217 100644
--- a/node_modules/react-native-notifications/lib/dist/Notifications.d.ts
+++ b/node_modules/react-native-notifications/lib/dist/Notifications.d.ts
@@ -37,6 +37,10 @@ export declare class NotificationsRoot {
* cancelLocalNotification
*/
cancelLocalNotification(notificationId: number): void;
+ /**
+ * cancelAllLocalNotifications
+ */
+ cancelAllLocalNotifications(): void;
/**
* removeAllDeliveredNotifications
*/
diff --git a/node_modules/react-native-notifications/lib/dist/Notifications.js b/node_modules/react-native-notifications/lib/dist/Notifications.js
index 44ab53f..8000701 100644
--- a/node_modules/react-native-notifications/lib/dist/Notifications.js
+++ b/node_modules/react-native-notifications/lib/dist/Notifications.js
@@ -55,6 +55,12 @@ class NotificationsRoot {
cancelLocalNotification(notificationId) {
return this.commands.cancelLocalNotification(notificationId);
}
+ /**
+ * cancelAllLocalNotifications
+ */
+ cancelAllLocalNotifications() {
+ this.commands.cancelAllLocalNotifications();
+ }
/**
* removeAllDeliveredNotifications
*/
diff --git a/node_modules/react-native-notifications/lib/ios/RNNotificationCenter.m b/node_modules/react-native-notifications/lib/ios/RNNotificationCenter.m
index 4b33656..0f189d6 100644
index afd5c73..6036dda 100644
--- a/node_modules/react-native-notifications/lib/ios/RNNotificationCenter.m
+++ b/node_modules/react-native-notifications/lib/ios/RNNotificationCenter.m
@@ -83,7 +83,7 @@
@@ -87,7 +87,7 @@
for (UNNotification *notification in notifications) {
[formattedNotifications addObject:[RCTConvert UNNotificationPayload:notification]];
}
@ -472,40 +411,4 @@ index 4b33656..0f189d6 100644
+ callback(formattedNotifications);
}];
}
diff --git a/node_modules/react-native-notifications/lib/src/Notifications.ts b/node_modules/react-native-notifications/lib/src/Notifications.ts
index 0848f6d..ceb271d 100644
--- a/node_modules/react-native-notifications/lib/src/Notifications.ts
+++ b/node_modules/react-native-notifications/lib/src/Notifications.ts
@@ -80,6 +80,13 @@ export class NotificationsRoot {
return this.commands.cancelLocalNotification(notificationId);
}
+ /**
+ * cancelAllLocalNotifications
+ */
+ public cancelAllLocalNotifications() {
+ this.commands.cancelAllLocalNotifications();
+ }
+
/**
* removeAllDeliveredNotifications
*/
diff --git a/node_modules/react-native-notifications/lib/src/NotificationsIOS.ts b/node_modules/react-native-notifications/lib/src/NotificationsIOS.ts
index 98fc19d..0c8ea3d 100644
--- a/node_modules/react-native-notifications/lib/src/NotificationsIOS.ts
+++ b/node_modules/react-native-notifications/lib/src/NotificationsIOS.ts
@@ -52,13 +52,6 @@ export class NotificationsIOS {
return this.commands.setBadgeCount(count);
}
- /**
- * cancelAllLocalNotifications
- */
- public cancelAllLocalNotifications() {
- this.commands.cancelAllLocalNotifications();
- }
-
/**
* checkPermissions
*/

View file

@ -92,6 +92,12 @@ jest.doMock('react-native', () => {
},
}),
},
Notifications: {
getDeliveredNotifications: jest.fn().mockResolvedValue([]),
removeChannelNotifications: jest.fn().mockImplementation(),
removeThreadNotifications: jest.fn().mockImplementation(),
removeServerNotifications: jest.fn().mockImplementation(),
},
APIClient: {
getConstants: () => ({
EVENTS: {
@ -301,7 +307,6 @@ jest.mock('react-native-notifications', () => {
setDeliveredNotifications: jest.fn((notifications) => {
deliveredNotifications = notifications;
}),
cancelAllLocalNotifications: jest.fn(),
NotificationAction: jest.fn(),
NotificationCategory: jest.fn(),
events: () => ({

View file

@ -1,24 +1,9 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
interface NativeNotificationSound {
name: string;
uri: string;
}
interface NativeNotificationPreferences {
selectedUri?: string;
shouldBlink: boolean;
shouldVibrate: boolean;
sounds: NativeNotificationSound[];
}
interface NativeNotification {
getDeliveredNotifications(): Promise<NotificationWithChannel[]>;
getPreferences(): Promise<NativeNotificationPreferences|null>;
play(soundUri: string): void;
removeDeliveredNotifications(identifier: string | string[], channelId?: string): void;
setNotificationSound(): void;
setShouldBlink(shouldBlink: boolean): void;
setShouldVibrate(shouldVibrate: boolean): void;
removeChannelNotifications(serverUrl: string, channelId: string): void;
removeThreadNotifications(serverUrl: string, threadId: string): void;
removeServerNotifications(serverUrl: string): void;
}