[Gekidou] refactor clean notifications (#6566)
This commit is contained in:
parent
25ae8fdb88
commit
c2f5092678
42 changed files with 1217 additions and 933 deletions
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 ->
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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? {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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};
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 ?"
|
||||
|
|
|
|||
159
ios/Gekidou/Sources/Gekidou/Storage/Database+Mentions.swift
Normal file
159
ios/Gekidou/Sources/Gekidou/Storage/Database+Mentions.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
36
ios/Gekidou/Sources/Gekidou/Storage/Database+Team.swift
Normal file
36
ios/Gekidou/Sources/Gekidou/Storage/Database+Team.swift
Normal 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
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 */,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
96
ios/Mattermost/NotificationHelper.swift
Normal file
96
ios/Mattermost/NotificationHelper.swift
Normal 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)
|
||||
})
|
||||
}
|
||||
}
|
||||
6
ios/Mattermost/NotificationsModule.h
Normal file
6
ios/Mattermost/NotificationsModule.h
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
#import <React/RCTBridgeModule.h>
|
||||
#import "RCTConvert+RNNotifications.h"
|
||||
|
||||
@interface NotificationsModule : NSObject <RCTBridgeModule>
|
||||
|
||||
@end
|
||||
32
ios/Mattermost/NotificationsModule.m
Normal file
32
ios/Mattermost/NotificationsModule.m
Normal 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
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
*/
|
||||
|
||||
|
|
@ -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: () => ({
|
||||
|
|
|
|||
21
types/global/native_notifications.d.ts
vendored
21
types/global/native_notifications.d.ts
vendored
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue