diff --git a/android/app/src/main/java/com/mattermost/helpers/DatabaseHelper.kt b/android/app/src/main/java/com/mattermost/helpers/DatabaseHelper.kt index e4559e53d..ac91378b1 100644 --- a/android/app/src/main/java/com/mattermost/helpers/DatabaseHelper.kt +++ b/android/app/src/main/java/com/mattermost/helpers/DatabaseHelper.kt @@ -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) 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 237a50525..9a61616b0 100644 --- a/android/app/src/main/java/com/mattermost/helpers/PushNotificationDataHelper.kt +++ b/android/app/src/main/java/com/mattermost/helpers/PushNotificationDataHelper.kt @@ -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() 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 + 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 } diff --git a/ios/Gekidou/Sources/Gekidou/Networking/Network+Posts.swift b/ios/Gekidou/Sources/Gekidou/Networking/Network+Posts.swift index 1a63db583..3b9b81bc2 100644 --- a/ios/Gekidou/Sources/Gekidou/Networking/Network+Posts.swift +++ b/ios/Gekidou/Sources/Gekidou/Networking/Network+Posts.swift @@ -31,11 +31,20 @@ public struct PostData: Codable { } extension Network { - public func fetchPostsForChannel(withId channelId: String, withSince since: Int64?, withServerUrl serverUrl: String, completionHandler: @escaping ResponseHandler) { - let queryParams = since == nil ? - "?page=0&per_page=\(POST_CHUNK_SIZE)" : - "?since=\(since!)" - let endpoint = "/channels/\(channelId)/posts\(queryParams)" + public func fetchPostsForChannel(withId channelId: String, withSince since: Int64?, withServerUrl serverUrl: String, withIsCRTEnabled isCRTEnabled: Bool, withRootId rootId: String, completionHandler: @escaping ResponseHandler) { + + let additionalParams = isCRTEnabled ? "&collapsedThreads=true&collapsedThreadsExtended=true" : "" + + let endpoint: String + if (isCRTEnabled && !rootId.isEmpty) { + let queryParams = "?skipFetchThreads=false&perPage=60&fromCreatedAt=0&direction=up" + endpoint = "/posts/\(rootId)/thread\(queryParams)\(additionalParams)" + } else { + let queryParams = since == nil ? + "?page=0&per_page=\(POST_CHUNK_SIZE)" : + "?since=\(since!)" + endpoint = "/channels/\(channelId)/posts\(queryParams)\(additionalParams)" + } let url = buildApiUrl(serverUrl, endpoint) return request(url, withMethod: "GET", withServerUrl: serverUrl, completionHandler: completionHandler) diff --git a/ios/Gekidou/Sources/Gekidou/Networking/PushNotification.swift b/ios/Gekidou/Sources/Gekidou/Networking/PushNotification.swift index 19bc8caeb..981d5dd4e 100644 --- a/ios/Gekidou/Sources/Gekidou/Networking/PushNotification.swift +++ b/ios/Gekidou/Sources/Gekidou/Networking/PushNotification.swift @@ -114,24 +114,32 @@ extension Network { let group = DispatchGroup() let channelId = notification.userInfo["channel_id"] as! String + let rootId = notification.userInfo.index(forKey: "root_id") != nil ? notification.userInfo["root_id"] as! String : "" let serverUrl = notification.userInfo["server_url"] as! String + let isCRTEnabled = notification.userInfo["is_crt_enabled"] as! Bool let currentUser = try! Database.default.queryCurrentUser(serverUrl) let currentUserId = currentUser?[Expression("id")] let currentUsername = currentUser?[Expression("username")] var postData: PostData? = nil + var threads: [Post] = [] var userIdsToLoad: Set = Set() var usernamesToLoad: Set = Set() var users: Set = Set() group.enter() let since = try? Database.default.queryPostsSinceForChannel(withId: channelId, withServerUrl: serverUrl) - self.fetchPostsForChannel(withId: channelId, withSince: since, withServerUrl: serverUrl) { data, response, error in + self.fetchPostsForChannel(withId: channelId, withSince: since, withServerUrl: serverUrl, withIsCRTEnabled: isCRTEnabled, withRootId: rootId) { data, response, error in if self.responseOK(response), let data = data { postData = try! JSONDecoder().decode(PostData.self, from: data) if postData?.posts.count ?? 0 > 0 { var authorIds: Set = Set() var usernames: Set = Set() + + var threadParticipantUserIds: Set = Set() // Used to exclude the "userIds" present in the thread participants + var threadParticipantUsernames: Set = Set() // Used to exclude the "usernames" present in the thread participants + var threadParticipantUsers = [String: User]() // All unique users from thread participants are stored here + postData!.posts.forEach{post in if (currentUserId != nil && post.user_id != currentUserId) { authorIds.insert(post.user_id) @@ -141,11 +149,41 @@ extension Network { usernames.insert($0) } } + + if (isCRTEnabled) { + // Add root post as a thread + let rootId = post.root_id + if (rootId.isEmpty) { + threads.append(post) + } + + let participants = post.participants ?? [] + if (participants.count > 0) { + participants.forEach { participant in + let userId = participant.id + if (userId != currentUserId) { + threadParticipantUserIds.insert(userId) + if (threadParticipantUsers[userId] != nil) { + threadParticipantUsers[userId] = participant + } + } + + let username = participant.username + if (username != "" && username != currentUsername) { + threadParticipantUsernames.insert(username) + } + } + } + } } - + if (authorIds.count > 0) { if let existingIds = try? Database.default.queryUsers(byIds: authorIds, withServerUrl: serverUrl) { userIdsToLoad = authorIds.filter { !existingIds.contains($0) } + // Filter the users found in the thread participants list + if (threadParticipantUserIds.count > 0) { + userIdsToLoad = userIdsToLoad.filter{ !threadParticipantUserIds.contains($0) } + } if (userIdsToLoad.count > 0) { group.enter() self.fetchUsers(byIds: Array(userIdsToLoad), withServerUrl: serverUrl) { data, response, error in @@ -158,10 +196,14 @@ extension Network { } } } - + if (usernames.count > 0) { if let existingUsernames = try? Database.default.queryUsers(byUsernames: usernames, withServerUrl: serverUrl) { usernamesToLoad = usernames.filter{ !existingUsernames.contains($0)} + // Filter the users found in the thread participants list + if (threadParticipantUsernames.count > 0) { + usernamesToLoad = usernamesToLoad.filter{ !threadParticipantUsernames.contains($0) } + } if (usernamesToLoad.count > 0) { group.enter() @@ -175,6 +217,18 @@ extension Network { } } } + + if (threadParticipantUserIds.count > 0) { + if let existingThreadParticipantUserIds = try? Database.default.queryUsers(byIds: threadParticipantUserIds, withServerUrl: serverUrl) { + threadParticipantUsers.forEach { (userId: String, user: User) in + if (!existingThreadParticipantUserIds.contains(userId)) { + users.insert(user) + } + } + } + + } + } } @@ -182,12 +236,16 @@ extension Network { } group.wait() - + group.enter() if (postData != nil && postData?.posts != nil && postData!.posts.count > 0) { if let db = try? Database.default.getDatabaseForServer(serverUrl) { + let receivingThreads = isCRTEnabled && !rootId.isEmpty try? db.transaction { - try? Database.default.handlePostData(db, postData!, channelId, since != nil) + try? Database.default.handlePostData(db, postData!, channelId, since != nil, receivingThreads) + if (threads.count > 0) { + try? Database.default.handleThreads(db, threads) + } if (users.count > 0) { try? Database.default.insertUsers(db, users) } @@ -195,7 +253,7 @@ extension Network { } } group.leave() - + if let contentHandler = contentHandler { contentHandler(notification) } diff --git a/ios/Gekidou/Sources/Gekidou/Storage/Database+Posts.swift b/ios/Gekidou/Sources/Gekidou/Storage/Database+Posts.swift index 5dbf7d13e..70ea12654 100644 --- a/ios/Gekidou/Sources/Gekidou/Storage/Database+Posts.swift +++ b/ios/Gekidou/Sources/Gekidou/Storage/Database+Posts.swift @@ -25,6 +25,11 @@ public struct Post: Codable { let pending_post_id: String let metadata: String var prev_post_id: String + // CRT + let participants: [User]? + let last_reply_at: Int64 + let reply_count: Int + let is_following: Bool public enum PostKeys: String, CodingKey { case id = "id" @@ -42,6 +47,11 @@ public struct Post: Codable { case props = "props" case pending_post_id = "pending_post_id" case metadata = "metadata" + // CRT + case participants = "participants" + case last_reply_at = "last_reply_at" + case reply_count = "reply_count" + case is_following = "is_following" } public init(from decoder: Decoder) throws { @@ -64,6 +74,11 @@ public struct Post: Codable { pending_post_id = try values.decode(String.self, forKey: .pending_post_id) let propsData = try values.decode([String:Any].self, forKey: .props) props = Database.default.json(from: propsData) ?? "{}" + // CRT + participants = try values.decodeIfPresent([User].self, forKey: .participants) ?? [] + last_reply_at = try values.decodeIfPresent(Int64.self, forKey: .last_reply_at) ?? 0 + reply_count = try values.decodeIfPresent(Int.self, forKey: .reply_count) ?? 0 + is_following = try values.decodeIfPresent(Bool.self, forKey: .is_following) ?? false } } @@ -82,6 +97,12 @@ struct PostSetters { let emojiSetters: [[Setter]] } +struct ThreadSetters { + let id: String + let threadSetters: [Setter] + let threadParticipantSetters: [[Setter]] +} + extension Database { public func queryPostsSinceForChannel(withId channelId: String, withServerUrl serverUrl: String) throws -> Int64? { let db = try getDatabaseForServer(serverUrl) @@ -142,16 +163,22 @@ extension Database { return (0, 0) } - - public func handlePostData(_ db: Connection, _ postData: PostData, _ channelId: String, _ usedSince: Bool = false) throws { + + public func handlePostData(_ db: Connection, _ postData: PostData, _ channelId: String, _ usedSince: Bool = false, _ receivingThreads: Bool = false) throws { let sortedChainedPosts = chainAndSortPosts(postData) try insertOrUpdatePosts(db, sortedChainedPosts, channelId) let earliest = sortedChainedPosts.first!.create_at let latest = sortedChainedPosts.last!.create_at - try handlePostsInChannel(db, channelId, earliest, latest, usedSince) + if (!receivingThreads) { + try handlePostsInChannel(db, channelId, earliest, latest, usedSince) + } try handlePostsInThread(db, postData.posts) } + public func handleThreads(_ db: Connection, _ threads: [Post]) throws { + try insertThreads(db, threads) + } + private func handlePostsInChannel(_ db: Connection, _ channelId: String, _ earliest: Int64, _ latest: Int64, _ usedSince: Bool = false) throws { if usedSince { try? updatePostsInChannelLatestOnly(db, channelId, latest) @@ -283,6 +310,23 @@ extension Database { } } + private func insertThreads(_ db: Connection, _ posts: [Post]) throws { + let setters = try createThreadSetters(db, from: posts) + for setter in setters { + let insertThread = threadTable.insert(or: .replace, setter.threadSetters) + try db.run(insertThread) + + let threadIdCol = Expression("thread_id") + let deletePreviousThreadParticipants = threadParticipantTable.where(threadIdCol == setter.id).delete() + try db.run(deletePreviousThreadParticipants) + + if !setter.threadParticipantSetters.isEmpty { + let insertThreadParticipants = threadParticipantTable.insertMany(setter.threadParticipantSetters) + try db.run(insertThreadParticipants) + } + } + } + private func createPostSetters(from posts: [Post]) -> [PostSetters] { let id = Expression("id") let createAt = Expression("create_at") @@ -431,6 +475,71 @@ extension Database { emojiSetters: emojiSetters) } + private func createThreadSetters(_ db: Connection, from posts: [Post]) throws -> [ThreadSetters] { + let id = Expression("id") + let lastReplyAt = Expression("last_reply_at") + let replyCount = Expression("reply_count") + let isFollowing = Expression("is_following") + let statusCol = Expression("_status") + + var threadsSetters: [ThreadSetters] = [] + + for post in posts { + + let query = threadTable + .select(id) + .where(id == post.id) + + if let _ = try? db.pluck(query) { + let updateQuery = threadTable + .where(id == post.id) + .update(lastReplyAt <- post.last_reply_at, + replyCount <- post.reply_count, + isFollowing <- post.is_following, + statusCol <- "updated" + ) + try db.run(updateQuery) + } else { + var setter = [Setter]() + setter.append(id <- post.id) + setter.append(lastReplyAt <- post.last_reply_at) + setter.append(replyCount <- post.reply_count) + setter.append(isFollowing <- post.is_following) + setter.append(statusCol <- "created") + + let threadSetter = ThreadSetters( + id: post.id, + threadSetters: setter, + threadParticipantSetters: createThreadParticipantSetters(from: post) + ) + threadsSetters.append(threadSetter) + } + } + + return threadsSetters + } + + private func createThreadParticipantSetters(from post: Post) -> [[Setter]] { + + var participantSetters = [[Setter]]() + + let id = Expression("id") + let userId = Expression("user_id") + let threadId = Expression("thread_id") + let statusCol = Expression("_status") + + for p in post.participants ?? [] { + var participantSetter = [Setter]() + participantSetter.append(id <- generateId() as String) + participantSetter.append(userId <- p.id) + participantSetter.append(threadId <- post.id) + participantSetter.append(statusCol <- "created") + participantSetters.append(participantSetter) + } + + return participantSetters + } + private func createPostsInThreadSetters(_ db: Connection, from posts: [Post]) throws -> [[Setter]] { var setters = [[Setter]]() var postsInThread = [String: [Post]]() diff --git a/ios/Gekidou/Sources/Gekidou/Storage/Database+Users.swift b/ios/Gekidou/Sources/Gekidou/Storage/Database+Users.swift index b16dc5373..6022e465e 100644 --- a/ios/Gekidou/Sources/Gekidou/Storage/Database+Users.swift +++ b/ios/Gekidou/Sources/Gekidou/Storage/Database+Users.swift @@ -61,7 +61,7 @@ public struct User: Codable, Hashable { roles = try container.decode(String.self, forKey: .roles) is_guest = roles.contains("system_guest") last_name = try container.decode(String.self, forKey: .last_name) - last_picture_update = try container.decode(Int64.self, forKey: .last_picture_update) + last_picture_update = try container.decodeIfPresent(Int64.self, forKey: .last_picture_update) ?? 0 locale = try container.decode(String.self, forKey: .locale) nickname = try container.decode(String.self, forKey: .nickname) position = try container.decode(String.self, forKey: .position) diff --git a/ios/Gekidou/Sources/Gekidou/Storage/Database.swift b/ios/Gekidou/Sources/Gekidou/Storage/Database.swift index 2534d1974..170ba1e4a 100644 --- a/ios/Gekidou/Sources/Gekidou/Storage/Database.swift +++ b/ios/Gekidou/Sources/Gekidou/Storage/Database.swift @@ -58,6 +58,8 @@ public class Database: NSObject { internal var fileTable = Table("File") internal var emojiTable = Table("CustomEmoji") internal var userTable = Table("User") + internal var threadTable = Table("Thread") + internal var threadParticipantTable = Table("ThreadParticipant") @objc public static let `default` = Database()