[Gekidou MM-40097 MM-44133] Save thread related data when client receives a push notification (#6275)

* Android changes

* Fixed updating records from native side

* Android: Handle crt related issues

* Android threads fix

* Android misc fixes

* Android addressing feedback

* ios changes WIP

* Update Podfile.lock

* iOS changes

* iOS feedback

* iOS updates the existing record like android

* Android misc

Co-authored-by: Mattermod <mattermod@users.noreply.github.com>
This commit is contained in:
Anurag Shivarathri
2022-06-09 16:50:14 +05:30
committed by GitHub
parent db171cc7dd
commit 1b5e41b424
7 changed files with 407 additions and 25 deletions

View File

@@ -142,7 +142,7 @@ class DatabaseHelper {
return null
}
fun handlePosts(db: Database, postsData: ReadableMap?, channelId: String) {
fun handlePosts(db: Database, postsData: ReadableMap?, channelId: String, receivingThreads: Boolean) {
// Posts, PostInChannel, PostInThread, Reactions, Files, CustomEmojis, Users
if (postsData != null) {
val ordered = postsData.getArray("order")?.toArrayList()
@@ -205,11 +205,38 @@ class DatabaseHelper {
}
}
handlePostsInChannel(db, channelId, earliest, latest)
if (!receivingThreads) {
handlePostsInChannel(db, channelId, earliest, latest)
}
handlePostsInThread(db, postsInThread)
}
}
fun handleThreads(db: Database, threads: ReadableArray) {
for (i in 0 until threads.size()) {
val thread = threads.getMap(i)
val threadId = thread.getString("id")
// Insert/Update the thread
val existingRecord = find(db, "Thread", threadId)
if (existingRecord == null) {
insertThread(db, thread)
} else {
updateThread(db, thread, existingRecord)
}
// Delete existing and insert thread participants
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)
}
}
}
}
fun handleUsers(db: Database, users: ReadableArray) {
for (i in 0 until users.size()) {
val user = users.getMap(i)
@@ -220,6 +247,8 @@ class DatabaseHelper {
false
}
val lastPictureUpdate = try { user.getDouble("last_picture_update") } catch (e: NoSuchKeyException) { 0 }
db.execute(
"insert into User (id, auth_service, update_at, delete_at, email, first_name, is_bot, is_guest, " +
@@ -235,7 +264,7 @@ class DatabaseHelper {
isBot,
roles.contains("system_guest"),
user.getString("last_name"),
user.getDouble("last_picture_update"),
lastPictureUpdate,
user.getString("locale"),
user.getString("nickname"),
user.getString("position"),
@@ -250,6 +279,27 @@ 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
@@ -360,6 +410,67 @@ 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 };
db.execute(
"insert into Thread " +
"(id, last_reply_at, last_viewed_at, reply_count, is_following, unread_replies, unread_mentions, _status)" +
" values (?, ?, ?, ?, ?, ?, ?, 'created')",
arrayOf(
thread.getString("id"),
thread.getDouble("last_reply_at") ?: 0,
lastViewedAt,
thread.getInt("reply_count") ?: 0,
isFollowing,
unreadReplies,
unreadMentions
)
)
}
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") };
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,
lastViewedAt,
thread.getInt("reply_count") ?: 0,
isFollowing,
unreadReplies,
unreadMentions,
thread.getString("id")
)
)
}
private fun insertThreadParticipants(db: Database, threadId: String, participants: ReadableArray) {
for (i in 0 until participants.size()) {
val participant = participants.getMap(i)
val id = RandomId.generate()
db.execute(
"insert into ThreadParticipant " +
"(id, thread_id, user_id, _status)" +
" values (?, ?, ?, 'created')",
arrayOf(
id,
threadId,
participant.getString("id")
)
)
}
}
private fun insertCustomEmojis(db: Database, customEmojis: JSONArray) {
for (i in 0 until customEmojis.length()) {
val emoji = customEmojis.getJSONObject(i)

View File

@@ -5,6 +5,7 @@ import android.os.Bundle
import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.ReadableArray
import com.facebook.react.bridge.ReadableMap
import com.facebook.react.bridge.WritableNativeArray
import com.nozbe.watermelondb.Database
import java.io.IOException
import java.util.concurrent.Executors
@@ -30,6 +31,8 @@ class PushNotificationDataRunnable {
try {
val serverUrl: String = initialData.getString("server_url") ?: return
val channelId = initialData.getString("channel_id")
val rootId = initialData.getString("root_id")
val isCRTEnabled = initialData.getString("is_crt_enabled") == "true"
val db = DatabaseHelper.instance!!.getDatabaseForServer(context, serverUrl)
if (db != null) {
@@ -38,12 +41,19 @@ class PushNotificationDataRunnable {
var userIdsToLoad: ReadableArray? = null
var usernamesToLoad: ReadableArray? = null
var threads: ReadableArray? = null
var usersFromThreads: ReadableArray? = null
val receivingThreads = isCRTEnabled && !rootId.isNullOrEmpty()
coroutineScope {
if (channelId != null) {
postData = fetchPosts(db, serverUrl, channelId)
postData = fetchPosts(db, serverUrl, channelId, isCRTEnabled, rootId)
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!!)
@@ -59,7 +69,11 @@ class PushNotificationDataRunnable {
db.transaction {
if (posts != null && channelId != null) {
DatabaseHelper.instance!!.handlePosts(db, posts!!.getMap("data"), channelId)
DatabaseHelper.instance!!.handlePosts(db, posts!!.getMap("data"), channelId, receivingThreads)
}
if (threads != null) {
DatabaseHelper.instance!!.handleThreads(db, threads!!)
}
if (userIdsToLoad != null && userIdsToLoad!!.size() > 0) {
@@ -69,6 +83,10 @@ class PushNotificationDataRunnable {
if (usernamesToLoad != null && usernamesToLoad!!.size() > 0) {
DatabaseHelper.instance!!.handleUsers(db, usernamesToLoad!!)
}
if (usersFromThreads != null) {
DatabaseHelper.instance!!.handleUsers(db, usersFromThreads!!)
}
}
db.close()
@@ -78,14 +96,28 @@ class PushNotificationDataRunnable {
}
}
private suspend fun fetchPosts(db: Database, serverUrl: String, channelId: String): ReadableMap? {
private suspend fun fetchPosts(db: Database, serverUrl: String, channelId: String, isCRTEnabled: Boolean, rootId: String?): ReadableMap? {
val regex = Regex("""\B@(([a-z0-9-._]*[a-z0-9_])[.-]*)""", setOf(RegexOption.IGNORE_CASE))
val since = DatabaseHelper.instance!!.queryPostSinceForChannel(db, channelId)
val currentUserId = DatabaseHelper.instance!!.queryCurrentUserId(db)?.removeSurrounding("\"")
val currentUser = DatabaseHelper.instance!!.find(db, "User", currentUserId)
val currentUsername = currentUser?.getString("username")
val queryParams = if (since == null) "?page=0&per_page=60" else "?since=${since.toLong()}"
val endpoint = "/api/v4/channels/$channelId/posts$queryParams"
var additionalParams = ""
if (isCRTEnabled) {
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"
} else {
var queryParams = if (since == null) "?page=0&per_page=60" else "?since=${since.toLong()}"
endpoint = "/api/v4/channels/$channelId/posts$queryParams$additionalParams"
}
val postsResponse = fetch(serverUrl, endpoint)
val results = Arguments.createMap()
@@ -100,6 +132,12 @@ class PushNotificationDataRunnable {
val iterator = posts.keySetIterator()
val userIds = mutableListOf<String>()
val usernames = mutableListOf<String>()
val threads = WritableNativeArray()
val threadParticipantUserIds = mutableListOf<String>() // Used to exclude the "userIds" present in the thread participants
val threadParticipantUsernames = mutableListOf<String>() // Used to exclude the "usernames" present in the thread participants
val threadParticipantUsers = HashMap<String, ReadableMap>() // All unique users from thread participants are stored here
while(iterator.hasNextKey()) {
val key = iterator.nextKey()
val post = posts.getMap(key)
@@ -117,6 +155,38 @@ class PushNotificationDataRunnable {
}
}
}
if (isCRTEnabled) {
// Add root post as a thread
val rootId = post?.getString("root_id")
if (rootId.isNullOrEmpty()) {
threads.pushMap(post!!)
}
// 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)
val userId = participant.getString("id")
if (userId != currentUserId && userId != null) {
if (!threadParticipantUserIds.contains(userId)) {
threadParticipantUserIds.add(userId)
}
if (!threadParticipantUsers.containsKey(userId)) {
threadParticipantUsers[userId!!] = participant
}
}
val username = participant.getString("username")
if (username != null && username != currentUsername && !threadParticipantUsernames.contains(username)) {
threadParticipantUsernames.add(username)
}
}
}
}
}
val existingUserIds = DatabaseHelper.instance!!.queryIds(db, "User", userIds.toTypedArray())
@@ -124,6 +194,27 @@ class PushNotificationDataRunnable {
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 = DatabaseHelper.instance!!.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 (userIds.size > 0) {
results.putArray("userIdsToLoad", ReadableArrayUtils.toWritableArray(userIds.toTypedArray()))
}
@@ -131,11 +222,13 @@ class PushNotificationDataRunnable {
if (usernames.size > 0) {
results.putArray("usernamesToLoad", ReadableArrayUtils.toWritableArray(usernames.toTypedArray()))
}
if (threads.size() > 0) {
results.putArray("threads", threads)
}
}
}
}
return results
}