From 9ed616afa9b6008f01751e77f302aa7a666084bf Mon Sep 17 00:00:00 2001 From: Elias Nahum Date: Mon, 20 Sep 2021 17:59:42 -0300 Subject: [PATCH] [Gekidou] iOS - Fetch and store data on push notification receipt --- .../Sources/Gekidou/Date+Extensions.swift | 6 + .../Gekidou/JSONDecode+Extension.swift | 300 +++++++++++ .../Gekidou/Networking/Network+Channels.swift | 24 - .../Gekidou/Networking/Network+Teams.swift | 24 - .../Gekidou/Networking/Network+Users.swift | 26 + .../Gekidou/Networking/PushNotification.swift | 163 ++++-- .../Gekidou/Storage/Database+Channels.swift | 204 ------- .../Gekidou/Storage/Database+Posts.swift | 505 +++++++----------- .../Gekidou/Storage/Database+Teams.swift | 100 ---- .../Gekidou/Storage/Database+Users.swift | 177 ++++++ .../Sources/Gekidou/Storage/Database.swift | 36 +- ios/Mattermost.xcodeproj/project.pbxproj | 26 +- ios/Mattermost/AppDelegate.m | 23 + .../NotificationService.swift | 8 +- .../project.pbxproj | 2 + 15 files changed, 892 insertions(+), 732 deletions(-) create mode 100644 ios/Gekidou/Sources/Gekidou/JSONDecode+Extension.swift delete mode 100644 ios/Gekidou/Sources/Gekidou/Networking/Network+Channels.swift delete mode 100644 ios/Gekidou/Sources/Gekidou/Networking/Network+Teams.swift create mode 100644 ios/Gekidou/Sources/Gekidou/Networking/Network+Users.swift delete mode 100644 ios/Gekidou/Sources/Gekidou/Storage/Database+Channels.swift delete mode 100644 ios/Gekidou/Sources/Gekidou/Storage/Database+Teams.swift create mode 100644 ios/Gekidou/Sources/Gekidou/Storage/Database+Users.swift diff --git a/ios/Gekidou/Sources/Gekidou/Date+Extensions.swift b/ios/Gekidou/Sources/Gekidou/Date+Extensions.swift index 2ccb60db2b..7c260c825e 100644 --- a/ios/Gekidou/Sources/Gekidou/Date+Extensions.swift +++ b/ios/Gekidou/Sources/Gekidou/Date+Extensions.swift @@ -16,3 +16,9 @@ extension Date { self = Date(timeIntervalSince1970: TimeInterval(milliseconds) / 1000) } } + +extension StringProtocol { + public subscript(offset: Int) -> Character { + self[index(startIndex, offsetBy: offset)] + } +} diff --git a/ios/Gekidou/Sources/Gekidou/JSONDecode+Extension.swift b/ios/Gekidou/Sources/Gekidou/JSONDecode+Extension.swift new file mode 100644 index 0000000000..522d3d7bcf --- /dev/null +++ b/ios/Gekidou/Sources/Gekidou/JSONDecode+Extension.swift @@ -0,0 +1,300 @@ +// +// JSONDecode+Extension.swift +// + +import Foundation + +// Inspired by https://gist.github.com/loudmouth/332e8d89d8de2c1eaf81875cfcd22e24 +// Used to decode dictionaries and array of dictionaries + +struct JSONCodingKeys: CodingKey { + var stringValue: String + var intValue: Int? + + init(stringValue: String) { + self.stringValue = stringValue + } + + init?(intValue: Int) { + self.init(stringValue: "\(intValue)") + self.intValue = intValue + } +} + +extension KeyedDecodingContainer { + + func decode(_ type: [String: Any].Type, forKey key: K) throws -> [String: Any] { + let container = try self.nestedContainer(keyedBy: JSONCodingKeys.self, forKey: key) + return try container.decode(type) + } + + func decodeIfPresent(_ type: [String: Any].Type, forKey key: K) throws -> [String: Any]? { + guard contains(key) else { + return nil + } + return try decode(type, forKey: key) + } + + func decode(_ type: [Any].Type, forKey key: K) throws -> [Any] { + var container = try self.nestedUnkeyedContainer(forKey: key) + return try container.decode(type) + } + + func decode(_ type: [[String: Any]].Type, forKey key: K) throws -> [[String: Any]] { + var container = try self.nestedUnkeyedContainer(forKey: key) + return try container.decode(type) + } + + func decodeIfPresent(_ type: [Any].Type, forKey key: K) throws -> [Any]? { + guard contains(key) else { + return nil + } + return try decode(type, forKey: key) + } + + func decode(_ type: [String: Any].Type) throws -> [String: Any] { + var dictionary = [String: Any]() + + for key in allKeys { + if let boolValue = try? decode(Bool.self, forKey: key) { + dictionary[key.stringValue] = boolValue + } else if let intValue = try? decode(Int.self, forKey: key) { + dictionary[key.stringValue] = intValue + } else if let intValue = try? decode(Int8.self, forKey: key) { + dictionary[key.stringValue] = intValue + } else if let intValue = try? decode(Int16.self, forKey: key) { + dictionary[key.stringValue] = intValue + } else if let intValue = try? decode(Int32.self, forKey: key) { + dictionary[key.stringValue] = intValue + } else if let intValue = try? decode(Int64.self, forKey: key) { + dictionary[key.stringValue] = intValue + } else if let intValue = try? decode(UInt.self, forKey: key) { + dictionary[key.stringValue] = intValue + } else if let intValue = try? decode(UInt8.self, forKey: key) { + dictionary[key.stringValue] = intValue + } else if let intValue = try? decode(UInt16.self, forKey: key) { + dictionary[key.stringValue] = intValue + } else if let intValue = try? decode(UInt32.self, forKey: key) { + dictionary[key.stringValue] = intValue + } else if let intValue = try? decode(UInt64.self, forKey: key) { + dictionary[key.stringValue] = intValue + } else if let doubleValue = try? decode(Float.self, forKey: key) { + dictionary[key.stringValue] = doubleValue + } else if let doubleValue = try? decode(Double.self, forKey: key) { + dictionary[key.stringValue] = doubleValue + } else if let stringValue = try? decode(String.self, forKey: key) { + dictionary[key.stringValue] = stringValue + } else if let nestedDictionary = try? decode(Dictionary.self, forKey: key) { + dictionary[key.stringValue] = nestedDictionary + } else if let nestedArray = try? decode(Array.self, forKey: key) { + dictionary[key.stringValue] = nestedArray + } else if let value = try? decodeNil(forKey: key), value { + //saving NSNull values in a dictionary will produce unexpected results for users, just skip + } + } + return dictionary + } + + func decodeIfPresent(forKey key: K, defaultValue: T) -> T { + do { + //below will throw + return try self.decodeIfPresent(T.self, forKey: key) ?? defaultValue + } catch { + return defaultValue + } + } + +} + +extension UnkeyedDecodingContainer { + + mutating func decode(_ type: [[String: Any]].Type) throws -> [[String: Any]] { + var array: [[String: Any]] = [] + while isAtEnd == false { + if let nestedDictionary = try? decode(Dictionary.self) { + array.append(nestedDictionary) + } + } + return array + } + + mutating func decode(_ type: [Any].Type) throws -> [Any] { + var array: [Any] = [] + while isAtEnd == false { + if let value = try? decode(Bool.self) { + array.append(value) + } else if let value = try? decode(Int.self) { + array.append(value) + } else if let value = try? decode(Int8.self) { + array.append(value) + } else if let value = try? decode(Int16.self) { + array.append(value) + } else if let value = try? decode(Int32.self) { + array.append(value) + } else if let value = try? decode(Int64.self) { + array.append(value) + } else if let value = try? decode(UInt.self) { + array.append(value) + } else if let value = try? decode(UInt16.self) { + array.append(value) + } else if let value = try? decode(UInt32.self) { + array.append(value) + } else if let value = try? decode(UInt64.self) { + array.append(value) + } else if let value = try? decode(Float.self) { + array.append(value) + } else if let value = try? decode(Double.self) { + array.append(value) + } else if let value = try? decode(String.self) { + array.append(value) + } else if let nestedDictionary = try? decode(Dictionary.self) { + array.append(nestedDictionary) + } else if let nestedArray = try? decodeNestedArray(Array.self) { + array.append(nestedArray) + } else if let value = try? decodeNil(), value { + array.append(NSNull()) //unavoidable, but should be fine. We return [Any]. An overload to return homegenous array would be nice. + } else { + //if the right type is not found, it will get stuck in an infinite loop, throw, we can't handle it + throw EncodingError.invalidValue("", EncodingError.Context(codingPath: codingPath, debugDescription: "")) + } + } + + return array + } + + mutating func decodeNestedArray(_ type: [Any].Type) throws -> [Any] { + // throws: `CocoaError.coderTypeMismatch` if the encountered stored value is not an unkeyed container. + var nestedContainer = try self.nestedUnkeyedContainer() + return try nestedContainer.decode(Array.self) + } + + mutating func decode(_ type: [String: Any].Type) throws -> [String: Any] { + // throws: `CocoaError.coderTypeMismatch` if the encountered stored value is not a keyed container. + let nestedContainer = try self.nestedContainer(keyedBy: JSONCodingKeys.self) + return try nestedContainer.decode(type) + } +} + +extension KeyedEncodingContainerProtocol where Key == JSONCodingKeys { + + mutating func encode(_ value: [String: Any]) throws { + for (key, value) in value { + let key = JSONCodingKeys(stringValue: key) + switch value { + case let value as Bool: + try encode(value, forKey: key) + case let value as Int: + try encode(value, forKey: key) + case let value as Int8: + try encode(value, forKey: key) + case let value as Int16: + try encode(value, forKey: key) + case let value as Int32: + try encode(value, forKey: key) + case let value as Int64: + try encode(value, forKey: key) + case let value as UInt: + try encode(value, forKey: key) + case let value as UInt8: + try encode(value, forKey: key) + case let value as UInt16: + try encode(value, forKey: key) + case let value as UInt32: + try encode(value, forKey: key) + case let value as UInt64: + try encode(value, forKey: key) + case let value as Float: + try encode(value, forKey: key) + case let value as Double: + try encode(value, forKey: key) + case let value as String: + try encode(value, forKey: key) + case let value as [String: Any]: + try encode(value, forKey: key) + case let value as [Any]: + try encode(value, forKey: key) + case is NSNull: + try encodeNil(forKey: key) + case Optional.none: + try encodeNil(forKey: key) + default: + throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: codingPath + [key], debugDescription: "Invalid JSON value")) + } + } + } +} + +extension KeyedEncodingContainerProtocol { + mutating func encode(_ value: [String: Any]?, forKey key: Key) throws { + guard let value = value else { return } + + var container = nestedContainer(keyedBy: JSONCodingKeys.self, forKey: key) + try container.encode(value) + } + + mutating func encode(_ value: [Any]?, forKey key: Key) throws { + guard let value = value else { return } + + var container = nestedUnkeyedContainer(forKey: key) + try container.encode(value) + } +} + +extension UnkeyedEncodingContainer { + + mutating func encode(_ value: [Any]) throws { + for (index, value) in value.enumerated() { + switch value { + case let value as Bool: + try encode(value) + case let value as Int: + try encode(value) + case let value as Int8: + try encode(value) + case let value as Int16: + try encode(value) + case let value as Int32: + try encode(value) + case let value as Int64: + try encode(value) + case let value as UInt: + try encode(value) + case let value as UInt8: + try encode(value) + case let value as UInt16: + try encode(value) + case let value as UInt32: + try encode(value) + case let value as UInt64: + try encode(value) + case let value as Float: + try encode(value) + case let value as Double: + try encode(value) + case let value as String: + try encode(value) + case let value as [String: Any]: + try encode(value) + case let value as [Any]: + try encodeNestedArray(value) + case is NSNull: + try encodeNil() + case Optional.none: + try encodeNil() + default: + let keys = JSONCodingKeys(intValue: index).map({ [ $0 ] }) ?? [] + throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: codingPath + keys, debugDescription: "Invalid JSON value")) + } + } + } + + mutating func encode(_ value: [String: Any]) throws { + var container = nestedContainer(keyedBy: JSONCodingKeys.self) + try container.encode(value) + } + + mutating func encodeNestedArray(_ value: [Any]) throws { + var container = nestedUnkeyedContainer() + try container.encode(value) + } +} diff --git a/ios/Gekidou/Sources/Gekidou/Networking/Network+Channels.swift b/ios/Gekidou/Sources/Gekidou/Networking/Network+Channels.swift deleted file mode 100644 index 222d9cf800..0000000000 --- a/ios/Gekidou/Sources/Gekidou/Networking/Network+Channels.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// Network+Channels.swift -// -// -// Created by Miguel Alatzar on 8/27/21. -// - -import Foundation - -extension Network { - public func fetchChannel(withId channelId: String, withServerUrl serverUrl: String, completionHandler: @escaping ResponseHandler) { - let endpoint = "/channels/\(channelId)" - let url = buildApiUrl(serverUrl, endpoint) - - return request(url, withMethod: "GET", withServerUrl: serverUrl, completionHandler: completionHandler) - } - - public func fetchChannelMembership(withChannelId channelId: String, withUserId userId: String, withServerUrl serverUrl: String, completionHandler: @escaping ResponseHandler) { - let endpoint = "/channels/\(channelId)/members/\(userId)" - let url = buildApiUrl(serverUrl, endpoint) - - return request(url, withMethod: "GET", withServerUrl: serverUrl, completionHandler: completionHandler) - } -} diff --git a/ios/Gekidou/Sources/Gekidou/Networking/Network+Teams.swift b/ios/Gekidou/Sources/Gekidou/Networking/Network+Teams.swift deleted file mode 100644 index 22ff2100a0..0000000000 --- a/ios/Gekidou/Sources/Gekidou/Networking/Network+Teams.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// Network+Teams.swift -// -// -// Created by Miguel Alatzar on 8/27/21. -// - -import Foundation - -extension Network { - public func fetchTeam(withId teamId: String, withServerUrl serverUrl: String, completionHandler: @escaping ResponseHandler) { - let endpoint = "/teams/\(teamId)" - let url = buildApiUrl(serverUrl, endpoint) - - return request(url, withMethod: "GET", withServerUrl: serverUrl, completionHandler: completionHandler) - } - - public func fetchTeamMembership(withTeamId teamId: String, withUserId userId: String, withServerUrl serverUrl: String, completionHandler: @escaping ResponseHandler) { - let endpoint = "/teams/\(teamId)/members/\(userId)" - let url = buildApiUrl(serverUrl, endpoint) - - return request(url, withMethod: "GET", withServerUrl: serverUrl, completionHandler: completionHandler) - } -} diff --git a/ios/Gekidou/Sources/Gekidou/Networking/Network+Users.swift b/ios/Gekidou/Sources/Gekidou/Networking/Network+Users.swift new file mode 100644 index 0000000000..bd0b5f87ac --- /dev/null +++ b/ios/Gekidou/Sources/Gekidou/Networking/Network+Users.swift @@ -0,0 +1,26 @@ +// +// Network+Channels.swift +// +// +// Created by Miguel Alatzar on 8/27/21. +// + +import Foundation + +extension Network { + public func fetchUsers(byIds userIds: [String], withServerUrl serverUrl: String, completionHandler: @escaping ResponseHandler) { + let endpoint = "/users/ids" + let url = buildApiUrl(serverUrl, endpoint) + let data = try? JSONSerialization.data(withJSONObject: userIds, options: []) + + return request(url, withMethod: "POST", withBody: data, withHeaders: nil, withServerUrl: serverUrl, completionHandler: completionHandler) + } + + public func fetchUsers(byUsernames usernames: [String], withServerUrl serverUrl: String, completionHandler: @escaping ResponseHandler) { + let endpoint = "users/usernames" + let url = buildApiUrl(serverUrl, endpoint) + let data = try? JSONSerialization.data(withJSONObject: usernames, options: []) + + return request(url, withMethod: "POST", withBody: data, withHeaders: nil, withServerUrl: serverUrl, completionHandler: completionHandler) + } +} diff --git a/ios/Gekidou/Sources/Gekidou/Networking/PushNotification.swift b/ios/Gekidou/Sources/Gekidou/Networking/PushNotification.swift index fe6b13d29d..a5565aafae 100644 --- a/ios/Gekidou/Sources/Gekidou/Networking/PushNotification.swift +++ b/ios/Gekidou/Sources/Gekidou/Networking/PushNotification.swift @@ -7,6 +7,7 @@ import Foundation import UserNotifications +import SQLite public struct AckNotification: Codable { let type: String @@ -18,12 +19,22 @@ public struct AckNotification: Codable { let platform = "ios" public enum AckNotificationKeys: String, CodingKey { - case type + case type = "type" + case id = "ack_id" + case postId = "post_id" + case serverUrl = "server_url" + case isIdLoaded = "id_loaded" + case platform = "platform" + } + + public enum AckNotificationRequestKeys: String, CodingKey { + case type = "type" case id = "ack_id" case postId = "post_id" case serverUrl = "server_url" case isIdLoaded = "is_id_loaded" - + case receivedAt = "received_at" + case platform = "platform" } public init(from decoder: Decoder) throws { @@ -42,6 +53,25 @@ public struct AckNotification: Codable { } } +extension AckNotification { + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: AckNotificationRequestKeys.self) + try container.encode(id, forKey: .id) + try container.encode(postId, forKey: .postId) + try container.encode(receivedAt, forKey: .receivedAt) + try container.encode(platform, forKey: .platform) + try container.encode(type, forKey: .type) + try container.encode(isIdLoaded, forKey: .isIdLoaded) + } +} + +extension String { + func removePrefix(_ prefix: String) -> String { + guard self.hasPrefix(prefix) else { return self } + return String(self.dropFirst(prefix.count)) + } +} + extension Network { @objc public func postNotificationReceipt(_ userInfo: [AnyHashable:Any]) { if let jsonData = try? JSONSerialization.data(withJSONObject: userInfo), @@ -50,6 +80,18 @@ extension Network { } } + private func matchUsername(in message: String) -> [String] { + let specialMentions = Set(["all", "here", "channel"]) + do { + let regex = try NSRegularExpression(pattern: "\\B@(([a-z0-9-._]*[a-z0-9_])[.-]*)", options: [.caseInsensitive]) + let results = regex.matches(in: message, range: _NSRange(message.startIndex..., in: message)) + return results.map{ String(message[Range($0.range, in: message)!]).removePrefix("@") }.filter{ !specialMentions.contains($0)} + } catch let error { + print("invalid regex: \(error.localizedDescription)") + return [] + } + } + public func postNotificationReceipt(_ ackNotification: AckNotification, completionHandler: @escaping ResponseHandler) { do { let jsonData = try JSONEncoder().encode(ackNotification) @@ -64,69 +106,70 @@ extension Network { } public func fetchAndStoreDataForPushNotification(_ notification: UNMutableNotificationContent, withContentHandler contentHandler: ((UNNotificationContent) -> Void)?) { - // TODO: All DB writes should be made in a single transaction + let operation = BlockOperation { let group = DispatchGroup() - var channel: Channel? = nil - var channelMembership: ChannelMembership? - - let teamId = notification.userInfo["team_id"] as! String? + let channelId = notification.userInfo["channel_id"] as! String let serverUrl = notification.userInfo["server_url"] as! String let currentUserId = try! Database.default.queryCurrentUserId(serverUrl) - - if let teamId = teamId { - if try! !Database.default.hasMyTeam(withId: teamId, withServerUrl: serverUrl) { - group.enter() - self.fetchTeam(withId: teamId, withServerUrl: serverUrl) { data, response, error in - if self.responseOK(response), let data = data { - let team = try! JSONDecoder().decode(Team.self, from: data) - try! Database.default.insertTeam(team, serverUrl) - } - - group.leave() - } - - group.enter() - self.fetchTeamMembership(withTeamId: teamId, withUserId: currentUserId, withServerUrl: serverUrl) { data, response, error in - if self.responseOK(response), let data = data { - let teamMembership = try! JSONDecoder().decode(TeamMembership.self, from: data) - if teamMembership.user_id == currentUserId { - try! Database.default.insertMyTeam(teamMembership, serverUrl) - } - } - - group.leave() - } - } - } + let currentUser = try! Database.default.queryCurrentUser(serverUrl) + let currentUsername = currentUser?[Expression("username")] - - group.enter() - self.fetchChannel(withId: channelId, withServerUrl: serverUrl) { data, response, error in - if self.responseOK(response), let data = data { - channel = try! JSONDecoder().decode(Channel.self, from: data) - } - - group.leave() - } - - group.enter() - self.fetchChannelMembership(withChannelId: channelId, withUserId: currentUserId, withServerUrl: serverUrl) { data, response, error in - if self.responseOK(response), let data = data { - channelMembership = try! JSONDecoder().decode(ChannelMembership.self, from: data) - } - - group.leave() - } + var postData: PostData? = nil + 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 if self.responseOK(response), let data = data { - let postData = try! JSONDecoder().decode(PostData.self, from: data) - if postData.posts.count > 0 { - try! Database.default.handlePostData(postData, channelId, serverUrl, since != nil) + postData = try! JSONDecoder().decode(PostData.self, from: data) + if postData?.posts.count ?? 0 > 0 { + var authorIds: Set = Set() + var usernames: Set = Set() + postData!.posts.forEach{post in + if (post.user_id != currentUserId) { + authorIds.insert(post.user_id) + } + self.matchUsername(in: post.message).forEach{ + if ($0 != currentUsername) { + usernames.insert($0) + } + } + } + + if (authorIds.count > 0) { + let existingIds = try! Database.default.queryUsers(byIds: authorIds, withServerUrl: serverUrl) + userIdsToLoad = authorIds.filter { !existingIds.contains($0) } + if (userIdsToLoad.count > 0) { + group.enter() + self.fetchUsers(byIds: Array(userIdsToLoad), withServerUrl: serverUrl) { data, response, error in + if self.responseOK(response), let data = data { + let usersData = try! JSONDecoder().decode([User].self, from: data) + usersData.forEach { users.insert($0) } + } + group.leave() + } + } + } + + if (usernames.count > 0) { + let existingUsernames = try! Database.default.queryUsers(byUsernames: usernames, withServerUrl: serverUrl) + usernamesToLoad = usernames.filter{ !existingUsernames.contains($0)} + if (usernamesToLoad.count > 0) { + group.enter() + + self.fetchUsers(byUsernames: Array(usernamesToLoad), withServerUrl: serverUrl) { data, response, error in + if self.responseOK(response), let data = data { + let usersData = try! JSONDecoder().decode([User].self, from: data) + usersData.forEach { users.insert($0) } + } + group.leave() + } + } + } } } @@ -135,7 +178,17 @@ extension Network { group.wait() - try! Database.default.handleChannelAndMembership(channel, channelMembership, serverUrl) + group.enter() + if (postData != nil) { + let db = try! Database.default.getDatabaseForServer(serverUrl) + try! db.transaction { + try! Database.default.handlePostData(db, postData!, channelId, since != nil) + if (users.count > 0) { + try! Database.default.insertUsers(db, users) + } + } + } + group.leave() if let contentHandler = contentHandler { contentHandler(notification) diff --git a/ios/Gekidou/Sources/Gekidou/Storage/Database+Channels.swift b/ios/Gekidou/Sources/Gekidou/Storage/Database+Channels.swift deleted file mode 100644 index 32b581c204..0000000000 --- a/ios/Gekidou/Sources/Gekidou/Storage/Database+Channels.swift +++ /dev/null @@ -1,204 +0,0 @@ -// -// Database+Channels.swift -// -// -// Created by Miguel Alatzar on 8/27/21. -// - -import Foundation -import SQLite - -public struct Channel: Codable { - let id: String - let create_at: Int64 - let update_at: Int64 - let delete_at: Int64 - let team_id: String - let type: String - let display_name: String - let name: String - let creator_id: String - let header: String - let purpose: String - let total_msg_count: Int64 -} - -public struct ChannelMembership: Codable { - let channel_id: String - let user_id: String - let roles: String - let last_viewed_at: Int64 - let mention_count: Int64 - let msg_count: Int64 - let notify_props: [String:String] -} - -extension Database { - public func queryChannel(withId channelId: String, withServerUrl serverUrl: String) throws -> Row? { - let db = try getDatabaseForServer(serverUrl) - - let idCol = Expression("id") - let query = channelTable.where(idCol == channelId) - - return try db.pluck(query) - } - - public func queryMyChannel(withId channelId: String, withServerUrl serverUrl: String) throws -> Row? { - let db = try getDatabaseForServer(serverUrl) - - let idCol = Expression("id") - let query = myChannelTable.where(idCol == channelId) - - return try db.pluck(query) - } - - public func insertChannel(_ channel: Channel, _ serverUrl: String) throws { - let db = try getDatabaseForServer(serverUrl) - - let setter = createChannelSetter(from: channel) - let query = channelTable.insert(or: .replace, setter) - try db.run(query) - } - - public func insertChannelInfo(_ channel: Channel, _ serverUrl: String) throws { - let db = try getDatabaseForServer(serverUrl) - - let setter = createChannelInfoSetter(from: channel) - let query = channelInfoTable.insert(or: .replace, setter) - try db.run(query) - } - - public func insertMyChannel(_ channelMembership: ChannelMembership, _ serverUrl: String) throws { - let db = try getDatabaseForServer(serverUrl) - - let setter = createMyChannelSetter(from: channelMembership) - let query = myChannelTable.insert(or: .replace, setter) - try db.run(query) - } - - public func insertMyChannelSettings(_ channelMembership: ChannelMembership, _ serverUrl: String) throws { - let db = try getDatabaseForServer(serverUrl) - - let setter = createMyChannelSettingsSetter(from: channelMembership) - let query = myChannelSettingsTable.insert(or: .replace, setter) - try db.run(query) - } - - public func insertChannelMembership(_ channelMembership: ChannelMembership, _ serverUrl: String) throws { - let db = try getDatabaseForServer(serverUrl) - - let setter = createChannelMembershipSetter(from: channelMembership) - let query = channelMembershipTable.insert(or: .replace, setter) - try db.run(query) - } - - public func handleChannelAndMembership(_ channel: Channel?, _ channelMembership: ChannelMembership?, _ serverUrl: String) throws { - if let channel = channel { - try insertChannel(channel, serverUrl) - try insertChannelInfo(channel, serverUrl) - } - - if let channelMembership = channelMembership { - try insertMyChannel(channelMembership, serverUrl) - try insertMyChannelSettings(channelMembership, serverUrl) - try insertChannelMembership(channelMembership, serverUrl) - } - - try updateMyChannelMessageCount(channel, channelMembership, serverUrl) - } - - private func updateMyChannelMessageCount(_ channel: Channel?, _ channelMembership: ChannelMembership?, _ serverUrl: String) throws { - if let channel = channel, let channelMembership = channelMembership { - let db = try getDatabaseForServer(serverUrl) - - let messageCount = channel.total_msg_count - channelMembership.msg_count - let idCol = Expression("id") - let messageCountCol = Expression("message_count") - let query = myChannelTable - .where(idCol == channel.id) - .update(messageCountCol <- messageCount) - - try db.run(query) - } - } - - private func createChannelSetter(from channel: Channel) -> [Setter] { - let id = Expression("id") - let createAt = Expression("create_at") - let updateAt = Expression("update_at") - let deleteAt = Expression("delete_at") - let teamId = Expression("team_id") - let type = Expression("type") - let displayName = Expression("display_name") - let name = Expression("name") - let creatorId = Expression("creator_id") - - var setter = [Setter]() - setter.append(id <- channel.id) - setter.append(createAt <- channel.create_at) - setter.append(updateAt <- channel.update_at) - setter.append(deleteAt <- channel.delete_at) - setter.append(teamId <- channel.team_id) - setter.append(type <- channel.type) - setter.append(displayName <- channel.display_name) - setter.append(name <- channel.name) - setter.append(creatorId <- channel.creator_id) - - return setter - } - - private func createChannelInfoSetter(from channel: Channel) -> [Setter] { - let id = Expression("id") - let header = Expression("header") - let purpose = Expression("purpose") - - var setter = [Setter]() - setter.append(id <- channel.id) - setter.append(header <- channel.header) - setter.append(purpose <- channel.purpose) - - return setter - } - - private func createMyChannelSetter(from channelMembership: ChannelMembership) -> [Setter] { - let id = Expression("id") - let roles = Expression("roles") - let lastViewedAt = Expression("last_viewed_at") - let mentionsCount = Expression("mentions_count") - - var setter = [Setter]() - setter.append(id <- channelMembership.channel_id) - setter.append(roles <- channelMembership.roles) - setter.append(lastViewedAt <- channelMembership.last_viewed_at) - setter.append(mentionsCount <- channelMembership.mention_count) - - return setter - } - - private func createMyChannelSettingsSetter(from channelMembership: ChannelMembership) -> [Setter] { - let id = Expression("id") - let notifyProps = Expression("notify_props") - - let notifyPropsJSON = try! JSONSerialization.data(withJSONObject: channelMembership.notify_props, options: []) - let notifyPropsString = String(data: notifyPropsJSON, encoding: String.Encoding.utf8)! - - var setter = [Setter]() - setter.append(id <- channelMembership.channel_id) - setter.append(notifyProps <- notifyPropsString) - - return setter - } - - private func createChannelMembershipSetter(from channelMembership: ChannelMembership) -> [Setter] { - let id = Expression("id") - let channelId = Expression("channel_id") - let userId = Expression("user_id") - - var setter = [Setter]() - setter.append(id <- channelMembership.channel_id) - setter.append(channelId <- channelMembership.channel_id) - setter.append(userId <- channelMembership.user_id) - - return setter - } -} diff --git a/ios/Gekidou/Sources/Gekidou/Storage/Database+Posts.swift b/ios/Gekidou/Sources/Gekidou/Storage/Database+Posts.swift index 7aac545c52..942ad17d36 100644 --- a/ios/Gekidou/Sources/Gekidou/Storage/Database+Posts.swift +++ b/ios/Gekidou/Sources/Gekidou/Storage/Database+Posts.swift @@ -8,132 +8,6 @@ import Foundation import SQLite -public struct EmbedMedia: Codable { - let type: String? - let url: String? - let secure_url: String? - let width: Int64? - let height: Int64? -} - -public struct EmbedData: Codable { - let type: String? - let url: String? - let title: String? - let description: String? - let determiner: String? - let site_name: String? - let locale: String? - let locales_alternate: String? - let images: [EmbedMedia]? - let audios: [EmbedMedia]? - let videos: [EmbedMedia]? -} - -public struct Embed: Codable { - let type: String? - let url: String? - let data: EmbedData? -} - -public struct Emoji: Codable { - let id: String - let name: String -} - -public struct File: Codable { - let id: String - let post_id: String - let name: String - let `extension`: String - let size: Int64 - let mime_type: String - let width: Int64 - let height: Int64 - let local_path: String? - let mini_preview: String? -} - -public struct Reaction: Codable { - let user_id: String - let post_id: String - let emoji_name: String - let create_at: Int64 -} - -public struct Image: Codable { - let width: Int64 - let height: Int64 - let format: String - let frame_count: Int64 -} - -public struct PostMetadata: Codable { - let embeds: [Embed]? - let images: [String:Image]? - var emojis: [Emoji]? - var files: [File]? - var reactions: [Reaction]? -} - -public struct PostPropsAddChannelMember: Codable { - let post_id: String - let usernames: String - let not_in_channel_usernames: String - let user_ids: String - let not_in_channel_user_ids: String - let not_in_groups_usernames: String - let not_in_groups_user_ids: String -} - -public struct PostPropsAttachment: Codable { - let id: Int64 - let fallback: String - let color: String - let pretext: String - let author_name: String - let author_link: String - let author_icon: String - let title: String - let title_link: String - let text: String - let image_url: String - let thumb_url: String - let footer: String - let footer_icon: String - - // TODO: - // fields - // timestamp - // actions -} - -public struct PostProps: Codable { - let userId: String? - let username: String? - let addedUserId: String? - let removedUserId: String? - let removedUsername: String? - let deleteBy: String? - let old_header: String? - let new_header: String? - let old_purpose: String? - let new_purpose: String? - let old_displayname: String? - let new_displayname: String? - let mentionHighlightDisabled: Bool? - let disable_group_highlight: Bool? - let override_username: Bool? - let from_webhook: Bool? - let override_icon_url: Bool? - let override_icon_emoji: Bool? - let add_channel_member: PostPropsAddChannelMember? - let attachments: [PostPropsAttachment]? - - // TODO: - // appBindings -} - public struct Post: Codable { let id: String let create_at: Int64 @@ -147,21 +21,62 @@ public struct Post: Codable { let original_id: String let message: String let type: String - var props: PostProps? - let hashtag: String? - let pending_post_id: String? - let reply_count: Int64 - let file_ids: [String]? - let metadata: PostMetadata? - let last_reply_at: Int64? - let failed: Bool? - let ownPost: Bool? - let participants: [String]? - var prev_post_id: String? + let props: String + let pending_post_id: String + let metadata: String + var prev_post_id: String + + public enum PostKeys: String, CodingKey { + case id = "id" + case create_at = "create_at" + case update_at = "update_at" + case delete_at = "delete_at" + case edit_at = "edit_at" + case is_pinned = "is_pinned" + case user_id = "user_id" + case channel_id = "channel_id" + case root_id = "root_id" + case original_id = "original_id" + case message = "message" + case type = "type" + case props = "props" + case pending_post_id = "pending_post_id" + case metadata = "metadata" + } + + public init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: PostKeys.self) + prev_post_id = "" + id = try values.decode(String.self, forKey: .id) + create_at = try values.decode(Int64.self, forKey: .create_at) + update_at = try values.decode(Int64.self, forKey: .update_at) + delete_at = try values.decode(Int64.self, forKey: .delete_at) + edit_at = try values.decode(Int64.self, forKey: .edit_at) + is_pinned = try values.decode(Bool.self, forKey: .is_pinned) + user_id = try values.decode(String.self, forKey: .user_id) + channel_id = try values.decode(String.self, forKey: .channel_id) + root_id = try values.decode(String.self, forKey: .root_id) + original_id = try values.decode(String.self, forKey: .original_id) + message = try values.decode(String.self, forKey: .message) + let meta = try values.decode([String:Any].self, forKey: .metadata) + metadata = Database.default.json(from: meta) ?? "{}" + type = try values.decode(String.self, forKey: .type) + 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) ?? "{}" + } } struct MetadataSetters { - let postMetadataSetters: [[Setter]] + let metadata: String + let reactionSetters: [[Setter]] + let fileSetters: [[Setter]] + let emojiSetters: [[Setter]] +} + +struct PostSetters { + let id: String + let postSetters: [Setter] let reactionSetters: [[Setter]] let fileSetters: [[Setter]] let emojiSetters: [[Setter]] @@ -228,32 +143,31 @@ extension Database { return (0, 0) } - public func handlePostData(_ postData: PostData, _ channelId: String, _ serverUrl: String, _ usedSince: Bool = false) throws { - try insertAndDeletePosts(postData.posts, channelId, serverUrl) - try handlePostsInChannel(postData, channelId, serverUrl, usedSince) - try handlePostMetadata(postData.posts, channelId, serverUrl) - try handlePostsInThread(postData.posts, serverUrl) + public func handlePostData(_ db: Connection, _ postData: PostData, _ channelId: String, _ usedSince: Bool = false) throws { + let sortedChainedPosts = chainAndSortPosts(postData) + try insertOrUpdatePosts(db, sortedChainedPosts, channelId) + try handlePostsInChannel(db, channelId, postData, usedSince) + try handlePostsInThread(db, postData.posts) } - private func handlePostsInChannel(_ postData: PostData, _ channelId: String, _ serverUrl: String, _ usedSince: Bool = false) throws { - let sortedChainedPosts = chainAndSortPosts(postData) - let earliest = sortedChainedPosts.first!.create_at - let latest = sortedChainedPosts.last!.create_at + private func handlePostsInChannel(_ db: Connection, _ channelId: String, _ postData: PostData, _ usedSince: Bool = false) throws { + let firstId = postData.order.first + let lastId = postData.order.last + let earliest = postData.posts.first(where: { $0.id == lastId})!.create_at + let latest = postData.posts.first(where: { $0.id == firstId})!.create_at if usedSince { - try updatePostsInChannelLatestOnly(latest, channelId, serverUrl) + try updatePostsInChannelLatestOnly(db, channelId, latest) } else { - let updated = try updatePostsInChannelEarliestAndLatest(earliest, latest, channelId, serverUrl) + let updated = try updatePostsInChannelEarliestAndLatest(db, channelId, earliest, latest) if (!updated) { - try insertPostsInChannel(earliest, latest, channelId, serverUrl) + try insertPostsInChannel(db, channelId, earliest, latest) } } } - private func handlePostsInThread(_ posts: [Post], _ serverUrl: String) throws { - let db = try getDatabaseForServer(serverUrl) - - let postsInThreadSetters = try createPostsInThreadSetters(from: posts, withServerUrl: serverUrl) + private func handlePostsInThread(_ db: Connection, _ posts: [Post]) throws { + let postsInThreadSetters = try createPostsInThreadSetters(db, from: posts) if !postsInThreadSetters.isEmpty { let insertQuery = postsInThreadTable.insertMany(or: .replace, postsInThreadSetters) try db.run(insertQuery) @@ -262,44 +176,45 @@ extension Database { private func chainAndSortPosts(_ postData: PostData) -> [Post] { let order = postData.order - let prevPostId = postData.prev_post_id let posts = postData.posts - - return order.enumerated().reduce([Post]()) { (chainedPosts: [Post], current) in - let index = current.0 - let postId = current.1 + var prevPostId = "" + + return posts.sorted(by: {$0.create_at < $1.create_at}).enumerated().map { (index, post) in + var modified = post + if (index == 0) { + modified.prev_post_id = postData.prev_post_id + } else { + modified.prev_post_id = prevPostId + } - var post = posts.first(where: {$0.id == postId})! - post.prev_post_id = index == order.count - 1 ? - prevPostId : - order[index + 1] - - return chainedPosts + [post] - }.sorted(by: { $0.create_at < $1.create_at }) + if (order.contains(post.id)) { + prevPostId = post.id + } + + return modified + } } - private func updatePostsInChannelLatestOnly(_ latest: Int64, _ channelId: String, _ serverUrl: String) throws { - let db = try getDatabaseForServer(serverUrl) - + private func updatePostsInChannelLatestOnly(_ db: Connection, _ channelId: String, _ latest: Int64) throws { let channelIdCol = Expression("channel_id") let latestCol = Expression("latest") + let statusCol = Expression("_status") let query = postsInChannelTable .where(channelIdCol == channelId) .order(latestCol.desc) .limit(1) - .update(latestCol <- latest) + .update(latestCol <- latest, statusCol <- "updated") try db.run(query) } - private func updatePostsInChannelEarliestAndLatest(_ earliest: Int64, _ latest: Int64, _ channelId: String, _ serverUrl: String) throws -> Bool { - let db = try getDatabaseForServer(serverUrl) - + private func updatePostsInChannelEarliestAndLatest(_ db: Connection, _ channelId: String, _ earliest: Int64, _ latest: Int64) throws -> Bool { let idCol = Expression("id") let channelIdCol = Expression("channel_id") let earliestCol = Expression("earliest") let latestCol = Expression("latest") + let statusCol = Expression("_status") let query = postsInChannelTable .where(channelIdCol == channelId && (earliestCol <= earliest || latestCol >= latest)) @@ -313,7 +228,7 @@ extension Database { let updateQuery = postsInChannelTable .filter(idCol == recordId) - .update(earliestCol <- min(earliest, recordEarliest), latestCol <- max(latest, recordLatest)) + .update(earliestCol <- min(earliest, recordEarliest), latestCol <- max(latest, recordLatest), statusCol <- "updated") try db.run(updateQuery) @@ -323,20 +238,20 @@ extension Database { return false } - private func insertPostsInChannel(_ earliest: Int64, _ latest: Int64, _ channelId: String, _ serverUrl: String) throws { - let db = try getDatabaseForServer(serverUrl) - - let rowIdCol = Expression("rowid") + private func insertPostsInChannel(_ db: Connection, _ channelId: String, _ earliest: Int64, _ latest: Int64) throws { + let idCol = Expression("id") let channelIdCol = Expression("channel_id") let earliestCol = Expression("earliest") let latestCol = Expression("latest") + let statusCol = Expression("_status") + let id = generateId() let query = postsInChannelTable - .insert(channelIdCol <- channelId, earliestCol <- earliest, latestCol <- latest) - let newRecordId = try db.run(query) + .insert(idCol <- id, channelIdCol <- channelId, earliestCol <- earliest, latestCol <- latest, statusCol <- "created") + try db.run(query) let deleteQuery = postsInChannelTable - .where(rowIdCol != newRecordId && + .where(idCol != id && channelIdCol == channelId && earliestCol >= earliest && latestCol <= latest) @@ -345,54 +260,33 @@ extension Database { try db.run(deleteQuery) } - private func insertAndDeletePosts(_ posts: [Post], _ channelId: String, _ serverUrl: String) throws { - let db = try getDatabaseForServer(serverUrl) - + private func insertOrUpdatePosts(_ db: Connection, _ posts: [Post], _ channelId: String) throws { let setters = createPostSetters(from: posts) - let insertQuery = postTable.insertMany(or: .replace, setters) - try db.run(insertQuery) - - let deleteIds = posts.reduce([String]()) { (accumulated, post) in - if let deleteId = post.pending_post_id { - return accumulated + [deleteId] + for setter in setters { + let insertPost = postTable.insert(or: .replace, setter.postSetters) + try db.run(insertPost) + + if !setter.emojiSetters.isEmpty { + let insertEmojis = emojiTable.insertMany(or: .ignore, setter.emojiSetters) + try db.run(insertEmojis) + } + + if !setter.fileSetters.isEmpty { + let insertFiles = fileTable.insertMany(or: .ignore, setter.fileSetters) + try db.run(insertFiles) + } + + if !setter.reactionSetters.isEmpty { + let postIdCol = Expression("post_id") + let deletePreviousReactions = reactionTable.where(postIdCol == setter.id).delete() + try db.run(deletePreviousReactions) + let insertReactions = reactionTable.insertMany(setter.reactionSetters) + try db.run(insertReactions) } - - return accumulated - } - let id = Expression("id") - let deleteQuery = postTable - .filter(deleteIds.contains(id)) - .delete() - try db.run(deleteQuery) - } - - private func handlePostMetadata(_ posts: [Post], _ channelId: String, _ serverUrl: String) throws { - let db = try getDatabaseForServer(serverUrl) - - let setters = createPostMetadataSetters(from: posts) - - if !setters.postMetadataSetters.isEmpty { - let insertQuery = postMetadataTable.insertMany(or: .replace, setters.postMetadataSetters) - try db.run(insertQuery) - } - - if !setters.reactionSetters.isEmpty { - let insertQuery = reactionTable.insertMany(or: .replace, setters.reactionSetters) - try db.run(insertQuery) - } - - if !setters.fileSetters.isEmpty { - let insertQuery = fileTable.insertMany(or: .replace, setters.fileSetters) - try db.run(insertQuery) - } - - if !setters.emojiSetters.isEmpty { - let insertQuery = emojiTable.insertMany(or: .replace, setters.emojiSetters) - try db.run(insertQuery) } } - private func createPostSetters(from posts: [Post]) -> [[Setter]] { + private func createPostSetters(from posts: [Post]) -> [PostSetters] { let id = Expression("id") let createAt = Expression("create_at") let updateAt = Expression("update_at") @@ -404,21 +298,18 @@ extension Database { let rootId = Expression("root_id") let originalId = Expression("original_id") let message = Expression("message") + let metadata = Expression("metadata") let type = Expression("type") -// let hashtag = Expression("hashtag") let pendingPostId = Expression("pending_post_id") -// let replyCount = Expression("reply_count") -// let fileIds = Expression("file_ids") -// let lastReplyAt = Expression("last_reply_at") -// let failed = Expression("failed") -// let ownPost = Expression("ownPost") let prevPostId = Expression("previous_post_id") -// let participants = Expression("participants") let props = Expression("props") + let statusCol = Expression("_status") + + var postsSetters: [PostSetters] = [] - var setters = [[Setter]]() for post in posts { var setter = [Setter]() + let metadataSetters = createPostMetadataSetters(from: post) setter.append(id <- post.id) setter.append(createAt <- post.create_at) setter.append(updateAt <- post.update_at) @@ -430,32 +321,28 @@ extension Database { setter.append(rootId <- post.root_id) setter.append(originalId <- post.original_id) setter.append(message <- post.message) + setter.append(metadata <- metadataSetters.metadata) setter.append(type <- post.type) -// setter.append(hashtag <- post.hashtag) setter.append(pendingPostId <- post.pending_post_id) -// setter.append(replyCount <- post.reply_count) -// setter.append(fileIds <- json(from: post.file_ids)) -// setter.append(lastReplyAt <- post.last_reply_at) -// setter.append(failed <- post.failed) -// setter.append(ownPost <- post.ownPost) setter.append(prevPostId <- post.prev_post_id) -// setter.append(participants <- json(from: post.participants)) + setter.append(props <- post.props) + setter.append(statusCol <- "created") - if let postProps = post.props { - let propsJSON = try! JSONEncoder().encode(postProps) - let propsString = String(data: propsJSON, encoding: String.Encoding.utf8) - setter.append(props <- propsString) - } - - setters.append(setter) + let postSetter = PostSetters( + id: post.id, + postSetters: setter, + reactionSetters: metadataSetters.reactionSetters, + fileSetters: metadataSetters.fileSetters, + emojiSetters: metadataSetters.emojiSetters + ) + postsSetters.append(postSetter) } - return setters + return postsSetters } - private func createPostMetadataSetters(from posts: [Post]) -> MetadataSetters { + private func createPostMetadataSetters(from post: Post) -> MetadataSetters { let id = Expression("id") - let data = Expression("data") let userId = Expression("user_id") let postId = Expression("post_id") let emojiName = Expression("emoji_name") @@ -468,85 +355,86 @@ extension Database { let height = Expression("height") let localPath = Expression("local_path") let imageThumbnail = Expression("image_thumbnail") + let statusCol = Expression("_status") - var postMetadataSetters = [[Setter]]() + var metadataString = "{}" var reactionSetters = [[Setter]]() var fileSetters = [[Setter]]() var emojiSetters = [[Setter]]() - for post in posts { - if var metadata = post.metadata { - // Reaction setters - if let reactions = metadata.reactions { - for reaction in reactions { + let json = try? JSONSerialization.jsonObject(with: post.metadata.data(using: .utf8)!, options: []) + if var metadata = json as? [String: Any] { + // Reaction setters + if let reactions = metadata["reactions"] as? [Any] { + for reaction in reactions { + if let r = reaction as? [String: Any] { var reactionSetter = [Setter]() - reactionSetter.append(userId <- reaction.user_id) - reactionSetter.append(postId <- reaction.post_id) - reactionSetter.append(emojiName <- reaction.emoji_name) - reactionSetter.append(createAt <- reaction.create_at) - + reactionSetter.append(id <- generateId()) + reactionSetter.append(userId <- r["user_id"] as! String) + reactionSetter.append(postId <- r["post_id"] as! String) + reactionSetter.append(emojiName <- r["emoji_name"] as! String) + reactionSetter.append(createAt <- r["create_at"] as! Int64) + reactionSetter.append(statusCol <- "created") + reactionSetters.append(reactionSetter) } - - metadata.reactions = nil } - - // File setters - if let files = metadata.files { - for file in files { + metadata.removeValue(forKey: "reactions") + } + + // File setters + if let files = metadata["files"] as? [Any] { + for file in files { + if let f = file as? [String: Any] { var fileSetter = [Setter]() - fileSetter.append(id <- file.id) - fileSetter.append(postId <- file.post_id) - fileSetter.append(name <- file.name) - fileSetter.append(ext <- file.`extension`) - fileSetter.append(size <- file.size) - fileSetter.append(mimeType <- file.mime_type) - fileSetter.append(width <- file.width) - fileSetter.append(height <- file.height) - fileSetter.append(localPath <- file.local_path) - fileSetter.append(imageThumbnail <- file.mini_preview) - + fileSetter.append(id <- f["id"] as! String) + fileSetter.append(postId <- f["post_id"] as! String) + fileSetter.append(name <- f["name"] as! String) + fileSetter.append(ext <- f["extension"] as! String) + fileSetter.append(size <- f["size"] as! Int64) + fileSetter.append(mimeType <- f["mime_type"] as! String) + fileSetter.append(width <- f["width"] as! Int64) + fileSetter.append(height <- f["height"] as! Int64) + fileSetter.append(localPath <- "") + fileSetter.append(imageThumbnail <- f["mini_preview"] as? String) + fileSetter.append(statusCol <- "created") + fileSetters.append(fileSetter) } - - metadata.files = nil } - // Emoji setters - if let emojis = metadata.emojis { - for emoji in emojis { - var emojiSetter = [Setter]() - emojiSetter.append(id <- emoji.id) - emojiSetter.append(name <- emoji.name) - - emojiSetters.append(emojiSetter) - } - - metadata.emojis = nil - } - - // Metadata setter - var metadataSetter = [Setter]() - - metadataSetter.append(id <- post.id) - - let dataJSON = try! JSONEncoder().encode(metadata) - let dataString = String(data: dataJSON, encoding: String.Encoding.utf8)! - metadataSetter.append(data <- dataString) - - postMetadataSetters.append(metadataSetter) + metadata.removeValue(forKey: "files") } + + // Emoji setters + if let emojis = metadata["emojis"] as? [Any] { + for emoji in emojis { + if let e = emoji as? [String: Any] { + var emojiSetter = [Setter]() + emojiSetter.append(id <- e["id"] as! String) + emojiSetter.append(name <- e["name"] as! String) + emojiSetter.append(statusCol <- "created") + + emojiSetters.append(emojiSetter) + } + } + + metadata.removeValue(forKey: "emojis") + } + + // Remaining Metadata + + let dataJSON = try! JSONSerialization.data(withJSONObject: metadata, options: []) + metadataString = String(data: dataJSON, encoding: String.Encoding.utf8)! } - return MetadataSetters(postMetadataSetters: postMetadataSetters, + return MetadataSetters(metadata: metadataString, reactionSetters: reactionSetters, fileSetters: fileSetters, emojiSetters: emojiSetters) } - private func createPostsInThreadSetters(from posts: [Post], withServerUrl serverUrl: String) throws -> [[Setter]] { - let db = try getDatabaseForServer(serverUrl) - + private func createPostsInThreadSetters(_ db: Connection, from posts: [Post]) throws -> [[Setter]] { var setters = [[Setter]]() var postsInThread = [String: [Post]]() @@ -562,6 +450,7 @@ extension Database { let rootIdCol = Expression("root_id") let earliestCol = Expression("earliest") let latestCol = Expression("latest") + let statusCol = Expression("_status") for (rootId, posts) in postsInThread { let sortedPosts = posts.sorted(by: { $0.create_at < $1.create_at }) @@ -579,13 +468,15 @@ extension Database { let updateQuery = postsInThreadTable .where(rootIdCol == rootId && earliestCol == rowEarliest && latestCol == rowLatest) .update(earliestCol <- min(earliest, rowEarliest), - latestCol <- max(latest, rowLatest)) + latestCol <- max(latest, rowLatest), statusCol <- "updated") try db.run(updateQuery) } else { var setter = [Setter]() + setter.append(Expression("id") <- generateId()) setter.append(rootIdCol <- rootId) setter.append(earliestCol <- earliest) setter.append(latestCol <- latest) + setter.append(statusCol <- "created") setters.append(setter) } diff --git a/ios/Gekidou/Sources/Gekidou/Storage/Database+Teams.swift b/ios/Gekidou/Sources/Gekidou/Storage/Database+Teams.swift deleted file mode 100644 index 3d585c5fed..0000000000 --- a/ios/Gekidou/Sources/Gekidou/Storage/Database+Teams.swift +++ /dev/null @@ -1,100 +0,0 @@ -// -// Database+Teams.swift -// -// -// Created by Miguel Alatzar on 8/27/21. -// - -import Foundation -import SQLite - -public struct Team: Codable { - let id: String - let update_at: Int64 - let display_name: String - let name: String - let description: String - let type: String - let allowed_domains: String - let allow_open_invite: Bool - let policy_id: String -} - -public struct TeamMembership: Codable { - let team_id: String - let user_id: String - let roles: String - let delete_at: Int64 - let scheme_user: Bool - let scheme_admin: Bool - let explicit_roles: String -} - -extension Database { - public func hasMyTeam(withId teamId: String, withServerUrl serverUrl: String) throws -> Bool { - let db = try getDatabaseForServer(serverUrl) - - let idCol = Expression("id") - let query = myTeamTable.where(idCol == teamId) - - if let _ = try db.pluck(query) { - return true - } - - return false - } - - public func insertTeam(_ team: Team, _ serverUrl: String) throws { - let db = try getDatabaseForServer(serverUrl) - - let setter = createTeamSetter(from: team) - let insertQuery = teamTable.insert(or: .replace, setter) - try db.run(insertQuery) - } - - public func insertMyTeam(_ teamMembership: TeamMembership, _ serverUrl: String) throws { - let db = try getDatabaseForServer(serverUrl) - - let setter = createMyTeamSetter(from: teamMembership) - let insertQuery = myTeamTable.insert(or: .replace, setter) - try db.run(insertQuery) - } - - private func createTeamSetter(from team: Team) -> [Setter] { - let id = Expression("id") - let updateAt = Expression("update_at") - let displayName = Expression("display_name") - let name = Expression("name") - let description = Expression("description") - let type = Expression("type") - let allowedDomains = Expression("allowed_domains") - let isAllowOpenInvite = Expression("is_allow_open_invite") - let policyId = Expression("policy_id") - - var setter = [Setter]() - setter.append(id <- team.id) - setter.append(updateAt <- team.update_at) - setter.append(displayName <- team.display_name) - setter.append(name <- team.name) - setter.append(description <- team.description) - setter.append(type <- team.type) - setter.append(allowedDomains <- team.allowed_domains) - setter.append(isAllowOpenInvite <- team.allow_open_invite) - - // TODO: policyId is not yet in the Team table - // setter.append(policyId <- team.policy_id) - - return setter - } - - private func createMyTeamSetter(from teamMembership: TeamMembership) -> [Setter] { - let teamId = Expression("team_id") - let roles = Expression("roles") - - var setter = [Setter]() - setter.append(teamId <- teamMembership.team_id) - setter.append(roles <- teamMembership.roles) - - return setter - } -} diff --git a/ios/Gekidou/Sources/Gekidou/Storage/Database+Users.swift b/ios/Gekidou/Sources/Gekidou/Storage/Database+Users.swift new file mode 100644 index 0000000000..b16dc53733 --- /dev/null +++ b/ios/Gekidou/Sources/Gekidou/Storage/Database+Users.swift @@ -0,0 +1,177 @@ +// +// File.swift +// +// +// Created by Elias Nahum on 16-09-21. +// + +import Foundation +import SQLite + +public struct User: Codable, Hashable { + let id: String + let auth_service: String + let update_at: Int64 + let delete_at: Int64 + let email: String + let first_name: String + let is_bot: Bool + let is_guest: Bool + let last_name: String + let last_picture_update: Int64 + let locale: String + let nickname: String + let position: String + let roles: String + let status: String + let username: String + let notify_props: String + let props: String + let timezone: String + + public enum UserKeys: String, CodingKey { + case id = "id" + case auth_service = "auth_service" + case update_at = "update_at" + case delete_at = "delete_at" + case email = "email" + case first_name = "first_name" + case is_bot = "is_bot" + case last_name = "last_name" + case last_picture_update = "last_picture_update" + case locale = "locale" + case nickname = "nickname" + case position = "position" + case roles = "roles" + case username = "username" + case notify_props = "notify_props" + case props = "props" + case timezone = "timezone" + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: UserKeys.self) + id = try container.decode(String.self, forKey: .id) + auth_service = try container.decode(String.self, forKey: .auth_service) + update_at = try container.decode(Int64.self, forKey: .update_at) + delete_at = try container.decode(Int64.self, forKey: .delete_at) + email = try container.decode(String.self, forKey: .email) + first_name = try container.decode(String.self, forKey: .first_name) + is_bot = container.contains(.is_bot) ? try container.decode(Bool.self, forKey: .is_bot) : false + 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) + locale = try container.decode(String.self, forKey: .locale) + nickname = try container.decode(String.self, forKey: .nickname) + position = try container.decode(String.self, forKey: .position) + status = "offline" + username = try container.decode(String.self, forKey: .username) + + let notifyPropsData = try container.decodeIfPresent([String: String].self, forKey: .notify_props) + if (notifyPropsData != nil) { + notify_props = Database.default.json(from: notifyPropsData) ?? "{}" + } else { + notify_props = "{}" + } + + let propsData = try container.decodeIfPresent([String: String].self, forKey: .props) + if (propsData != nil) { + props = Database.default.json(from: propsData) ?? "{}" + } else { + props = "{}" + } + + let timezoneData = try container.decodeIfPresent([String: String].self, forKey: .timezone) + if (timezoneData != nil) { + timezone = Database.default.json(from: timezoneData) ?? "{}" + } else { + timezone = "{}" + } + } +} + +extension Database { + public func queryUsers(byIds: Set, withServerUrl: String) throws -> Set { + let db = try getDatabaseForServer(withServerUrl) + + var result: Set = Set() + let idCol = Expression("id") + for user in try db.prepare( + userTable.select(idCol).filter(byIds.contains(idCol)) + ) { + result.insert(user[idCol]) + } + + return result + } + + public func queryUsers(byUsernames: Set, withServerUrl: String) throws -> Set { + let db = try getDatabaseForServer(withServerUrl) + + var result: Set = Set() + let usernameCol = Expression("username") + for user in try db.prepare( + userTable.select(usernameCol).filter(byUsernames.contains(usernameCol)) + ) { + result.insert(user[usernameCol]) + } + + return result + } + + public func insertUsers(_ db: Connection, _ users: Set) throws { + let setters = createUserSettedrs(from: users) + let insertQuery = userTable.insertMany(or: .replace, setters) + try db.run(insertQuery) + } + + private func createUserSettedrs(from users: Set) -> [[Setter]] { + let id = Expression("id") + let authService = Expression("auth_service") + let updateAt = Expression("update_at") + let deleteAt = Expression("delete_at") + let email = Expression("email") + let firstName = Expression("first_name") + let isBot = Expression("is_bot") + let isGuest = Expression("is_guest") + let lastName = Expression("last_name") + let lastPictureUpdate = Expression("last_picture_update") + let locale = Expression("locale") + let nickname = Expression("nickname") + let position = Expression("position") + let roles = Expression("roles") + let status = Expression("status") + let username = Expression("username") + let notifyProps = Expression("notify_props") + let props = Expression("props") + let timezone = Expression("timezone") + + var setters = [[Setter]]() + for user in users { + var setter = [Setter]() + setter.append(id <- user.id) + setter.append(authService <- user.auth_service) + setter.append(updateAt <- user.update_at) + setter.append(deleteAt <- user.delete_at) + setter.append(email <- user.email) + setter.append(firstName <- user.first_name) + setter.append(isBot <- user.is_bot) + setter.append(isGuest <- user.is_guest) + setter.append(lastName <- user.last_name) + setter.append(lastPictureUpdate <- user.last_picture_update) + setter.append(locale <- user.locale) + setter.append(nickname <- user.nickname) + setter.append(position <- user.position) + setter.append(roles <- user.roles) + setter.append(status <- user.status) + setter.append(username <- user.username) + setter.append(notifyProps <- user.notify_props) + setter.append(props <- user.props) + setter.append(timezone <- user.timezone) + setters.append(setter) + } + + return setters + } +} diff --git a/ios/Gekidou/Sources/Gekidou/Storage/Database.swift b/ios/Gekidou/Sources/Gekidou/Storage/Database.swift index 5f3f2bee1a..d9c71a0672 100644 --- a/ios/Gekidou/Sources/Gekidou/Storage/Database.swift +++ b/ios/Gekidou/Sources/Gekidou/Storage/Database.swift @@ -57,6 +57,7 @@ public class Database: NSObject { internal var reactionTable = Table("Reaction") internal var fileTable = Table("File") internal var emojiTable = Table("CustomEmoji") + internal var userTable = Table("User") @objc public static let `default` = Database() @@ -77,10 +78,28 @@ public class Database: NSObject { } } + public func generateId() -> String { + let alphabet = "0123456789abcdefghijklmnopqrstuvwxyz" + let alphabetLenght = alphabet.count + let idLenght = 16 + var id = "" + + for _ in 1...(idLenght / 2) { + let random = floor(drand48() * Double(alphabetLenght) * Double(alphabetLenght)) + let firstIndex = Int(floor(random / Double(alphabetLenght))) + let lastIndex = Int(random) % alphabetLenght + id += String(alphabet[firstIndex]) + id += String(alphabet[lastIndex]) + } + + return id + } + public func getOnlyServerUrl() throws -> String { let db = try Connection(DEFAULT_DB_PATH) let url = Expression("url") - let query = serversTable.select(url) + let lastActiveAt = Expression("last_active_at") + let query = serversTable.select(url).filter(lastActiveAt > 0) var serverUrl: String? for result in try db.prepare(query) { @@ -126,7 +145,20 @@ public class Database: NSObject { throw DatabaseError.NoResults(query.asSQL()) } - private func json(from object:Any?) -> String? { + internal func queryCurrentUser(_ serverUrl: String) throws -> Row? { + let currentUserId = try queryCurrentUserId(serverUrl) + let idCol = Expression("id") + let query = userTable.where(idCol == currentUserId) + let db = try getDatabaseForServer(serverUrl) + + if let result = try db.pluck(query) { + return result + } + + throw DatabaseError.NoResults(query.asSQL()) + } + + internal func json(from object:Any?) -> String? { guard let object = object, let data = try? JSONSerialization.data(withJSONObject: object, options: []) else { return nil } diff --git a/ios/Mattermost.xcodeproj/project.pbxproj b/ios/Mattermost.xcodeproj/project.pbxproj index 60dd1c9cc8..2ba215847f 100644 --- a/ios/Mattermost.xcodeproj/project.pbxproj +++ b/ios/Mattermost.xcodeproj/project.pbxproj @@ -14,9 +14,9 @@ 13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; }; 2D5296A8926B4D7FBAF2D6E2 /* OpenSans-Light.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 6561AEAC21CC40B8A72ABB93 /* OpenSans-Light.ttf */; }; 4953BF602368AE8600593328 /* SwimeProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4953BF5F2368AE8600593328 /* SwimeProxy.swift */; }; - 49AE36FF26D4455800EF4E52 /* BuildFile in Frameworks */ = {isa = PBXBuildFile; productRef = 49AE36FE26D4455800EF4E52 /* SwiftPackageProductDependency */; }; - 49AE370126D4455D00EF4E52 /* BuildFile in Frameworks */ = {isa = PBXBuildFile; productRef = 49AE370026D4455D00EF4E52 /* SwiftPackageProductDependency */; }; - 49AE370526D5CD7800EF4E52 /* BuildFile in Frameworks */ = {isa = PBXBuildFile; productRef = 49AE370426D5CD7800EF4E52 /* SwiftPackageProductDependency */; }; + 49AE36FF26D4455800EF4E52 /* Gekidou in Frameworks */ = {isa = PBXBuildFile; productRef = 49AE36FE26D4455800EF4E52 /* Gekidou */; }; + 49AE370126D4455D00EF4E52 /* Gekidou in Frameworks */ = {isa = PBXBuildFile; productRef = 49AE370026D4455D00EF4E52 /* Gekidou */; }; + 49AE370526D5CD7800EF4E52 /* Gekidou in Frameworks */ = {isa = PBXBuildFile; productRef = 49AE370426D5CD7800EF4E52 /* Gekidou */; }; 49B4C050230C981C006E919E /* libUploadAttachments.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7FABE04522137F2A00D0F595 /* libUploadAttachments.a */; }; 531BEBC72513E93C00BC05B1 /* compass-icons.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 531BEBC52513E93C00BC05B1 /* compass-icons.ttf */; }; 536CC6C323E79287002C478C /* RNNotificationEventHandler+HandleReplyAction.m in Sources */ = {isa = PBXBuildFile; fileRef = 536CC6C123E79287002C478C /* RNNotificationEventHandler+HandleReplyAction.m */; }; @@ -168,7 +168,6 @@ 7F240ADA220E089300637665 /* Item.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Item.swift; sourceTree = ""; }; 7F240ADC220E094A00637665 /* TeamsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeamsViewController.swift; sourceTree = ""; }; 7F292A701E8AB73400A450A3 /* SplashScreenResource */ = {isa = PBXFileReference; lastKnownFileType = folder; path = SplashScreenResource; sourceTree = ""; }; - 7F292AA51E8ABB1100A450A3 /* splash.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = splash.png; path = SplashScreenResource/splash.png; sourceTree = ""; }; 7F325D6DAAF1047EB948EFF7 /* Pods-Mattermost-MattermostTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mattermost-MattermostTests.debug.xcconfig"; path = "Target Support Files/Pods-Mattermost-MattermostTests/Pods-Mattermost-MattermostTests.debug.xcconfig"; sourceTree = ""; }; 7F43D6051F6BF9EB001FC614 /* libPods-Mattermost.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = "libPods-Mattermost.a"; path = "../../../../../../../Library/Developer/Xcode/DerivedData/Mattermost-czlinsdviifujheezzjvmisotjrm/Build/Products/Debug-iphonesimulator/libPods-Mattermost.a"; sourceTree = ""; }; 7F54ABFAE6CE4A6DB11D1ED7 /* Roboto-BlackItalic.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "Roboto-BlackItalic.ttf"; path = "../assets/fonts/Roboto-BlackItalic.ttf"; sourceTree = ""; }; @@ -220,7 +219,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 49AE370526D5CD7800EF4E52 /* BuildFile in Frameworks */, + 49AE370526D5CD7800EF4E52 /* Gekidou in Frameworks */, 49B4C050230C981C006E919E /* libUploadAttachments.a in Frameworks */, 6C9B1EFD6561083917AF06CF /* libPods-Mattermost.a in Frameworks */, ); @@ -230,7 +229,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 49AE36FF26D4455800EF4E52 /* BuildFile in Frameworks */, + 49AE36FF26D4455800EF4E52 /* Gekidou in Frameworks */, 7FABE0562213884700D0F595 /* libUploadAttachments.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -239,7 +238,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 49AE370126D4455D00EF4E52 /* BuildFile in Frameworks */, + 49AE370126D4455D00EF4E52 /* Gekidou in Frameworks */, 7F581F78221EEA7C0099E66B /* libUploadAttachments.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -309,7 +308,6 @@ 7F151D43221B082A00FAD8F3 /* Mattermost-Bridging-Header.h */, 7FEB10991F61019C0039A015 /* MattermostManaged.h */, 7FEB109A1F61019C0039A015 /* MattermostManaged.m */, - 7F292AA51E8ABB1100A450A3 /* splash.png */, 7F0F4B0924BA173900E14C60 /* LaunchScreen.storyboard */, 7FCEFB9126B7934F006DC1DE /* SDWebImageDownloaderOperation+Swizzle.h */, 7FCEFB9226B7934F006DC1DE /* SDWebImageDownloaderOperation+Swizzle.m */, @@ -476,7 +474,7 @@ ); name = Mattermost; packageProductDependencies = ( - 49AE370426D5CD7800EF4E52 /* SwiftPackageProductDependency */, + 49AE370426D5CD7800EF4E52 /* Gekidou */, ); productName = "Hello World"; productReference = 13B07F961A680F5B00A75B9A /* Mattermost.app */; @@ -497,7 +495,7 @@ ); name = MattermostShare; packageProductDependencies = ( - 49AE36FE26D4455800EF4E52 /* SwiftPackageProductDependency */, + 49AE36FE26D4455800EF4E52 /* Gekidou */, ); productName = MattermostShare; productReference = 7F240A19220D3A2300637665 /* MattermostShare.appex */; @@ -518,7 +516,7 @@ ); name = NotificationService; packageProductDependencies = ( - 49AE370026D4455D00EF4E52 /* SwiftPackageProductDependency */, + 49AE370026D4455D00EF4E52 /* Gekidou */, ); productName = NotificationService; productReference = 7F581D32221ED5C60099E66B /* NotificationService.appex */; @@ -1250,15 +1248,15 @@ /* End XCConfigurationList section */ /* Begin XCSwiftPackageProductDependency section */ - 49AE36FE26D4455800EF4E52 /* SwiftPackageProductDependency */ = { + 49AE36FE26D4455800EF4E52 /* Gekidou */ = { isa = XCSwiftPackageProductDependency; productName = Gekidou; }; - 49AE370026D4455D00EF4E52 /* SwiftPackageProductDependency */ = { + 49AE370026D4455D00EF4E52 /* Gekidou */ = { isa = XCSwiftPackageProductDependency; productName = Gekidou; }; - 49AE370426D5CD7800EF4E52 /* SwiftPackageProductDependency */ = { + 49AE370426D5CD7800EF4E52 /* Gekidou */ = { isa = XCSwiftPackageProductDependency; productName = Gekidou; }; diff --git a/ios/Mattermost/AppDelegate.m b/ios/Mattermost/AppDelegate.m index 829b6b3ed9..819abd75e7 100644 --- a/ios/Mattermost/AppDelegate.m +++ b/ios/Mattermost/AppDelegate.m @@ -9,6 +9,7 @@ #import #import #import +#import #import #import @@ -28,6 +29,7 @@ NSString* const NOTIFICATION_MESSAGE_ACTION = @"message"; NSString* const NOTIFICATION_CLEAR_ACTION = @"clear"; NSString* const NOTIFICATION_UPDATE_BADGE_ACTION = @"update_badge"; +MattermostBucket* bucket = nil; -(void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)(void))completionHandler { os_log(OS_LOG_DEFAULT, "Mattermost will attach session from handleEventsForBackgroundURLSession!! identifier=%{public}@", identifier); @@ -48,6 +50,10 @@ NSString* const NOTIFICATION_UPDATE_BADGE_ACTION = @"update_badge"; { self.moduleRegistryAdapter = [[UMModuleRegistryAdapter alloc] initWithModuleRegistryProvider:[[UMModuleRegistryProvider alloc] init]]; + if (bucket == nil) { + bucket = [[MattermostBucket alloc] init]; + } + // Clear keychain on first run in case of reinstallation if (![[NSUserDefaults standardUserDefaults] objectForKey:@"FirstRun"]) { @@ -184,4 +190,21 @@ RNHWKeyboardEvent *hwKeyEvent = nil; NSString *selected = sender.input; [hwKeyEvent sendHWKeyEvent:@"shift-enter"]; } + +-(void)applicationDidBecomeActive:(UIApplication *)application { + [bucket setPreference:@"ApplicationIsForeground" value:@"true"]; +} + +-(void)applicationWillResignActive:(UIApplication *)application { + [bucket setPreference:@"ApplicationIsForeground" value:@"false"]; +} + +-(void)applicationDidEnterBackground:(UIApplication *)application { + [bucket setPreference:@"ApplicationIsForeground" value:@"false"]; +} + +-(void)applicationWillTerminate:(UIApplication *)application { + [bucket setPreference:@"ApplicationIsForeground" value:@"false"]; +} + @end diff --git a/ios/NotificationService/NotificationService.swift b/ios/NotificationService/NotificationService.swift index 562dc21f07..fa6189b9ac 100644 --- a/ios/NotificationService/NotificationService.swift +++ b/ios/NotificationService/NotificationService.swift @@ -73,8 +73,12 @@ class NotificationService: UNNotificationServiceExtension { } } } - - Network.default.fetchAndStoreDataForPushNotification(bestAttemptContent, withContentHandler: contentHandler) + + if (MattermostBucket.init().getPreference("ApplicationIsForeground") as? String != "true") { + Network.default.fetchAndStoreDataForPushNotification(bestAttemptContent, withContentHandler: contentHandler) + } else if let contentHandler = contentHandler { + contentHandler(bestAttemptContent) + } } override func serviceExtensionTimeWillExpire() { diff --git a/ios/UploadAttachments/UploadAttachments.xcodeproj/project.pbxproj b/ios/UploadAttachments/UploadAttachments.xcodeproj/project.pbxproj index feb40789e4..1e8f4dd1e4 100644 --- a/ios/UploadAttachments/UploadAttachments.xcodeproj/project.pbxproj +++ b/ios/UploadAttachments/UploadAttachments.xcodeproj/project.pbxproj @@ -19,6 +19,7 @@ 7FABE0F7221466F900D0F595 /* UploadManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FABE0F6221466F900D0F595 /* UploadManager.swift */; }; 7FABE0FA2214674200D0F595 /* StoreManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 7FABE0F82214674200D0F595 /* StoreManager.m */; }; 7FABE0FC2214800F00D0F595 /* UploadSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FABE0FB2214800F00D0F595 /* UploadSession.swift */; }; + 7FD4146126F3C663001A7F12 /* MattermostBucket.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 7FABE04D2213818A00D0F595 /* MattermostBucket.h */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -28,6 +29,7 @@ dstPath = "include/$(PRODUCT_NAME)"; dstSubfolderSpec = 16; files = ( + 7FD4146126F3C663001A7F12 /* MattermostBucket.h in CopyFiles */, 7F80232C229C91AD0034D6D4 /* MMMConstants.h in CopyFiles */, ); runOnlyForDeploymentPostprocessing = 0;