[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

@@ -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)

View File

@@ -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<String>("id")]
let currentUsername = currentUser?[Expression<String>("username")]
var postData: PostData? = nil
var threads: [Post] = []
var userIdsToLoad: Set<String> = Set()
var usernamesToLoad: Set<String> = Set()
var users: Set<User> = 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<String> = Set()
var usernames: Set<String> = Set()
var threadParticipantUserIds: Set<String> = Set() // Used to exclude the "userIds" present in the thread participants
var threadParticipantUsernames: Set<String> = 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)
}

View File

@@ -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<String>("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<String>("id")
let createAt = Expression<Int64>("create_at")
@@ -431,6 +475,71 @@ extension Database {
emojiSetters: emojiSetters)
}
private func createThreadSetters(_ db: Connection, from posts: [Post]) throws -> [ThreadSetters] {
let id = Expression<String>("id")
let lastReplyAt = Expression<Int64>("last_reply_at")
let replyCount = Expression<Int>("reply_count")
let isFollowing = Expression<Bool>("is_following")
let statusCol = Expression<String>("_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<String>("id")
let userId = Expression<String>("user_id")
let threadId = Expression<String>("thread_id")
let statusCol = Expression<String>("_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]]()

View File

@@ -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)

View File

@@ -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()