diff --git a/android/app/src/main/java/com/mattermost/helpers/PushNotificationDataHelper.kt b/android/app/src/main/java/com/mattermost/helpers/PushNotificationDataHelper.kt index 43e67739c5..dc0e00de32 100644 --- a/android/app/src/main/java/com/mattermost/helpers/PushNotificationDataHelper.kt +++ b/android/app/src/main/java/com/mattermost/helpers/PushNotificationDataHelper.kt @@ -2,8 +2,8 @@ package com.mattermost.helpers import android.content.Context import android.os.Bundle -import android.text.TextUtils import android.util.Log +import com.facebook.react.bridge.Arguments import com.facebook.react.bridge.ReadableArray import com.facebook.react.bridge.ReadableMap @@ -17,13 +17,16 @@ import kotlinx.coroutines.sync.withLock class PushNotificationDataHelper(private val context: Context) { private var coroutineScope = CoroutineScope(Dispatchers.Default) - fun fetchAndStoreDataForPushNotification(initialData: Bundle) { + fun fetchAndStoreDataForPushNotification(initialData: Bundle, isReactInit: Boolean): Bundle? { + var result: Bundle? = null val job = coroutineScope.launch(Dispatchers.Default) { - PushNotificationDataRunnable.start(context, initialData) + result = PushNotificationDataRunnable.start(context, initialData, isReactInit) } runBlocking { job.join() } + + return result } } @@ -33,83 +36,73 @@ class PushNotificationDataRunnable { private val dbHelper = DatabaseHelper.instance!! private val mutex = Mutex() - suspend fun start(context: Context, initialData: Bundle) { + suspend fun start(context: Context, initialData: Bundle, isReactInit: Boolean): Bundle? { // for more info see: https://blog.danlew.net/2020/01/28/coroutines-and-java-synchronization-dont-mix/ mutex.withLock { - val serverUrl: String = initialData.getString("server_url") ?: return + val serverUrl: String = initialData.getString("server_url") ?: return null val db = dbHelper.getDatabaseForServer(context, serverUrl) + var result: Bundle? = null try { - val teamId = initialData.getString("team_id") - val channelId = initialData.getString("channel_id") - val rootId = initialData.getString("root_id") - val isCRTEnabled = initialData.getString("is_crt_enabled") == "true" - Log.i("ReactNative", "Start fetching notification data in server=$serverUrl for channel=$channelId") - if (db != null) { - var teamData: ReadableMap? = null - var myTeamData: ReadableMap? = null - var channelData: ReadableMap? = null - var myChannelData: ReadableMap? = null - var loadedProfiles: ReadableArray? = null - var postData: ReadableMap? - var posts: ReadableMap? = null - var userIdsToLoad: ReadableArray? = null - var usernamesToLoad: ReadableArray? = null + val teamId = initialData.getString("team_id") + val channelId = initialData.getString("channel_id") + val postId = initialData.getString("post_id") + val rootId = initialData.getString("root_id") + val isCRTEnabled = initialData.getString("is_crt_enabled") == "true" + + Log.i("ReactNative", "Start fetching notification data in server=$serverUrl for channel=$channelId") - var threads: ReadableArray? = null - var usersFromThreads: ReadableArray? = null val receivingThreads = isCRTEnabled && !rootId.isNullOrEmpty() + val notificationData = Arguments.createMap() - coroutineScope { - if (teamId != null && !TextUtils.isEmpty(teamId)) { - val res = fetchTeamIfNeeded(db, serverUrl, teamId) - teamData = res.first - myTeamData = res.second - } - - if (channelId != null) { - val channelRes = fetchMyChannel(db, serverUrl, channelId, isCRTEnabled) - channelData = channelRes.first - myChannelData = channelRes.second - loadedProfiles = channelRes.third - - postData = fetchPosts(db, serverUrl, channelId, isCRTEnabled, rootId, loadedProfiles) - - posts = postData?.getMap("posts") - userIdsToLoad = postData?.getArray("userIdsToLoad") - usernamesToLoad = postData?.getArray("usernamesToLoad") - threads = postData?.getArray("threads") - usersFromThreads = postData?.getArray("usersFromThreads") - - if (userIdsToLoad != null && userIdsToLoad!!.size() > 0) { - val users = fetchUsersById(serverUrl, userIdsToLoad!!) - userIdsToLoad = users?.getArray("data") - } - - if (usernamesToLoad != null && usernamesToLoad!!.size() > 0) { - val users = fetchUsersByUsernames(serverUrl, usernamesToLoad!!) - usernamesToLoad = users?.getArray("data") - } - } + if (!teamId.isNullOrEmpty()) { + val res = fetchTeamIfNeeded(db, serverUrl, teamId) + res.first?.let { notificationData.putMap("team", it) } + res.second?.let { notificationData.putMap("myTeam", it) } } - db.transaction { - teamData?.let { insertTeam(db, it) } - myTeamData?.let { insertMyTeam(db, it) } - channelData?.let { handleChannel(db, it) } - myChannelData?.let { handleMyChannel(db, it) } + if (channelId != null && postId != null) { + val channelRes = fetchMyChannel(db, serverUrl, channelId, isCRTEnabled) + channelRes.first?.let { notificationData.putMap("channel", it) } + channelRes.second?.let { notificationData.putMap("myChannel", it) } + val loadedProfiles = channelRes.third - if (channelId != null) { - dbHelper.handlePosts(db, posts?.getMap("data"), channelId, receivingThreads) + // Fetch categories if needed + if (!teamId.isNullOrEmpty() && notificationData.getMap("myTeam") != null) { + // should load all categories + val res = fetchMyTeamCategories(db, serverUrl, teamId) + res?.let { notificationData.putMap("categories", it) } + } else if (notificationData.getMap("channel") != null) { + // check if the channel is in the category for the team + val res = addToDefaultCategoryIfNeeded(db, notificationData.getMap("channel")!!) + res?.let { notificationData.putArray("categoryChannels", it) } } - threads?.let { handleThreads(db, it) } + val postData = fetchPosts(db, serverUrl, channelId, isCRTEnabled, rootId, loadedProfiles) + postData?.getMap("posts")?.let { notificationData.putMap("posts", it) } - loadedProfiles?.let { handleUsers(db, it) } - userIdsToLoad?.let { handleUsers(db, it) } - usernamesToLoad?.let { handleUsers(db, it) } - usersFromThreads?.let { handleUsers(db, it) } + var notificationThread: ReadableMap? = null + if (isCRTEnabled && !rootId.isNullOrEmpty()) { + notificationThread = fetchThread(db, serverUrl, rootId, teamId) + } + + getThreadList(notificationThread, postData?.getArray("threads"))?.let { + val threadsArray = Arguments.createArray() + for(item in it) { + threadsArray.pushMap(item) + } + notificationData.putArray("threads", threadsArray) + } + + val userList = fetchNeededUsers(serverUrl, loadedProfiles, postData) + notificationData.putArray("users", ReadableArrayUtils.toWritableArray(userList.toArray())) + } + + result = Arguments.toBundle(notificationData) + + if (!isReactInit) { + dbHelper.saveToDatabase(db, notificationData, teamId, channelId, receivingThreads) } Log.i("ReactNative", "Done processing push notification=$serverUrl for channel=$channelId") @@ -120,7 +113,42 @@ class PushNotificationDataRunnable { db?.close() Log.i("ReactNative", "DONE fetching notification data") } + + return result } } + + private fun getThreadList(notificationThread: ReadableMap?, threads: ReadableArray?): ArrayList? { + threads?.let { + val threadsArray = ArrayList() + val threadIds = ArrayList() + notificationThread?.let { thread -> + thread.getString("id")?.let { it1 -> threadIds.add(it1) } + threadsArray.add(thread) + } + for(i in 0 until it.size()) { + val thread = it.getMap(i) + val threadId = thread.getString("id") + if (threadId != null) { + if (threadIds.contains(threadId)) { + // replace the values for participants and is_following + val index = threadsArray.indexOfFirst { el -> el.getString("id") == threadId } + val prev = threadsArray[index] + val merge = Arguments.createMap() + merge.merge(prev) + merge.putBoolean("is_following", thread.getBoolean("is_following")) + merge.putArray("participants", thread.getArray("participants")) + threadsArray[index] = merge + } else { + threadsArray.add(thread) + threadIds.add(threadId) + } + } + } + return threadsArray + } + + return null + } } } diff --git a/android/app/src/main/java/com/mattermost/helpers/ReadableArrayUtils.java b/android/app/src/main/java/com/mattermost/helpers/ReadableArrayUtils.java index 0da230fdfc..c7bad6827c 100644 --- a/android/app/src/main/java/com/mattermost/helpers/ReadableArrayUtils.java +++ b/android/app/src/main/java/com/mattermost/helpers/ReadableArrayUtils.java @@ -2,6 +2,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.ReadableType; import com.facebook.react.bridge.WritableArray; @@ -109,7 +110,9 @@ public class ReadableArrayUtils { writableArray.pushString((String) value); } else if (value instanceof Map) { writableArray.pushMap(ReadableMapUtils.toWritableMap((Map) value)); - } else if (value.getClass().isArray()) { + } else if (value instanceof ReadableMap) { + writableArray.pushMap((ReadableMap) value); + }else if (value.getClass().isArray()) { writableArray.pushArray(ReadableArrayUtils.toWritableArray((Object[]) value)); } } diff --git a/android/app/src/main/java/com/mattermost/helpers/database_extension/Category.kt b/android/app/src/main/java/com/mattermost/helpers/database_extension/Category.kt new file mode 100644 index 0000000000..0dcc83d721 --- /dev/null +++ b/android/app/src/main/java/com/mattermost/helpers/database_extension/Category.kt @@ -0,0 +1,87 @@ +package com.mattermost.helpers.database_extension + +import com.facebook.react.bridge.ReadableArray +import com.facebook.react.bridge.ReadableMap +import com.nozbe.watermelondb.Database + +fun insertCategory(db: Database, category: ReadableMap) { + try { + val id = category.getString("id") ?: return + val collapsed = false + val displayName = category.getString("display_name") + val muted = category.getBoolean("muted") + val sortOrder = category.getInt("sort_order") + val sorting = category.getString("sorting") ?: "recent" + val teamId = category.getString("team_id") + val type = category.getString("type") + + db.execute( + """ + INSERT INTO Category + (id, collapsed, display_name, muted, sort_order, sorting, team_id, type, _changed, _status) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, '', 'created') + """.trimIndent(), + arrayOf( + id, collapsed, displayName, muted, + sortOrder / 10, sorting, teamId, type + ) + ) + } catch (e: Exception) { + e.printStackTrace() + } +} + +fun insertCategoryChannels(db: Database, categoryId: String, teamId: String, channelIds: ReadableArray) { + try { + for (i in 0 until channelIds.size()) { + val channelId = channelIds.getString(i) + val id = "${teamId}_$channelId" + db.execute( + """ + INSERT INTO CategoryChannel + (id, category_id, channel_id, sort_order, _changed, _status) + VALUES (?, ?, ?, ?, '', 'created') + """.trimIndent(), + arrayOf(id, categoryId, channelId, i) + ) + } + } catch (e: Exception) { + e.printStackTrace() + } +} + +fun insertCategoriesWithChannels(db: Database, orderCategories: ReadableMap) { + val categories = orderCategories.getArray("categories") ?: return + for (i in 0 until categories.size()) { + val category = categories.getMap(i) + val id = category.getString("id") + val teamId = category.getString("team_id") + val channelIds = category.getArray("channel_ids") + insertCategory(db, category) + if (id != null && teamId != null) { + channelIds?.let { insertCategoryChannels(db, id, teamId, it) } + } + } +} + +fun insertChannelToDefaultCategory(db: Database, categoryChannels: ReadableArray) { + try { + for (i in 0 until categoryChannels.size()) { + val cc = categoryChannels.getMap(i) + val id = cc.getString("id") + val categoryId = cc.getString("category_id") + val channelId = cc.getString("channel_id") + val count = countByColumn(db, "CategoryChannel", "category_id", categoryId) + db.execute( + """ + INSERT INTO CategoryChannel + (id, category_id, channel_id, sort_order, _changed, _status) + VALUES (?, ?, ?, ?, '', 'created') + """.trimIndent(), + arrayOf(id, categoryId, channelId, if (count > 0) count + 1 else count) + ) + } + } catch (e: Exception) { + e.printStackTrace() + } +} diff --git a/android/app/src/main/java/com/mattermost/helpers/database_extension/Channel.kt b/android/app/src/main/java/com/mattermost/helpers/database_extension/Channel.kt index dd41120f5a..635b5b4f07 100644 --- a/android/app/src/main/java/com/mattermost/helpers/database_extension/Channel.kt +++ b/android/app/src/main/java/com/mattermost/helpers/database_extension/Channel.kt @@ -1,6 +1,7 @@ package com.mattermost.helpers.database_extension import com.facebook.react.bridge.ReadableMap +import com.mattermost.helpers.DatabaseHelper import com.mattermost.helpers.ReadableMapUtils import com.nozbe.watermelondb.Database import org.json.JSONException @@ -22,20 +23,6 @@ fun findMyChannel(db: Database?, channelId: String): Boolean { return false } -internal fun updateMyChannelLastFetchedAt(db: Database, channelId: String, lastFetchedAt: Double) { - try { - db.execute( - "UPDATE MyChannel SET last_fetched_at = ?, _status = 'updated' WHERE id = ?", - arrayOf( - lastFetchedAt, - channelId - ) - ) - } catch (e: Exception) { - e.printStackTrace() - } -} - internal fun handleChannel(db: Database, channel: ReadableMap) { try { val exists = channel.getString("id")?.let { findChannel(db, it) } ?: false @@ -50,10 +37,26 @@ internal fun handleChannel(db: Database, channel: ReadableMap) { } } -internal fun handleMyChannel(db: Database, myChannel: ReadableMap) { +internal fun DatabaseHelper.handleMyChannel(db: Database, myChannel: ReadableMap, postsData: ReadableMap?, receivingThreads: Boolean) { try { val json = ReadableMapUtils.toJSONObject(myChannel) val exists = myChannel.getString("id")?.let { findMyChannel(db, it) } ?: false + + if (postsData != null && !receivingThreads) { + val posts = ReadableMapUtils.toJSONObject(postsData.getMap("posts")).toMap() + val postList = posts.toList() + val lastFetchedAt = postList.fold(0.0) { acc, next -> + 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 + val value = maxOf(createAt, updateAt, deleteAt) + + maxOf(value, acc) + } + json.put("last_fetched_at", lastFetchedAt) + } + if (exists) { updateMyChannel(db, json) return @@ -85,8 +88,8 @@ fun insertChannel(db: Database, channel: JSONObject): Boolean { db.execute( """ INSERT INTO Channel - (id, create_at, delete_at, update_at, creator_id, display_name, name, team_id, type, is_group_constrained, shared, _status) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'created') + (id, create_at, delete_at, update_at, creator_id, display_name, name, team_id, type, is_group_constrained, shared, _changed, _status) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, '', 'created') """.trimIndent(), arrayOf( id, createAt, deleteAt, updateAt, @@ -110,8 +113,8 @@ fun insertChannelInfo(db: Database, channel: JSONObject) { db.execute( """ INSERT INTO ChannelInfo - (id, header, purpose, guest_count, member_count, pinned_post_count, _status) - VALUES (?, ?, ?, 0, 0, 0, 'created') + (id, header, purpose, guest_count, member_count, pinned_post_count, _changed, _status) + VALUES (?, ?, ?, 0, 0, 0, '', 'created') """.trimIndent(), arrayOf(id, header, purpose) ) @@ -130,15 +133,15 @@ fun insertMyChannel(db: Database, myChanel: JSONObject): Boolean { val lastPostAt = try { myChanel.getDouble("last_post_at") } catch (e: JSONException) { 0 } val lastViewedAt = try { myChanel.getDouble("last_viewed_at") } catch (e: JSONException) { 0 } val viewedAt = 0 - val lastFetchedAt = 0 + val lastFetchedAt = try { myChanel.getDouble("last_fetched_at") } catch (e: JSONException) { 0 } val manuallyUnread = false db.execute( """ INSERT INTO MyChannel (id, roles, message_count, mentions_count, is_unread, manually_unread, - last_post_at, last_viewed_at, viewed_at, last_fetched_at, _status) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'created') + last_post_at, last_viewed_at, viewed_at, last_fetched_at, _changed, _status) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, '', 'created') """, arrayOf( id, roles, msgCount, mentionsCount, isUnread, manuallyUnread, @@ -160,8 +163,8 @@ fun insertMyChannelSettings(db: Database, myChanel: JSONObject) { db.execute( """ - INSERT INTO MyChannelSettings (id, notify_props) - VALUES (?, ?) + INSERT INTO MyChannelSettings (id, notify_props, _changed, _status) + VALUES (?, ?, '', 'created') """, arrayOf(id, notifyProps) ) @@ -172,15 +175,15 @@ fun insertMyChannelSettings(db: Database, myChanel: JSONObject) { fun insertChannelMember(db: Database, myChanel: JSONObject) { try { - val userId = queryCurrentUserId(db)?.removeSurrounding("\"") ?: return + val userId = queryCurrentUserId(db) ?: return val channelId = try { myChanel.getString("id") } catch (e: JSONException) { return } val schemeAdmin = try { myChanel.getBoolean("scheme_admin") } catch (e: JSONException) { false } val id = "$channelId-$userId" db.execute( """ INSERT INTO ChannelMembership - (id, channel_id, user_id, scheme_admin, _status) - VALUES (?, ?, ?, ?, 'created') + (id, channel_id, user_id, scheme_admin, _changed, _status) + VALUES (?, ?, ?, ?, '', 'created') """, arrayOf(id, channelId, userId, schemeAdmin) ) @@ -198,14 +201,18 @@ fun updateMyChannel(db: Database, myChanel: JSONObject) { val isUnread = try { myChanel.getBoolean("is_unread") } catch (e: JSONException) { false } val lastPostAt = try { myChanel.getDouble("last_post_at") } catch (e: JSONException) { 0 } val lastViewedAt = try { myChanel.getDouble("last_viewed_at") } catch (e: JSONException) { 0 } + val lastFetchedAt = try { myChanel.getDouble("last_fetched_at") } catch (e: JSONException) { 0 } db.execute( """ UPDATE MyChannel SET message_count=?, mentions_count=?, is_unread=?, - last_post_at=?, last_viewed_at=?, _status = 'updated' + last_post_at=?, last_viewed_at=?, last_fetched_at=?, _status = 'updated' WHERE id=? """, - arrayOf(msgCount, mentionsCount, isUnread, lastPostAt, lastViewedAt, id) + arrayOf( + msgCount, mentionsCount, isUnread, + lastPostAt, lastViewedAt, lastFetchedAt, id + ) ) } catch (e: Exception) { e.printStackTrace() diff --git a/android/app/src/main/java/com/mattermost/helpers/database_extension/CustomEmoji.kt b/android/app/src/main/java/com/mattermost/helpers/database_extension/CustomEmoji.kt index 9d08f7c498..c9d9c01540 100644 --- a/android/app/src/main/java/com/mattermost/helpers/database_extension/CustomEmoji.kt +++ b/android/app/src/main/java/com/mattermost/helpers/database_extension/CustomEmoji.kt @@ -9,7 +9,7 @@ internal fun insertCustomEmojis(db: Database, customEmojis: JSONArray) { val emoji = customEmojis.getJSONObject(i) if (find(db, "CustomEmoji", emoji.getString("id")) == null) { db.execute( - "INSERT INTO CustomEmoji (id, name, _status) VALUES (?, ?, 'created')", + "INSERT INTO CustomEmoji (id, name, _changed, _status) VALUES (?, ?, '', 'created')", arrayOf( emoji.getString("id"), emoji.getString("name"), diff --git a/android/app/src/main/java/com/mattermost/helpers/database_extension/File.kt b/android/app/src/main/java/com/mattermost/helpers/database_extension/File.kt index 7158f69b32..460ec0aa24 100644 --- a/android/app/src/main/java/com/mattermost/helpers/database_extension/File.kt +++ b/android/app/src/main/java/com/mattermost/helpers/database_extension/File.kt @@ -20,8 +20,8 @@ internal fun insertFiles(db: Database, files: JSONArray) { db.execute( """ INSERT INTO File - (id, extension, height, image_thumbnail, local_path, mime_type, name, post_id, size, width, _status) - VALUES (?, ?, ?, ?, '', ?, ?, ?, ?, ?, 'created') + (id, extension, height, image_thumbnail, local_path, mime_type, name, post_id, size, width, _changed, _status) + VALUES (?, ?, ?, ?, '', ?, ?, ?, ?, ?, '', 'created') """.trimIndent(), arrayOf( id, extension, height, miniPreview, diff --git a/android/app/src/main/java/com/mattermost/helpers/database_extension/General.kt b/android/app/src/main/java/com/mattermost/helpers/database_extension/General.kt index 1461339f03..aefe81369a 100644 --- a/android/app/src/main/java/com/mattermost/helpers/database_extension/General.kt +++ b/android/app/src/main/java/com/mattermost/helpers/database_extension/General.kt @@ -6,9 +6,33 @@ import com.facebook.react.bridge.Arguments import com.facebook.react.bridge.ReadableMap import com.mattermost.helpers.DatabaseHelper import com.nozbe.watermelondb.Database +import com.nozbe.watermelondb.QueryArgs import com.nozbe.watermelondb.mapCursor -import java.lang.Exception import java.util.* +import kotlin.Exception + +internal fun DatabaseHelper.saveToDatabase(db: Database, data: ReadableMap, teamId: String?, channelId: String?, receivingThreads: Boolean) { + db.transaction { + val posts = data.getMap("posts") + data.getMap("team")?.let { insertTeam(db, it) } + data.getMap("myTeam")?.let { insertMyTeam(db, it) } + data.getMap("channel")?.let { handleChannel(db, it) } + data.getMap("myChannel")?.let { handleMyChannel(db, it, posts, receivingThreads) } + data.getMap("categories")?.let { insertCategoriesWithChannels(db, it) } + data.getArray("categoryChannels")?.let { insertChannelToDefaultCategory(db, it) } + if (channelId != null) { + handlePosts(db, posts, channelId, receivingThreads) + } + data.getArray("threads")?.let { + val threadsArray = ArrayList() + for (i in 0 until it.size()) { + threadsArray.add(it.getMap(i)) + } + handleThreads(db, threadsArray, teamId) + } + data.getArray("users")?.let { handleUsers(db, it) } + } +} fun DatabaseHelper.getServerUrlForIdentifier(identifier: String): String? { try { @@ -63,6 +87,27 @@ fun find(db: Database, tableName: String, id: String?): ReadableMap? { } } +fun findByColumns(db: Database, tableName: String, columnNames: Array, values: QueryArgs): ReadableMap? { + try { + val whereString = columnNames.joinToString(" AND ") { "$it = ?" } + db.rawQuery( + "SELECT * FROM $tableName WHERE $whereString LIMIT 1", + values + ).use { cursor -> + if (cursor.count <= 0) { + return null + } + val resultMap = Arguments.createMap() + cursor.moveToFirst() + resultMap.mapCursor(cursor) + return resultMap + } + } catch (e: Exception) { + e.printStackTrace() + return null + } +} + fun queryIds(db: Database, tableName: String, ids: Array): List { val list: MutableList = ArrayList() val args = TextUtils.join(",", Arrays.stream(ids).map { "?" }.toArray()) @@ -103,3 +148,21 @@ fun queryByColumn(db: Database, tableName: String, columnName: String, values: A } return list } + +fun countByColumn(db: Database, tableName: String, columnName: String, value: Any?): Int { + try { + db.rawQuery( + "SELECT COUNT(*) FROM $tableName WHERE $columnName == ? LIMIT 1", + arrayOf(value) + ).use { cursor -> + if (cursor.count <= 0) { + return 0 + } + cursor.moveToFirst() + return cursor.getInt(0) + } + } catch (e: Exception) { + e.printStackTrace() + return 0 + } +} diff --git a/android/app/src/main/java/com/mattermost/helpers/database_extension/Post.kt b/android/app/src/main/java/com/mattermost/helpers/database_extension/Post.kt index 6ff58d8439..9cd6f2bec8 100644 --- a/android/app/src/main/java/com/mattermost/helpers/database_extension/Post.kt +++ b/android/app/src/main/java/com/mattermost/helpers/database_extension/Post.kt @@ -57,6 +57,24 @@ fun queryPostSinceForChannel(db: Database?, channelId: String): Double? { return null } +fun queryLastPostInThread(db: Database?, rootId: String): Double? { + try { + if (db != null) { + val query = "SELECT create_at FROM Post WHERE root_id=? AND delete_at=0 ORDER BY create_at DESC LIMIT 1" + db.rawQuery(query, arrayOf(rootId)).use { cursor -> + if (cursor.count == 1) { + cursor.moveToFirst() + return cursor.getDouble(0) + } + } + } + } catch (e: Exception) { + e.printStackTrace() + } + + return null +} + internal fun insertPost(db: Database, post: JSONObject) { try { val id = try { post.getString("id") } catch (e: JSONException) { return } @@ -83,8 +101,8 @@ internal fun insertPost(db: Database, post: JSONObject) { """ INSERT INTO Post (id, channel_id, create_at, delete_at, update_at, edit_at, is_pinned, message, metadata, original_id, pending_post_id, - previous_post_id, root_id, type, user_id, props, _status) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'created') + previous_post_id, root_id, type, user_id, props, _changed, _status) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, '', 'created') """.trimIndent(), arrayOf( id, channelId, createAt, deleteAt, updateAt, editAt, @@ -173,20 +191,10 @@ fun DatabaseHelper.handlePosts(db: Database, postsData: ReadableMap?, channelId: val postList = posts.toList() var earliest = 0.0 var latest = 0.0 - var lastFetchedAt = 0.0 if (ordered != null && posts.isNotEmpty()) { val firstId = ordered.first() val lastId = ordered.last() - lastFetchedAt = postList.fold(0.0) { acc, next -> - 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 - val value = maxOf(createAt, updateAt, deleteAt) - - maxOf(value, acc) - } var prevPostId = "" val sortedPosts = postList.sortedBy { (_, value) -> @@ -213,18 +221,17 @@ fun DatabaseHelper.handlePosts(db: Database, postsData: ReadableMap?, channelId: } val jsonPost = JSONObject(post) - val rootId = post["root_id"] as? String - - if (!rootId.isNullOrEmpty()) { - var thread = postsInThread[rootId]?.toMutableList() - if (thread == null) { - thread = mutableListOf() - } - - thread.add(jsonPost) - postsInThread[rootId] = thread.toList() + val postId = post["id"] as? String ?: "" + val rootId = post["root_id"] as? String ?: "" + val postInThread = rootId.ifEmpty { postId } + var thread = postsInThread[postInThread]?.toMutableList() + if (thread == null) { + thread = mutableListOf() } + thread.add(jsonPost) + postsInThread[postInThread] = thread.toList() + if (find(db, "Post", key) == null) { insertPost(db, jsonPost) } else { @@ -240,7 +247,6 @@ fun DatabaseHelper.handlePosts(db: Database, postsData: ReadableMap?, channelId: if (!receivingThreads) { handlePostsInChannel(db, channelId, earliest, latest) - updateMyChannelLastFetchedAt(db, channelId, lastFetchedAt) } handlePostsInThread(db, postsInThread) } diff --git a/android/app/src/main/java/com/mattermost/helpers/database_extension/PostsInChannel.kt b/android/app/src/main/java/com/mattermost/helpers/database_extension/PostsInChannel.kt index 46a685a0c7..06a9abfa79 100644 --- a/android/app/src/main/java/com/mattermost/helpers/database_extension/PostsInChannel.kt +++ b/android/app/src/main/java/com/mattermost/helpers/database_extension/PostsInChannel.kt @@ -24,8 +24,8 @@ internal fun insertPostInChannel(db: Database, channelId: String, earliest: Doub db.execute( """ INSERT INTO PostsInChannel - (id, channel_id, earliest, latest, _status) - VALUES (?, ?, ?, ?, 'created') + (id, channel_id, earliest, latest, _changed, _status) + VALUES (?, ?, ?, ?, '', 'created') """.trimIndent(), arrayOf(id, channelId, earliest, latest)) diff --git a/android/app/src/main/java/com/mattermost/helpers/database_extension/Reaction.kt b/android/app/src/main/java/com/mattermost/helpers/database_extension/Reaction.kt index 38bd693e8a..2cb0c4f8fe 100644 --- a/android/app/src/main/java/com/mattermost/helpers/database_extension/Reaction.kt +++ b/android/app/src/main/java/com/mattermost/helpers/database_extension/Reaction.kt @@ -12,8 +12,8 @@ internal fun insertReactions(db: Database, reactions: JSONArray) { db.execute( """ INSERT INTO Reaction - (id, create_at, emoji_name, post_id, user_id, _status) - VALUES (?, ?, ?, ?, ?, 'created') + (id, create_at, emoji_name, post_id, user_id, _changed, _status) + VALUES (?, ?, ?, ?, ?, '', 'created') """.trimIndent(), arrayOf( id, diff --git a/android/app/src/main/java/com/mattermost/helpers/database_extension/System.kt b/android/app/src/main/java/com/mattermost/helpers/database_extension/System.kt index e8413af282..8d18379afd 100644 --- a/android/app/src/main/java/com/mattermost/helpers/database_extension/System.kt +++ b/android/app/src/main/java/com/mattermost/helpers/database_extension/System.kt @@ -4,8 +4,13 @@ import com.nozbe.watermelondb.Database import org.json.JSONObject fun queryCurrentUserId(db: Database): String? { - val result = find(db, "System", "currentUserId")!! - return result.getString("value") + val result = find(db, "System", "currentUserId") + return result?.getString("value")?.removeSurrounding("\"") +} + +fun queryCurrentTeamId(db: Database): String? { + val result = find(db, "System", "currentTeamId") + return result?.getString("value")?.removeSurrounding("\"") } fun queryConfigDisplayNameSetting(db: Database): String? { diff --git a/android/app/src/main/java/com/mattermost/helpers/database_extension/Team.kt b/android/app/src/main/java/com/mattermost/helpers/database_extension/Team.kt index 2d9d77c190..a3fbf6d59c 100644 --- a/android/app/src/main/java/com/mattermost/helpers/database_extension/Team.kt +++ b/android/app/src/main/java/com/mattermost/helpers/database_extension/Team.kt @@ -1,8 +1,10 @@ package com.mattermost.helpers.database_extension +import com.facebook.react.bridge.Arguments import com.facebook.react.bridge.NoSuchKeyException import com.facebook.react.bridge.ReadableMap import com.nozbe.watermelondb.Database +import com.nozbe.watermelondb.mapCursor fun findTeam(db: Database?, teamId: String): Boolean { if (db != null) { @@ -20,6 +22,22 @@ fun findMyTeam(db: Database?, teamId: String): Boolean { return false } +fun queryMyTeams(db: Database?): ArrayList? { + db?.rawQuery("SELECT * FROM MyTeam")?.use { cursor -> + val results = ArrayList() + if (cursor.count > 0) { + while(cursor.moveToNext()) { + val map = Arguments.createMap() + map.mapCursor(cursor) + results.add(map) + } + } + + return results + } + return null +} + fun insertTeam(db: Database, team: ReadableMap): Boolean { val id = try { team.getString("id") } catch (e: Exception) { return false } val deleteAt = try {team.getDouble("delete_at") } catch (e: Exception) { 0 } @@ -44,8 +62,8 @@ fun insertTeam(db: Database, team: ReadableMap): Boolean { """ INSERT INTO Team ( id, allow_open_invite, description, display_name, name, update_at, type, allowed_domains, - group_constrained, last_team_icon_update, invite_id, _status - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + group_constrained, last_team_icon_update, invite_id, _changed, _status + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, '', ?) """.trimIndent(), arrayOf( id, isAllowOpenInvite, description, displayName, name, updateAt, @@ -70,13 +88,13 @@ fun insertMyTeam(db: Database, myTeam: ReadableMap): Boolean { return try { db.execute( - "INSERT INTO MyTeam (id, roles, status) VALUES (?, ?, ?)", + "INSERT INTO MyTeam (id, roles, _changed, _status) VALUES (?, ?, '', ?)", arrayOf(id, roles, status) ) db.execute( """ - INSERT INTO TeamMembership (id, team_id, user_id, scheme_admin, status) - VALUES (?, ?, ?, ?, ?) + INSERT INTO TeamMembership (id, team_id, user_id, scheme_admin, _changed, _status) + VALUES (?, ?, ?, ?, '', ?) """.trimIndent(), arrayOf(membershipId, id, currentUserId, schemeAdmin, status) ) diff --git a/android/app/src/main/java/com/mattermost/helpers/database_extension/Thread.kt b/android/app/src/main/java/com/mattermost/helpers/database_extension/Thread.kt index 0bf1b3c038..1ed6f54111 100644 --- a/android/app/src/main/java/com/mattermost/helpers/database_extension/Thread.kt +++ b/android/app/src/main/java/com/mattermost/helpers/database_extension/Thread.kt @@ -71,8 +71,8 @@ internal fun insertThreadParticipants(db: Database, threadId: String, participan db.execute( """ INSERT INTO ThreadParticipant - (id, thread_id, user_id, _status) - VALUES (?, ?, ?, 'created') + (id, thread_id, user_id, _changed, _status) + VALUES (?, ?, ?, '', 'created') """.trimIndent(), arrayOf(id, threadId, participant.getString("id")) ) @@ -82,6 +82,45 @@ internal fun insertThreadParticipants(db: Database, threadId: String, participan } } +fun insertTeamThreadsSync(db: Database, teamId: String, earliest: Double, latest: Double) { + try { + val query = """ + INSERT INTO TeamThreadsSync (id, _changed, _status, earliest, latest) + VALUES (?, '', 'created', ?, ?) + """ + db.execute(query, arrayOf(teamId, earliest, latest)) + } catch (e: Exception) { + e.printStackTrace() + } +} + +fun updateTeamThreadsSync(db: Database, teamId: String, earliest: Double, latest: Double, existingRecord: ReadableMap) { + try { + val storeEarliest = minOf(earliest, existingRecord.getDouble("earliest")) + val storeLatest = maxOf(latest, existingRecord.getDouble("latest")) + val query = "UPDATE TeamThreadsSync SET earliest=?, latest=? WHERE id=?" + db.execute(query, arrayOf(storeEarliest, storeLatest, teamId)) + } catch (e: Exception) { + e.printStackTrace() + } +} + +fun syncParticipants(db: Database, thread: ReadableMap) { + try { + val threadId = thread.getString("id") + val participants = thread.getArray("participants") + if (participants != null) { + db.execute("DELETE FROM ThreadParticipant WHERE thread_id = ?", arrayOf(threadId)) + + if (participants.size() > 0) { + insertThreadParticipants(db, threadId!!, participants) + } + } + } catch (e: Exception) { + e.printStackTrace() + } +} + internal fun handlePostsInThread(db: Database, postsInThread: Map>) { postsInThread.forEach { (key, list) -> try { @@ -93,11 +132,13 @@ internal fun handlePostsInThread(db: Database, postsInThread: Map, teamId: String?) { + val teamIds = ArrayList() + if (teamId.isNullOrEmpty()) { + val myTeams = queryMyTeams(db) + if (myTeams != null) { + for (myTeam in myTeams) { + myTeam.getString("id")?.let { teamIds.add(it) } + } + } + } else { + teamIds.add(teamId) + } + + for (i in 0 until threads.size) { try { - val thread = threads.getMap(i) - val threadId = thread.getString("id") + val thread = threads[i] + handleThread(db, thread, teamIds) + } catch (e: Exception) { + e.printStackTrace() + } + } - // Insert/Update the thread - val existingRecord = find(db, "Thread", threadId) - if (existingRecord == null) { - insertThread(db, thread) - } else { - updateThread(db, thread, existingRecord) - } + handleTeamThreadsSync(db, threads, teamIds) +} - // Delete existing and insert thread participants - val participants = thread.getArray("participants") - if (participants != null) { - db.execute("DELETE FROM ThreadParticipant WHERE thread_id = ?", arrayOf(threadId)) +fun handleThread(db: Database, thread: ReadableMap, teamIds: ArrayList) { + // Insert/Update the thread + val threadId = thread.getString("id") + val isFollowing = thread.getBoolean("is_following") + val existingRecord = find(db, "Thread", threadId) + if (existingRecord == null) { + insertThread(db, thread) + } else { + updateThread(db, thread, existingRecord) + } - if (participants.size() > 0) { - insertThreadParticipants(db, threadId!!, participants) - } - } + syncParticipants(db, thread) + + // this is per team + if (isFollowing) { + for (teamId in teamIds) { + handleThreadInTeam(db, thread, teamId) + } + } +} + +fun handleThreadInTeam(db: Database, thread: ReadableMap, teamId: String) { + val threadId = thread.getString("id") ?: return + val existingRecord = findByColumns( + db, + "ThreadsInTeam", + arrayOf("thread_id", "team_id"), + arrayOf(threadId, teamId) + ) + if (existingRecord == null) { + try { + val id = RandomId.generate() + val query = """ + INSERT INTO ThreadsInTeam (id, team_id, thread_id, _changed, _status) + VALUES (?, ?, ?, '', 'created') + """ + db.execute(query, arrayOf(id, teamId, threadId)) } catch (e: Exception) { e.printStackTrace() } } } + +fun handleTeamThreadsSync(db: Database, threadList: ArrayList, teamIds: ArrayList) { + val sortedList = threadList.filter{ it.getBoolean("is_following") } + .sortedBy { it.getDouble("last_reply_at") } + .map { it.getDouble("last_reply_at") } + val earliest = sortedList.first() + val latest = sortedList.last() + + for (teamId in teamIds) { + val existingTeamThreadsSync = find(db, "TeamThreadsSync", teamId) + if (existingTeamThreadsSync == null) { + insertTeamThreadsSync(db, teamId, earliest, latest) + } else { + updateTeamThreadsSync(db, teamId, earliest, latest, existingTeamThreadsSync) + } + } +} diff --git a/android/app/src/main/java/com/mattermost/helpers/database_extension/User.kt b/android/app/src/main/java/com/mattermost/helpers/database_extension/User.kt index 391040fd00..a379bfc6ab 100644 --- a/android/app/src/main/java/com/mattermost/helpers/database_extension/User.kt +++ b/android/app/src/main/java/com/mattermost/helpers/database_extension/User.kt @@ -11,7 +11,7 @@ fun getLastPictureUpdate(db: Database?, userId: String): Double? { if (db != null) { var id = userId if (userId == "me") { - (queryCurrentUserId(db)?.removeSurrounding("\"") ?: userId).also { id = it } + (queryCurrentUserId(db) ?: userId).also { id = it } } val userQuery = "SELECT last_picture_update FROM User WHERE id=?" db.rawQuery(userQuery, arrayOf(id)).use { cursor -> @@ -30,7 +30,7 @@ fun getLastPictureUpdate(db: Database?, userId: String): Double? { fun getCurrentUserLocale(db: Database): String { try { - val currentUserId = queryCurrentUserId(db)?.removeSurrounding("\"") ?: return "en" + val currentUserId = queryCurrentUserId(db) ?: return "en" val userQuery = "SELECT locale FROM User WHERE id=?" db.rawQuery(userQuery, arrayOf(currentUserId)).use { cursor -> if (cursor.count == 1) { @@ -62,8 +62,8 @@ fun handleUsers(db: Database, users: ReadableArray) { """ INSERT INTO User (id, auth_service, update_at, delete_at, email, first_name, is_bot, is_guest, last_name, last_picture_update, locale, nickname, position, roles, status, username, notify_props, - props, timezone, _status) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'created') + props, timezone, _changed, _status) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, '', 'created') """.trimIndent(), arrayOf( user.getString("id"), diff --git a/android/app/src/main/java/com/mattermost/helpers/push_notification/Category.kt b/android/app/src/main/java/com/mattermost/helpers/push_notification/Category.kt new file mode 100644 index 0000000000..c0ba078f8d --- /dev/null +++ b/android/app/src/main/java/com/mattermost/helpers/push_notification/Category.kt @@ -0,0 +1,70 @@ +package com.mattermost.helpers.push_notification + +import com.facebook.react.bridge.Arguments +import com.facebook.react.bridge.ReadableArray +import com.facebook.react.bridge.ReadableMap +import com.mattermost.helpers.PushNotificationDataRunnable +import com.mattermost.helpers.database_extension.findByColumns +import com.mattermost.helpers.database_extension.queryCurrentUserId +import com.mattermost.helpers.database_extension.queryMyTeams +import com.nozbe.watermelondb.Database + +suspend fun PushNotificationDataRunnable.Companion.fetchMyTeamCategories(db: Database, serverUrl: String, teamId: String): ReadableMap? { + return try { + val userId = queryCurrentUserId(db) + val categories = fetch(serverUrl, "/api/v4/users/$userId/teams/$teamId/channels/categories") + categories?.getMap("data") + } catch (e: Exception) { + e.printStackTrace() + null + } +} + +fun PushNotificationDataRunnable.Companion.addToDefaultCategoryIfNeeded(db: Database, channel: ReadableMap): ReadableArray? { + val channelId = channel.getString("id") ?: return null + val channelType = channel.getString("type") + val categoryChannels = Arguments.createArray() + if (channelType == "D" || channelType == "G") { + val myTeams = queryMyTeams(db) + myTeams?.let { + for (myTeam in it) { + val map = categoryChannelForTeam(db, channelId, myTeam.getString("id"), "direct_messages") + if (map != null) { + categoryChannels.pushMap(map) + } + } + } + } else { + val map = categoryChannelForTeam(db, channelId, channel.getString("team_id"), "channels") + if (map != null) { + categoryChannels.pushMap(map) + } + } + + return categoryChannels +} + +private fun categoryChannelForTeam(db: Database, channelId: String, teamId: String?, type: String): ReadableMap? { + teamId?.let { id -> + val category = findByColumns(db, "Category", arrayOf("type", "team_id"), arrayOf(type, id)) + val categoryId = category?.getString("id") + categoryId?.let { cId -> + val cc = findByColumns( + db, + "CategoryChannel", + arrayOf("category_id", "channel_id"), + arrayOf(cId, channelId) + ) + if (cc == null) { + val map = Arguments.createMap() + map.putString("channel_id", channelId) + map.putString("category_id", cId) + map.putString("id", "${id}_$channelId") + return map + } + } + } + + return null +} + diff --git a/android/app/src/main/java/com/mattermost/helpers/push_notification/Channel.kt b/android/app/src/main/java/com/mattermost/helpers/push_notification/Channel.kt index 22f0f46d95..7cf96a6c60 100644 --- a/android/app/src/main/java/com/mattermost/helpers/push_notification/Channel.kt +++ b/android/app/src/main/java/com/mattermost/helpers/push_notification/Channel.kt @@ -58,61 +58,76 @@ suspend fun PushNotificationDataRunnable.Companion.fetchMyChannel(db: Database, } private suspend fun PushNotificationDataRunnable.Companion.fetchMyChannelData(serverUrl: String, channelId: String, isCRTEnabled: Boolean, channelData: ReadableMap): ReadableMap? { - val myChannel = fetch(serverUrl, "/api/v4/channels/$channelId/members/me") - val myChannelData = myChannel?.getMap("data") - if (myChannelData != null) { - val data = Arguments.createMap() - data.merge(myChannelData) - data.putString("id", channelId) - val totalMsg = if (isCRTEnabled) { - channelData.getInt("total_msg_count_root") - } else { - channelData.getInt("total_msg_count") - } - val myMsgCount = if (isCRTEnabled) { - myChannelData.getInt("msg_count_root") - } else { - myChannelData.getInt("msg_count") - } + try { + val myChannel = fetch(serverUrl, "/api/v4/channels/$channelId/members/me") + val myChannelData = myChannel?.getMap("data") + if (myChannelData != null) { + val data = Arguments.createMap() + data.merge(myChannelData) + data.putString("id", channelId) - val mentionCount = if (isCRTEnabled) { - myChannelData.getInt("mention_count_root") - } else { - myChannelData.getInt("mention_count") - } + val totalMsg = if (isCRTEnabled) { + channelData.getInt("total_msg_count_root") + } else { + channelData.getInt("total_msg_count") + } - val lastPostAt = if (isCRTEnabled) { - try { channelData.getDouble("last_root_post_at") } - catch (e: Exception) { channelData.getDouble("last_post_at") } - } else { - channelData.getDouble("last_post_at") - } + val myMsgCount = if (isCRTEnabled) { + myChannelData.getInt("msg_count_root") + } else { + myChannelData.getInt("msg_count") + } - data.putInt("message_count", 0.coerceAtLeast(totalMsg - myMsgCount)) - data.putInt("mentions_count", mentionCount) - data.putBoolean("is_unread", myMsgCount > 0) - data.putDouble("last_post_at", lastPostAt) - return data + val mentionCount = if (isCRTEnabled) { + myChannelData.getInt("mention_count_root") + } else { + myChannelData.getInt("mention_count") + } + + val lastPostAt = if (isCRTEnabled) { + try { + channelData.getDouble("last_root_post_at") + } catch (e: Exception) { + channelData.getDouble("last_post_at") + } + } else { + channelData.getDouble("last_post_at") + } + + val messageCount = 0.coerceAtLeast(totalMsg - myMsgCount) + data.putInt("message_count", messageCount) + data.putInt("mentions_count", mentionCount) + data.putBoolean("is_unread", messageCount > 0) + data.putDouble("last_post_at", lastPostAt) + return data + } + } catch (e: Exception) { + e.printStackTrace() } return null } private suspend fun PushNotificationDataRunnable.Companion.fetchProfileInChannel(db: Database, serverUrl: String, channelId: String): ReadableArray? { - val currentUserId = queryCurrentUserId(db)?.removeSurrounding("\"") - val profilesInChannel = fetch(serverUrl, "/api/v4/users?in_channel=${channelId}&page=0&per_page=8&sort=") - val profilesArray = profilesInChannel?.getArray("data") - val result = Arguments.createArray() - if (profilesArray != null) { - for (i in 0 until profilesArray.size()) { - val profile = profilesArray.getMap(i) - if (profile.getString("id") != currentUserId) { - result.pushMap(profile) + return try { + val currentUserId = queryCurrentUserId(db) + val profilesInChannel = fetch(serverUrl, "/api/v4/users?in_channel=${channelId}&page=0&per_page=8&sort=") + val profilesArray = profilesInChannel?.getArray("data") + val result = Arguments.createArray() + if (profilesArray != null) { + for (i in 0 until profilesArray.size()) { + val profile = profilesArray.getMap(i) + if (profile.getString("id") != currentUserId) { + result.pushMap(profile) + } } } - } - return result + result + } catch (e: Exception) { + e.printStackTrace() + null + } } private fun PushNotificationDataRunnable.Companion.displayUsername(user: ReadableMap, displayNameSetting: String): String { diff --git a/android/app/src/main/java/com/mattermost/helpers/push_notification/Post.kt b/android/app/src/main/java/com/mattermost/helpers/push_notification/Post.kt index 6b6bb449c3..c1694dcb13 100644 --- a/android/app/src/main/java/com/mattermost/helpers/push_notification/Post.kt +++ b/android/app/src/main/java/com/mattermost/helpers/push_notification/Post.kt @@ -1,6 +1,7 @@ package com.mattermost.helpers.push_notification import com.facebook.react.bridge.Arguments +import com.facebook.react.bridge.NoSuchKeyException import com.facebook.react.bridge.ReadableArray import com.facebook.react.bridge.ReadableMap import com.facebook.react.bridge.WritableNativeArray @@ -10,144 +11,172 @@ import com.mattermost.helpers.ReadableMapUtils import com.mattermost.helpers.database_extension.* import com.nozbe.watermelondb.Database -internal suspend fun PushNotificationDataRunnable.Companion.fetchPosts(db: Database, serverUrl: String, channelId: String, isCRTEnabled: Boolean, rootId: String?, loadedProfiles: ReadableArray?): ReadableMap? { - val regex = Regex("""\B@(([a-z\d-._]*[a-z\d_])[.-]*)""", setOf(RegexOption.IGNORE_CASE)) - val since = queryPostSinceForChannel(db, channelId) - val currentUserId = queryCurrentUserId(db)?.removeSurrounding("\"") - val currentUser = find(db, "User", currentUserId) - val currentUsername = currentUser?.getString("username") +internal suspend fun PushNotificationDataRunnable.Companion.fetchPosts( + db: Database, serverUrl: String, channelId: String, isCRTEnabled: Boolean, + rootId: String?, loadedProfiles: ReadableArray? +): ReadableMap? { + return try { + val regex = Regex("""\B@(([a-z\d-._]*[a-z\d_])[.-]*)""", setOf(RegexOption.IGNORE_CASE)) + val currentUserId = queryCurrentUserId(db) + val currentUser = find(db, "User", currentUserId) + val currentUsername = currentUser?.getString("username") - var additionalParams = "" - if (isCRTEnabled) { - additionalParams = "&collapsedThreads=true&collapsedThreadsExtended=true" - } + var additionalParams = "" + if (isCRTEnabled) { + additionalParams = "&collapsedThreads=true&collapsedThreadsExtended=true" + } - val receivingThreads = isCRTEnabled && !rootId.isNullOrEmpty() - val endpoint = if (receivingThreads) { - val queryParams = "?skipFetchThreads=false&perPage=60&fromCreatedAt=0&direction=up" - "/api/v4/posts/$rootId/thread$queryParams$additionalParams" - } else { - val queryParams = if (since == null) "?page=0&per_page=60" else "?since=${since.toLong()}" - "/api/v4/channels/$channelId/posts$queryParams$additionalParams" - } + val receivingThreads = isCRTEnabled && !rootId.isNullOrEmpty() + val endpoint = if (receivingThreads) { + val since = rootId?.let { queryLastPostInThread(db, it) } + val queryParams = if (since == null) "?perPage=60&fromCreatedAt=0&direction=up" else + "?fromCreateAt=${since.toLong()}&direction=down" - val postsResponse = fetch(serverUrl, endpoint) - val results = Arguments.createMap() + "/api/v4/posts/$rootId/thread$queryParams$additionalParams" + } else { + val since = queryPostSinceForChannel(db, channelId) + val queryParams = if (since == null) "?page=0&per_page=60" else "?since=${since.toLong()}" + "/api/v4/channels/$channelId/posts$queryParams$additionalParams" + } - if (postsResponse != null) { - val data = ReadableMapUtils.toMap(postsResponse) - results.putMap("posts", postsResponse) - val postsData = data["data"] as? Map<*, *> - if (postsData != null) { - val postsMap = postsData["posts"] - if (postsMap != null) { - @Suppress("UNCHECKED_CAST") - val posts = ReadableMapUtils.toWritableMap(postsMap as? Map) - val iterator = posts.keySetIterator() - val userIds = mutableListOf() - val usernames = mutableListOf() + val postsResponse = fetch(serverUrl, endpoint) + val postData = postsResponse?.getMap("data") + val results = Arguments.createMap() - val threads = WritableNativeArray() - val threadParticipantUserIds = mutableListOf() // Used to exclude the "userIds" present in the thread participants - val threadParticipantUsernames = mutableListOf() // Used to exclude the "usernames" present in the thread participants - val threadParticipantUsers = HashMap() // All unique users from thread participants are stored here - val userIdsAlreadyLoaded = mutableListOf() - if (loadedProfiles != null) { - for( i in 0 until loadedProfiles.size()) { - loadedProfiles.getMap(i).getString("id")?.let { userIdsAlreadyLoaded.add(it) } + if (postData != null) { + val data = ReadableMapUtils.toMap(postData) + results.putMap("posts", postData) + if (data != null) { + val postsMap = data["posts"] + if (postsMap != null) { + @Suppress("UNCHECKED_CAST") + val posts = ReadableMapUtils.toWritableMap(postsMap as? Map) + val iterator = posts.keySetIterator() + val userIds = mutableListOf() + val usernames = mutableListOf() + + val threads = WritableNativeArray() + val threadParticipantUserIds = mutableListOf() // Used to exclude the "userIds" present in the thread participants + val threadParticipantUsernames = mutableListOf() // Used to exclude the "usernames" present in the thread participants + val threadParticipantUsers = HashMap() // All unique users from thread participants are stored here + val userIdsAlreadyLoaded = mutableListOf() + if (loadedProfiles != null) { + for (i in 0 until loadedProfiles.size()) { + loadedProfiles.getMap(i).getString("id")?.let { userIdsAlreadyLoaded.add(it) } + } } - } - while(iterator.hasNextKey()) { - val key = iterator.nextKey() - val post = posts.getMap(key) - val userId = post?.getString("user_id") - if (userId != null && userId != currentUserId && !userIdsAlreadyLoaded.contains(userId) && !userIds.contains(userId)) { - userIds.add(userId) - } - val message = post?.getString("message") - if (message != null) { - val matchResults = regex.findAll(message) - matchResults.iterator().forEach { - val username = it.value.removePrefix("@") - if (!usernames.contains(username) && currentUsername != username && !specialMentions.contains(username)) { - usernames.add(username) + while (iterator.hasNextKey()) { + val key = iterator.nextKey() + val post = posts.getMap(key) + val userId = post?.getString("user_id") + if (userId != null && userId != currentUserId && !userIdsAlreadyLoaded.contains(userId) && !userIds.contains(userId)) { + userIds.add(userId) + } + val message = post?.getString("message") + if (message != null) { + val matchResults = regex.findAll(message) + matchResults.iterator().forEach { + val username = it.value.removePrefix("@") + if (!usernames.contains(username) && currentUsername != username && !specialMentions.contains(username)) { + usernames.add(username) + } + } + } + + if (isCRTEnabled) { + // Add root post as a thread + val threadId = post?.getString("root_id") + if (threadId.isNullOrEmpty()) { + post?.let { + val thread = Arguments.createMap() + thread.putString("id", it.getString("id")) + thread.putInt("reply_count", it.getInt("reply_count")) + thread.putDouble("last_reply_at", 0.0) + thread.putDouble("last_viewed_at", 0.0) + thread.putArray("participants", it.getArray("participants")) + thread.putMap("post", it) + thread.putBoolean("is_following", try { + it.getBoolean("is_following") + } catch (e: NoSuchKeyException) { + false + }) + thread.putInt("unread_replies", 0) + thread.putInt("unread_mentions", 0) + thread.putDouble("delete_at", it.getDouble("delete_at")) + threads.pushMap(thread) + } + } + + // Add participant userIds and usernames to exclude them from getting fetched again + val participants = post?.getArray("participants") + participants?.let { + for (i in 0 until it.size()) { + val participant = it.getMap(i) + + val participantId = participant.getString("id") + if (participantId != currentUserId && participantId != null) { + if (!threadParticipantUserIds.contains(participantId) && !userIdsAlreadyLoaded.contains(participantId)) { + threadParticipantUserIds.add(participantId) + } + + if (!threadParticipantUsers.containsKey(participantId)) { + threadParticipantUsers[participantId] = participant + } + } + + val username = participant.getString("username") + if (username != null && username != currentUsername && !threadParticipantUsernames.contains(username)) { + threadParticipantUsernames.add(username) + } + } } } } - if (isCRTEnabled) { - // Add root post as a thread - val threadId = post?.getString("root_id") - if (threadId.isNullOrEmpty()) { - threads.pushMap(post!!) - } + val existingUserIds = queryIds(db, "User", userIds.toTypedArray()) + val existingUsernames = queryByColumn(db, "User", "username", usernames.toTypedArray()) + userIds.removeAll { it in existingUserIds } + usernames.removeAll { it in existingUsernames } - // Add participant userIds and usernames to exclude them from getting fetched again - val participants = post.getArray("participants") - if (participants != null) { - for (i in 0 until participants.size()) { - val participant = participants.getMap(i) + if (threadParticipantUserIds.size > 0) { + // Do not fetch users found in thread participants as we get the user's data in the posts response already + userIds.removeAll { it in threadParticipantUserIds } + usernames.removeAll { it in threadParticipantUsernames } - val participantId = participant.getString("id") - if (participantId != currentUserId && participantId != null) { - if (!threadParticipantUserIds.contains(participantId) && !userIdsAlreadyLoaded.contains(participantId)) { - threadParticipantUserIds.add(participantId) - } + // Get users from thread participants + val existingThreadParticipantUserIds = queryIds(db, "User", threadParticipantUserIds.toTypedArray()) - if (!threadParticipantUsers.containsKey(participantId)) { - threadParticipantUsers[participantId] = participant - } - } - - val username = participant.getString("username") - if (username != null && username != currentUsername && !threadParticipantUsernames.contains(username)) { - threadParticipantUsernames.add(username) - } + // Exclude the thread participants already present in the DB from getting inserted again + val usersFromThreads = WritableNativeArray() + threadParticipantUsers.forEach { (userId, user) -> + if (!existingThreadParticipantUserIds.contains(userId)) { + usersFromThreads.pushMap(user) } } - } - } - val existingUserIds = queryIds(db, "User", userIds.toTypedArray()) - val existingUsernames = queryByColumn(db, "User", "username", usernames.toTypedArray()) - userIds.removeAll { it in existingUserIds } - usernames.removeAll { it in existingUsernames } - - if (threadParticipantUserIds.size > 0) { - // Do not fetch users found in thread participants as we get the user's data in the posts response already - userIds.removeAll { it in threadParticipantUserIds } - usernames.removeAll { it in threadParticipantUsernames } - - // Get users from thread participants - val existingThreadParticipantUserIds = queryIds(db, "User", threadParticipantUserIds.toTypedArray()) - - // Exclude the thread participants already present in the DB from getting inserted again - val usersFromThreads = WritableNativeArray() - threadParticipantUsers.forEach{ (userId, user) -> - if (!existingThreadParticipantUserIds.contains(userId)) { - usersFromThreads.pushMap(user) + if (usersFromThreads.size() > 0) { + results.putArray("usersFromThreads", usersFromThreads) } } - if (usersFromThreads.size() > 0) { - results.putArray("usersFromThreads", usersFromThreads) + if (userIds.size > 0) { + results.putArray("userIdsToLoad", ReadableArrayUtils.toWritableArray(userIds.toTypedArray())) } - } - if (userIds.size > 0) { - results.putArray("userIdsToLoad", ReadableArrayUtils.toWritableArray(userIds.toTypedArray())) - } + if (usernames.size > 0) { + results.putArray("usernamesToLoad", ReadableArrayUtils.toWritableArray(usernames.toTypedArray())) + } - if (usernames.size > 0) { - results.putArray("usernamesToLoad", ReadableArrayUtils.toWritableArray(usernames.toTypedArray())) - } - - if (threads.size() > 0) { - results.putArray("threads", threads) + if (threads.size() > 0) { + results.putArray("threads", threads) + } } } } + results + } catch (e: Exception) { + e.printStackTrace() + null } - return results } diff --git a/android/app/src/main/java/com/mattermost/helpers/push_notification/Team.kt b/android/app/src/main/java/com/mattermost/helpers/push_notification/Team.kt index d0b7560bc9..92ba655498 100644 --- a/android/app/src/main/java/com/mattermost/helpers/push_notification/Team.kt +++ b/android/app/src/main/java/com/mattermost/helpers/push_notification/Team.kt @@ -7,17 +7,22 @@ import com.mattermost.helpers.database_extension.findTeam import com.nozbe.watermelondb.Database suspend fun PushNotificationDataRunnable.Companion.fetchTeamIfNeeded(db: Database, serverUrl: String, teamId: String): Pair { - var team: ReadableMap? = null - var myTeam: ReadableMap? = null - val teamExists = findTeam(db, teamId) - val myTeamExists = findMyTeam(db, teamId) - if (!teamExists) { - team = fetch(serverUrl, "/api/v4/teams/$teamId") - } + return try { + var team: ReadableMap? = null + var myTeam: ReadableMap? = null + val teamExists = findTeam(db, teamId) + val myTeamExists = findMyTeam(db, teamId) + if (!teamExists) { + team = fetch(serverUrl, "/api/v4/teams/$teamId") + } - if (!myTeamExists) { - myTeam = fetch(serverUrl, "/api/v4/teams/$teamId/members/me") - } + if (!myTeamExists) { + myTeam = fetch(serverUrl, "/api/v4/teams/$teamId/members/me") + } - return Pair(team, myTeam) + Pair(team, myTeam) + } catch (e: Exception) { + e.printStackTrace() + Pair(null, null) + } } diff --git a/android/app/src/main/java/com/mattermost/helpers/push_notification/Thread.kt b/android/app/src/main/java/com/mattermost/helpers/push_notification/Thread.kt new file mode 100644 index 0000000000..0171ca650b --- /dev/null +++ b/android/app/src/main/java/com/mattermost/helpers/push_notification/Thread.kt @@ -0,0 +1,19 @@ +package com.mattermost.helpers.push_notification + +import com.facebook.react.bridge.ReadableMap +import com.mattermost.helpers.PushNotificationDataRunnable +import com.mattermost.helpers.database_extension.* +import com.nozbe.watermelondb.Database + +internal suspend fun PushNotificationDataRunnable.Companion.fetchThread(db: Database, serverUrl: String, threadId: String, teamId: String?): ReadableMap? { + val currentUserId = queryCurrentUserId(db) ?: return null + val threadTeamId = (if (teamId.isNullOrEmpty()) queryCurrentTeamId(db) else teamId) ?: return null + + return try { + val thread = fetch(serverUrl, "/api/v4/users/$currentUserId/teams/${threadTeamId}/threads/$threadId") + thread?.getMap("data") + } catch (e: Exception) { + e.printStackTrace() + null + } +} diff --git a/android/app/src/main/java/com/mattermost/helpers/push_notification/User.kt b/android/app/src/main/java/com/mattermost/helpers/push_notification/User.kt index 628c6f6012..78c5203234 100644 --- a/android/app/src/main/java/com/mattermost/helpers/push_notification/User.kt +++ b/android/app/src/main/java/com/mattermost/helpers/push_notification/User.kt @@ -6,16 +6,56 @@ import com.facebook.react.bridge.ReadableMap import com.mattermost.helpers.PushNotificationDataRunnable import com.mattermost.helpers.ReadableArrayUtils -internal suspend fun PushNotificationDataRunnable.Companion.fetchUsersById(serverUrl: String, userIds: ReadableArray): ReadableMap? { - val endpoint = "api/v4/users/ids" - val options = Arguments.createMap() - options.putArray("body", ReadableArrayUtils.toWritableArray(ReadableArrayUtils.toArray(userIds))) - return fetchWithPost(serverUrl, endpoint, options) +internal suspend fun PushNotificationDataRunnable.Companion.fetchUsersById(serverUrl: String, userIds: ReadableArray): ReadableArray? { + return try { + val endpoint = "api/v4/users/ids" + val options = Arguments.createMap() + options.putArray("body", ReadableArrayUtils.toWritableArray(ReadableArrayUtils.toArray(userIds))) + val result = fetchWithPost(serverUrl, endpoint, options) + result?.getArray("data") + } catch (e: Exception) { + e.printStackTrace() + null + } } -internal suspend fun PushNotificationDataRunnable.Companion.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) +internal suspend fun PushNotificationDataRunnable.Companion.fetchUsersByUsernames(serverUrl: String, usernames: ReadableArray): ReadableArray? { + return try { + val endpoint = "api/v4/users/usernames" + val options = Arguments.createMap() + options.putArray("body", ReadableArrayUtils.toWritableArray(ReadableArrayUtils.toArray(usernames))) + val result = fetchWithPost(serverUrl, endpoint, options) + result?.getArray("data") + } catch (e: Exception) { + e.printStackTrace() + null + } +} + +internal suspend fun PushNotificationDataRunnable.Companion.fetchNeededUsers(serverUrl: String, loadedUsers: ReadableArray?, data: ReadableMap?): ArrayList { + val userList = ArrayList() + loadedUsers?.let { PushNotificationDataRunnable.addUsersToList(it, userList) } + data?.getArray("userIdsToLoad")?.let { ids -> + if (ids.size() > 0) { + val result = fetchUsersById(serverUrl, ids) + result?.let { PushNotificationDataRunnable.addUsersToList(it, userList) } + } + } + + data?.getArray("usernamesToLoad")?.let { ids -> + if (ids.size() > 0) { + val result = fetchUsersByUsernames(serverUrl, ids) + result?.let { PushNotificationDataRunnable.addUsersToList(it, userList) } + } + } + + data?.getArray("usersFromThreads")?.let { PushNotificationDataRunnable.addUsersToList(it, userList) } + + return userList +} + +internal fun PushNotificationDataRunnable.Companion.addUsersToList(users: ReadableArray, list: ArrayList) { + for (i in 0 until users.size()) { + list.add(users.getMap(i)) + } } diff --git a/android/app/src/main/java/com/mattermost/rnbeta/CustomPushNotification.java b/android/app/src/main/java/com/mattermost/rnbeta/CustomPushNotification.java index e2eaa9b38d..04d77fb7e0 100644 --- a/android/app/src/main/java/com/mattermost/rnbeta/CustomPushNotification.java +++ b/android/app/src/main/java/com/mattermost/rnbeta/CustomPushNotification.java @@ -12,11 +12,13 @@ import androidx.core.app.NotificationCompat; import java.util.Objects; +import com.facebook.react.bridge.ReadableMap; 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.ReadableMapUtils; import com.mattermost.share.ShareModule; import com.wix.reactnativenotifications.core.NotificationIntentAdapter; import com.wix.reactnativenotifications.core.notification.PushNotification; @@ -95,13 +97,17 @@ public class CustomPushNotification extends PushNotification { if (type.equals(CustomPushNotificationHelper.PUSH_TYPE_MESSAGE)) { if (channelId != null) { Bundle notificationBundle = mNotificationProps.asBundle(); - if (serverUrl != null && !isReactInit) { + if (serverUrl != null) { // 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(notificationBundle); + Bundle notificationResult = dataHelper.fetchAndStoreDataForPushNotification(notificationBundle, isReactInit); + if (notificationResult != null) { + notificationBundle.putBundle("data", notificationResult); + mNotificationProps = createProps(notificationBundle); + } } createSummary = NotificationHelper.addNotificationToPreferences( mContext, diff --git a/patches/@nozbe+watermelondb+0.25.5.patch b/patches/@nozbe+watermelondb+0.25.5.patch index da655ee0fa..7a5708d452 100644 --- a/patches/@nozbe+watermelondb+0.25.5.patch +++ b/patches/@nozbe+watermelondb+0.25.5.patch @@ -1,5 +1,5 @@ diff --git a/node_modules/@nozbe/watermelondb/Database/index.js b/node_modules/@nozbe/watermelondb/Database/index.js -index 8d71c6f..30832c8 100644 +index 8d71c6f..7a4b570 100644 --- a/node_modules/@nozbe/watermelondb/Database/index.js +++ b/node_modules/@nozbe/watermelondb/Database/index.js @@ -91,7 +91,9 @@ var Database = /*#__PURE__*/function () { @@ -38,7 +38,7 @@ index 96114ec..ecfe3c1 100644 prepareDestroyPermanently(): this diff --git a/node_modules/@nozbe/watermelondb/Model/index.js b/node_modules/@nozbe/watermelondb/Model/index.js -index b0e3a83..1bbce74 100644 +index b0e3a83..d7ead09 100644 --- a/node_modules/@nozbe/watermelondb/Model/index.js +++ b/node_modules/@nozbe/watermelondb/Model/index.js @@ -81,7 +81,17 @@ var Model = /*#__PURE__*/function () { @@ -101,9 +101,18 @@ index b0e3a83..1bbce74 100644 this.__ensureNotDisposable("Model.prepareDestroyPermanently()"); diff --git a/node_modules/@nozbe/watermelondb/native/android/src/main/java/com/nozbe/watermelondb/Database.kt b/node_modules/@nozbe/watermelondb/native/android/src/main/java/com/nozbe/watermelondb/Database.kt -index ca31e20..b45c753 100644 +index ca31e20..764519f 100644 --- a/node_modules/@nozbe/watermelondb/native/android/src/main/java/com/nozbe/watermelondb/Database.kt +++ b/node_modules/@nozbe/watermelondb/native/android/src/main/java/com/nozbe/watermelondb/Database.kt +@@ -11,7 +11,7 @@ import java.io.File + class Database( + private val name: String, + private val context: Context, +- private val openFlags: Int = SQLiteDatabase.CREATE_IF_NECESSARY or SQLiteDatabase.ENABLE_WRITE_AHEAD_LOGGING ++ private val openFlags: Int = SQLiteDatabase.CREATE_IF_NECESSARY + ) { + + private val db: SQLiteDatabase by lazy { @@ -22,6 +22,21 @@ class Database( if (name == ":memory:" || name.contains("mode=memory")) { context.cacheDir.delete() @@ -127,9 +136,18 @@ index ca31e20..b45c753 100644 // On some systems there is some kind of lock on `/databases` folder ¯\_(ツ)_/¯ context.getDatabasePath("$name.db").path.replace("/databases", "") diff --git a/node_modules/@nozbe/watermelondb/native/shared/Database.cpp b/node_modules/@nozbe/watermelondb/native/shared/Database.cpp -index 1a1cabf..01bbb2b 100644 +index 1a1cabf..c4459c8 100644 --- a/node_modules/@nozbe/watermelondb/native/shared/Database.cpp +++ b/node_modules/@nozbe/watermelondb/native/shared/Database.cpp +@@ -21,7 +21,7 @@ Database::Database(jsi::Runtime *runtime, std::string path, bool usesExclusiveLo + executeMultiple("pragma temp_store = memory;"); + #endif + +- executeMultiple("pragma journal_mode = WAL;"); ++// executeMultiple("pragma journal_mode = WAL;"); + + #ifdef ANDROID + // NOTE: This was added in an attempt to fix mysterious `database disk image is malformed` issue when using @@ -54,6 +54,7 @@ void Database::destroy() { const std::lock_guard lock(mutex_);