forked from Ivasoft/mattermost-mobile
iOS refactor push notifications to store data or send to JS for processing (#7128)
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import Foundation
|
||||
|
||||
extension ImageCache {
|
||||
func image(for userId: String, updatedAt: Double, onServer serverUrl: String) -> Data? {
|
||||
func image(for userId: String, updatedAt: Double, forServer serverUrl: String) -> Data? {
|
||||
lock.lock(); defer { lock.unlock() }
|
||||
let key = "\(serverUrl)-\(userId)-\(updatedAt)" as NSString
|
||||
if let image = imageCache.object(forKey: key) as? Data {
|
||||
|
||||
@@ -6,9 +6,9 @@ extension ImageCache {
|
||||
keysCache.removeAllObjects()
|
||||
}
|
||||
|
||||
func insertImage(_ data: Data?, for userId: String, updatedAt: Double, onServer serverUrl: String ) {
|
||||
func insertImage(_ data: Data?, for userId: String, updatedAt: Double, forServer serverUrl: String ) {
|
||||
guard let data = data else {
|
||||
return removeImage(for: userId, onServer: serverUrl)
|
||||
return removeImage(for: userId, forServer: serverUrl)
|
||||
}
|
||||
|
||||
lock.lock(); defer { lock.unlock() }
|
||||
@@ -18,7 +18,7 @@ extension ImageCache {
|
||||
keysCache.setObject(imageKey, forKey: cacheKey)
|
||||
}
|
||||
|
||||
func removeImage(for userId: String, onServer serverUrl: String) {
|
||||
func removeImage(for userId: String, forServer serverUrl: String) {
|
||||
lock.lock(); defer { lock.unlock() }
|
||||
let cacheKey = "\(serverUrl)-\(userId)" as NSString
|
||||
if let key = keysCache.object(forKey: cacheKey) {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import Foundation
|
||||
|
||||
protocol ImageCacheType: class {
|
||||
func image(for userId: String, updatedAt: Double, onServer serverUrl: String) -> Data?
|
||||
protocol ImageCacheType: AnyObject {
|
||||
func image(for userId: String, updatedAt: Double, forServer serverUrl: String) -> Data?
|
||||
|
||||
func insertImage(_ data: Data?, for userId: String, updatedAt: Double, onServer serverUrl: String )
|
||||
func insertImage(_ data: Data?, for userId: String, updatedAt: Double, forServer serverUrl: String )
|
||||
|
||||
func removeImage(for userId: String, onServer serverUrl: String)
|
||||
func removeImage(for userId: String, forServer serverUrl: String)
|
||||
|
||||
func removeAllImages()
|
||||
}
|
||||
|
||||
109
ios/Gekidou/Sources/Gekidou/DataTypes/Category.swift
Normal file
109
ios/Gekidou/Sources/Gekidou/DataTypes/Category.swift
Normal file
@@ -0,0 +1,109 @@
|
||||
import Foundation
|
||||
|
||||
public struct CategoriesWithOrder: Codable {
|
||||
let order: [String]
|
||||
let categories: [Category]
|
||||
|
||||
public enum CategoriesWithOrderKeys: String, CodingKey {
|
||||
case order, categories
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let values = try decoder.container(keyedBy: CategoriesWithOrderKeys.self)
|
||||
order = values.decodeIfPresent(forKey: .order, defaultValue: [String]())
|
||||
categories = (try? values.decode([Category].self, forKey: .categories)) ?? [Category]()
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CategoriesWithOrderKeys.self)
|
||||
try container.encode(self.order, forKey: .order)
|
||||
try container.encode(self.categories, forKey: .categories)
|
||||
}
|
||||
}
|
||||
|
||||
public struct Category: Codable {
|
||||
let id: String
|
||||
let channelIds: [String]
|
||||
let collapsed: Bool
|
||||
let displayName: String
|
||||
let muted: Bool
|
||||
let sortOrder: Int
|
||||
let sorting: String
|
||||
let teamId: String
|
||||
let type: String
|
||||
let userId: String
|
||||
|
||||
public enum CategoryKeys: String, CodingKey {
|
||||
case id, collapsed, muted, sorting, type
|
||||
case channelIds = "channel_ids"
|
||||
case displayName = "display_name"
|
||||
case sortOrder = "sort_order"
|
||||
case teamId = "team_id"
|
||||
case userId = "user_id"
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let values = try decoder.container(keyedBy: CategoryKeys.self)
|
||||
id = try values.decode(String.self, forKey: .id)
|
||||
teamId = try values.decode(String.self, forKey: .teamId)
|
||||
userId = try values.decode(String.self, forKey: .userId)
|
||||
channelIds = values.decodeIfPresent(forKey: .channelIds, defaultValue: [String]())
|
||||
collapsed = false
|
||||
displayName = values.decodeIfPresent(forKey: .displayName, defaultValue: "")
|
||||
muted = values.decodeIfPresent(forKey: .muted, defaultValue: false)
|
||||
sortOrder = values.decodeIfPresent(forKey: .sortOrder, defaultValue: 0)
|
||||
sorting = values.decodeIfPresent(forKey: .sorting, defaultValue: "recent")
|
||||
type = values.decodeIfPresent(forKey: .type, defaultValue: "custom")
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CategoryKeys.self)
|
||||
try container.encode(self.id, forKey: .id)
|
||||
try container.encode(self.channelIds, forKey: .channelIds)
|
||||
try container.encode(self.collapsed, forKey: .collapsed)
|
||||
try container.encode(self.displayName, forKey: .displayName)
|
||||
try container.encode(self.muted, forKey: .muted)
|
||||
try container.encode(self.sortOrder, forKey: .sortOrder)
|
||||
try container.encode(self.sorting, forKey: .sorting)
|
||||
try container.encode(self.teamId, forKey: .teamId)
|
||||
try container.encode(self.type, forKey: .type)
|
||||
try container.encode(self.userId, forKey: .userId)
|
||||
}
|
||||
}
|
||||
|
||||
public struct CategoryChannel: Codable {
|
||||
let id: String
|
||||
let categoryId: String
|
||||
let channelId: String
|
||||
let sortOrder: Int
|
||||
|
||||
public enum CategoryChannelKeys: String, CodingKey {
|
||||
case id
|
||||
case channelId = "channel_id"
|
||||
case categoryId = "category_id"
|
||||
case sortOrder = "sort_order"
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let values = try decoder.container(keyedBy: CategoryChannelKeys.self)
|
||||
id = try values.decode(String.self, forKey: .id)
|
||||
channelId = try values.decode(String.self, forKey: .channelId)
|
||||
categoryId = try values.decode(String.self, forKey: .categoryId)
|
||||
sortOrder = values.decodeIfPresent(forKey: .sortOrder, defaultValue: 0)
|
||||
}
|
||||
|
||||
public init(id: String, categoryId: String, channelId: String, sortOrder: Int = 0) {
|
||||
self.id = id
|
||||
self.categoryId = categoryId
|
||||
self.channelId = channelId
|
||||
self.sortOrder = sortOrder
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CategoryChannelKeys.self)
|
||||
try container.encode(self.id, forKey: .id)
|
||||
try container.encode(self.channelId, forKey: .channelId)
|
||||
try container.encode(self.categoryId, forKey: .categoryId)
|
||||
try container.encode(self.sortOrder, forKey: .sortOrder)
|
||||
}
|
||||
}
|
||||
100
ios/Gekidou/Sources/Gekidou/DataTypes/Channel.swift
Normal file
100
ios/Gekidou/Sources/Gekidou/DataTypes/Channel.swift
Normal file
@@ -0,0 +1,100 @@
|
||||
import Foundation
|
||||
|
||||
public struct Channel: Codable {
|
||||
let id: String
|
||||
let createAt: Double
|
||||
let creatorId: String
|
||||
let deleteAt: Double
|
||||
var displayName: String = ""
|
||||
let extraUpdateAt: Double
|
||||
let groupConstrained: Bool
|
||||
let header: String
|
||||
let lastPostAt: Double
|
||||
let lastRootPostAt: Double
|
||||
let name: String
|
||||
let policyId: String
|
||||
let props: String
|
||||
let purpose: String
|
||||
let schemeId: String
|
||||
let shared: Bool
|
||||
let teamId: String
|
||||
let totalMsgCount: Int
|
||||
let totalMsgCountRoot: Int
|
||||
let type: String
|
||||
let updateAt: Double
|
||||
|
||||
public enum ChannelKeys: String, CodingKey {
|
||||
case id
|
||||
case createAt = "create_at"
|
||||
case creatorId = "creator_id"
|
||||
case deleteAt = "delete_at"
|
||||
case displayName = "display_name"
|
||||
case extraUpdateAt = "extra_update_at"
|
||||
case groupConstrained = "group_constrained"
|
||||
case header
|
||||
case lastPostAt = "last_post_at"
|
||||
case lastRootPostAt = "last_root_post_at"
|
||||
case name
|
||||
case policyId = "policy_id"
|
||||
case props
|
||||
case purpose
|
||||
case schemeId = "scheme_id"
|
||||
case shared
|
||||
case teamId = "team_id"
|
||||
case totalMsgCount = "total_msg_count"
|
||||
case totalMsgCountRoot = "total_msg_count_root"
|
||||
case type
|
||||
case updateAt = "update_at"
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let values = try decoder.container(keyedBy: ChannelKeys.self)
|
||||
id = try values.decode(String.self, forKey: .id)
|
||||
creatorId = values.decodeIfPresent(forKey: .creatorId, defaultValue: "")
|
||||
createAt = values.decodeIfPresent(forKey: .createAt, defaultValue: 0)
|
||||
deleteAt = values.decodeIfPresent(forKey: .deleteAt, defaultValue: 0)
|
||||
displayName = values.decodeIfPresent(forKey: .displayName, defaultValue: "")
|
||||
extraUpdateAt = values.decodeIfPresent(forKey: .extraUpdateAt, defaultValue: 0)
|
||||
groupConstrained = values.decodeIfPresent(forKey: .groupConstrained, defaultValue: false)
|
||||
header = values.decodeIfPresent(forKey: .header, defaultValue: "")
|
||||
lastPostAt = values.decodeIfPresent(forKey: .lastPostAt, defaultValue: 0)
|
||||
lastRootPostAt = values.decodeIfPresent(forKey: .lastRootPostAt, defaultValue: 0)
|
||||
name = values.decodeIfPresent(forKey: .name, defaultValue: "")
|
||||
policyId = values.decodeIfPresent(forKey: .policyId, defaultValue: "")
|
||||
let propsData = try? values.decode([String:Any].self, forKey: .props)
|
||||
props = Database.default.json(from: propsData) ?? "{}"
|
||||
purpose = values.decodeIfPresent(forKey: .purpose, defaultValue: "")
|
||||
schemeId = values.decodeIfPresent(forKey: .schemeId, defaultValue: "")
|
||||
shared = values.decodeIfPresent(forKey: .shared, defaultValue: false)
|
||||
teamId = values.decodeIfPresent(forKey: .teamId, defaultValue: "")
|
||||
totalMsgCount = values.decodeIfPresent(forKey: .totalMsgCount, defaultValue: 0)
|
||||
totalMsgCountRoot = values.decodeIfPresent(forKey: .totalMsgCountRoot, defaultValue: 0)
|
||||
type = values.decodeIfPresent(forKey: .type, defaultValue: "O")
|
||||
updateAt = values.decodeIfPresent(forKey: .updateAt, defaultValue: 0)
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: ChannelKeys.self)
|
||||
try container.encode(self.id, forKey: .id)
|
||||
try container.encode(self.creatorId, forKey: .creatorId)
|
||||
try container.encode(self.createAt, forKey: .createAt)
|
||||
try container.encode(self.deleteAt, forKey: .deleteAt)
|
||||
try container.encode(self.displayName, forKey: .displayName)
|
||||
try container.encode(self.extraUpdateAt, forKey: .extraUpdateAt)
|
||||
try container.encode(self.groupConstrained, forKey: .groupConstrained)
|
||||
try container.encode(self.header, forKey: .header)
|
||||
try container.encode(self.lastPostAt, forKey: .lastPostAt)
|
||||
try container.encode(self.lastRootPostAt, forKey: .lastRootPostAt)
|
||||
try container.encode(self.name, forKey: .name)
|
||||
try container.encode(self.policyId, forKey: .policyId)
|
||||
try container.encode(self.props, forKey: .props)
|
||||
try container.encode(self.purpose, forKey: .purpose)
|
||||
try container.encode(self.schemeId, forKey: .schemeId)
|
||||
try container.encode(self.shared, forKey: .shared)
|
||||
try container.encode(self.teamId, forKey: .teamId)
|
||||
try container.encode(self.totalMsgCount, forKey: .totalMsgCount)
|
||||
try container.encode(self.totalMsgCountRoot, forKey: .totalMsgCountRoot)
|
||||
try container.encode(self.type, forKey: .type)
|
||||
try container.encode(self.updateAt, forKey: .updateAt)
|
||||
}
|
||||
}
|
||||
85
ios/Gekidou/Sources/Gekidou/DataTypes/ChannelMember.swift
Normal file
85
ios/Gekidou/Sources/Gekidou/DataTypes/ChannelMember.swift
Normal file
@@ -0,0 +1,85 @@
|
||||
import Foundation
|
||||
|
||||
public struct ChannelMember: Codable {
|
||||
let id: String
|
||||
let explicitRoles: String
|
||||
let lastUpdateAt: Double
|
||||
let lastViewedAt: Double
|
||||
let mentionCount: Int
|
||||
let mentionCountRoot: Int
|
||||
let msgCount: Int
|
||||
let msgCountRoot: Int
|
||||
let notifyProps: String
|
||||
let roles: String
|
||||
let schemeAdmin: Bool
|
||||
let schemeGuest: Bool
|
||||
let schemeUser: Bool
|
||||
let urgentMentionCount: Int
|
||||
let userId: String
|
||||
var internalMsgCount: Int
|
||||
var internalMsgCountRoot: Int
|
||||
|
||||
|
||||
public enum ChannelMemberKeys: String, CodingKey {
|
||||
case internalMsgCount, internalMsgCountRoot
|
||||
case id = "channel_id"
|
||||
case explicitRoles = "explicit_roles"
|
||||
case lastUpdateAt = "last_update_at"
|
||||
case lastViewedAt = "last_viewed_at"
|
||||
case mentionCount = "mention_count"
|
||||
case mentionCountRoot = "mention_count_root"
|
||||
case msgCount = "msg_count"
|
||||
case msgCountRoot = "msg_count_root"
|
||||
case notifyProps = "notify_props"
|
||||
case roles
|
||||
case schemeAdmin = "scheme_admin"
|
||||
case schemeGuest = "scheme_guest"
|
||||
case schemeUser = "scheme_user"
|
||||
case urgentMentionCount = "urgent_mention_count"
|
||||
case userId = "user_id"
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let values = try decoder.container(keyedBy: ChannelMemberKeys.self)
|
||||
id = try values.decode(String.self, forKey: .id)
|
||||
userId = try values.decode(String.self, forKey: .userId)
|
||||
explicitRoles = values.decodeIfPresent(forKey: .explicitRoles, defaultValue: "")
|
||||
lastUpdateAt = values.decodeIfPresent(forKey: .lastUpdateAt, defaultValue: 0)
|
||||
lastViewedAt = values.decodeIfPresent(forKey: .lastViewedAt, defaultValue: 0)
|
||||
mentionCount = values.decodeIfPresent(forKey: .mentionCount, defaultValue: 0)
|
||||
mentionCountRoot = values.decodeIfPresent(forKey: .mentionCountRoot, defaultValue: 0)
|
||||
msgCount = values.decodeIfPresent(forKey: .msgCount, defaultValue: 0)
|
||||
msgCountRoot = values.decodeIfPresent(forKey: .msgCountRoot, defaultValue: 0)
|
||||
let propsData = try values.decode([String:Any].self, forKey: .notifyProps)
|
||||
notifyProps = Database.default.json(from: propsData) ?? "{}"
|
||||
roles = values.decodeIfPresent(forKey: .roles, defaultValue: "")
|
||||
schemeAdmin = values.decodeIfPresent(forKey: .schemeAdmin, defaultValue: false)
|
||||
schemeGuest = values.decodeIfPresent(forKey: .schemeGuest, defaultValue: false)
|
||||
schemeUser = values.decodeIfPresent(forKey: .schemeUser, defaultValue: true)
|
||||
urgentMentionCount = values.decodeIfPresent(forKey: .urgentMentionCount, defaultValue: 0)
|
||||
internalMsgCount = 0
|
||||
internalMsgCountRoot = 0
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: ChannelMemberKeys.self)
|
||||
try container.encode(self.id, forKey: .id)
|
||||
try container.encode(self.explicitRoles, forKey: .explicitRoles)
|
||||
try container.encode(self.lastUpdateAt, forKey: .lastUpdateAt)
|
||||
try container.encode(self.lastViewedAt, forKey: .lastViewedAt)
|
||||
try container.encode(self.mentionCount, forKey: .mentionCount)
|
||||
try container.encode(self.mentionCountRoot, forKey: .mentionCountRoot)
|
||||
try container.encode(self.msgCount, forKey: .msgCount)
|
||||
try container.encode(self.msgCountRoot, forKey: .msgCountRoot)
|
||||
try container.encode(self.notifyProps, forKey: .notifyProps)
|
||||
try container.encode(self.roles, forKey: .roles)
|
||||
try container.encode(self.schemeAdmin, forKey: .schemeAdmin)
|
||||
try container.encode(self.schemeGuest, forKey: .schemeGuest)
|
||||
try container.encode(self.schemeUser, forKey: .schemeUser)
|
||||
try container.encode(self.urgentMentionCount, forKey: .urgentMentionCount)
|
||||
try container.encode(self.userId, forKey: .userId)
|
||||
try container.encode(self.internalMsgCount, forKey: .internalMsgCount)
|
||||
try container.encode(self.internalMsgCountRoot, forKey: .internalMsgCountRoot)
|
||||
}
|
||||
}
|
||||
|
||||
103
ios/Gekidou/Sources/Gekidou/DataTypes/Post.swift
Normal file
103
ios/Gekidou/Sources/Gekidou/DataTypes/Post.swift
Normal file
@@ -0,0 +1,103 @@
|
||||
import Foundation
|
||||
|
||||
public struct Post: Codable {
|
||||
let id: String
|
||||
let createAt: Double
|
||||
let updateAt: Double
|
||||
let editAt: Double
|
||||
let deleteAt: Double
|
||||
let isPinned: Bool
|
||||
let userId: String
|
||||
let channelId: String
|
||||
let rootId: String
|
||||
let originalId: String
|
||||
let message: String
|
||||
let type: String
|
||||
let props: String
|
||||
let pendingPostId: String
|
||||
let metadata: String
|
||||
var prevPostId: String
|
||||
// CRT
|
||||
let participants: [User]?
|
||||
let lastReplyAt: Double
|
||||
let replyCount: Int
|
||||
let isFollowing: Bool
|
||||
|
||||
public enum PostKeys: String, CodingKey {
|
||||
case id, message, type, props, metadata, participants
|
||||
case createAt = "create_at"
|
||||
case updateAt = "update_at"
|
||||
case deleteAt = "delete_at"
|
||||
case editAt = "edit_at"
|
||||
case isPinned = "is_pinned"
|
||||
case userId = "user_id"
|
||||
case channelId = "channel_id"
|
||||
case rootId = "root_id"
|
||||
case originalId = "original_id"
|
||||
case pendingPostId = "pending_post_id"
|
||||
case prevPostId = "previous_post_id"
|
||||
// CRT
|
||||
case lastReplyAt = "last_reply_at"
|
||||
case replyCount = "reply_count"
|
||||
case isFollowing = "is_following"
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let values = try decoder.container(keyedBy: PostKeys.self)
|
||||
prevPostId = ""
|
||||
id = try values.decode(String.self, forKey: .id)
|
||||
channelId = try values.decode(String.self, forKey: .channelId)
|
||||
userId = try values.decode(String.self, forKey: .userId)
|
||||
createAt = values.decodeIfPresent(forKey: .createAt, defaultValue: 0)
|
||||
updateAt = values.decodeIfPresent(forKey: .updateAt, defaultValue: 0)
|
||||
deleteAt = values.decodeIfPresent(forKey: .deleteAt, defaultValue: 0)
|
||||
editAt = values.decodeIfPresent(forKey: .editAt, defaultValue: 0)
|
||||
isPinned = values.decodeIfPresent(forKey: .isPinned, defaultValue: false)
|
||||
rootId = values.decodeIfPresent(forKey: .rootId, defaultValue: "")
|
||||
originalId = values.decodeIfPresent(forKey: .originalId, defaultValue: "")
|
||||
message = values.decodeIfPresent(forKey: .message, defaultValue: "")
|
||||
type = values.decodeIfPresent(forKey: .type, defaultValue: "")
|
||||
pendingPostId = values.decodeIfPresent(forKey: .pendingPostId, defaultValue: "")
|
||||
lastReplyAt = values.decodeIfPresent(forKey: .lastReplyAt, defaultValue: 0)
|
||||
replyCount = values.decodeIfPresent(forKey: .replyCount, defaultValue: 0)
|
||||
isFollowing = values.decodeIfPresent(forKey: .isFollowing, defaultValue: false)
|
||||
|
||||
participants = (try? values.decodeIfPresent([User].self, forKey: .participants)) ?? nil
|
||||
|
||||
if let meta = try? values.decode([String:Any].self, forKey: .metadata) {
|
||||
metadata = Database.default.json(from: meta) ?? "{}"
|
||||
} else {
|
||||
metadata = "{}"
|
||||
}
|
||||
|
||||
if let propsData = try? values.decode([String:Any].self, forKey: .props) {
|
||||
props = Database.default.json(from: propsData) ?? "{}"
|
||||
} else {
|
||||
props = "{}"
|
||||
}
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: PostKeys.self)
|
||||
try container.encode(self.id, forKey: .id)
|
||||
try container.encode(self.createAt, forKey: .createAt)
|
||||
try container.encode(self.updateAt, forKey: .updateAt)
|
||||
try container.encode(self.editAt, forKey: .editAt)
|
||||
try container.encode(self.deleteAt, forKey: .deleteAt)
|
||||
try container.encode(self.isPinned, forKey: .isPinned)
|
||||
try container.encode(self.userId, forKey: .userId)
|
||||
try container.encode(self.channelId, forKey: .channelId)
|
||||
try container.encode(self.rootId, forKey: .rootId)
|
||||
try container.encode(self.originalId, forKey: .originalId)
|
||||
try container.encode(self.message, forKey: .message)
|
||||
try container.encode(self.type, forKey: .type)
|
||||
try container.encode(self.props, forKey: .props)
|
||||
try container.encode(self.pendingPostId, forKey: .pendingPostId)
|
||||
try container.encode(self.metadata, forKey: .metadata)
|
||||
try container.encode(self.prevPostId, forKey: .prevPostId)
|
||||
try container.encodeIfPresent(self.participants, forKey: .participants)
|
||||
try container.encode(self.lastReplyAt, forKey: .lastReplyAt)
|
||||
try container.encode(self.replyCount, forKey: .replyCount)
|
||||
try container.encode(self.isFollowing, forKey: .isFollowing)
|
||||
}
|
||||
}
|
||||
30
ios/Gekidou/Sources/Gekidou/DataTypes/PostResponse.swift
Normal file
30
ios/Gekidou/Sources/Gekidou/DataTypes/PostResponse.swift
Normal file
@@ -0,0 +1,30 @@
|
||||
import Foundation
|
||||
|
||||
public struct PostResponse: Codable {
|
||||
let order: [String]
|
||||
let posts: [String:Post]
|
||||
let nextPostId: String
|
||||
let prevPostId: String
|
||||
|
||||
public enum PostResponseKeys: String, CodingKey {
|
||||
case order, posts
|
||||
case nextPostId = "next_post_id"
|
||||
case prevPostId = "prev_post_id"
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let values = try decoder.container(keyedBy: PostResponseKeys.self)
|
||||
order = values.decodeIfPresent(forKey: .order, defaultValue: [String]())
|
||||
nextPostId = values.decodeIfPresent(forKey: .nextPostId, defaultValue: "")
|
||||
prevPostId = values.decodeIfPresent(forKey: .prevPostId, defaultValue: "")
|
||||
posts = (try? values.decode([String:Post].self, forKey: .posts)) ?? [String:Post]()
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: PostResponseKeys.self)
|
||||
try container.encode(self.order, forKey: .order)
|
||||
try container.encode(self.posts, forKey: .posts)
|
||||
try container.encode(self.nextPostId, forKey: .nextPostId)
|
||||
try container.encode(self.prevPostId, forKey: .prevPostId)
|
||||
}
|
||||
}
|
||||
66
ios/Gekidou/Sources/Gekidou/DataTypes/PostThread.swift
Normal file
66
ios/Gekidou/Sources/Gekidou/DataTypes/PostThread.swift
Normal file
@@ -0,0 +1,66 @@
|
||||
import Foundation
|
||||
|
||||
public struct PostThread: Codable {
|
||||
let id: String
|
||||
var lastReplyAt: Double
|
||||
var lastViewedAt: Double
|
||||
let replyCount: Int
|
||||
var unreadReplies: Int
|
||||
var unreadMentions: Int
|
||||
let post: Post?
|
||||
let participants: [User]
|
||||
let isFollowing: Bool
|
||||
let deleteAt: Double
|
||||
|
||||
public enum PostThreadKeys: String, CodingKey {
|
||||
case id, post, participants
|
||||
case lastReplyAt = "last_reply_at"
|
||||
case lastViewedAt = "last_viewed_at"
|
||||
case replyCount = "reply_count"
|
||||
case unreadReplies = "unread_replies"
|
||||
case unreadMentions = "unread_mentions"
|
||||
case isFollowing = "is_following"
|
||||
case deleteAt = "delete_at"
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let values = try decoder.container(keyedBy: PostThreadKeys.self)
|
||||
id = try values.decode(String.self, forKey: .id)
|
||||
post = values.decodeIfPresent(forKey: .post, defaultValue: nil)
|
||||
participants = values.decodeIfPresent(forKey: .participants, defaultValue: [User]())
|
||||
lastReplyAt = values.decodeIfPresent(forKey: .lastReplyAt, defaultValue: 0)
|
||||
lastViewedAt = values.decodeIfPresent(forKey: .lastViewedAt, defaultValue: 0)
|
||||
replyCount = values.decodeIfPresent(forKey: .replyCount, defaultValue: 0)
|
||||
unreadReplies = values.decodeIfPresent(forKey: .unreadReplies, defaultValue: 0)
|
||||
unreadMentions = values.decodeIfPresent(forKey: .unreadMentions, defaultValue: 0)
|
||||
isFollowing = values.decodeIfPresent(forKey: .isFollowing, defaultValue: false)
|
||||
deleteAt = values.decodeIfPresent(forKey: .deleteAt, defaultValue: 0)
|
||||
}
|
||||
|
||||
public init(from post: Post) {
|
||||
id = post.id
|
||||
replyCount = post.replyCount
|
||||
participants = post.participants ?? [User]()
|
||||
isFollowing = post.isFollowing
|
||||
deleteAt = post.deleteAt
|
||||
lastReplyAt = 0
|
||||
lastViewedAt = 0
|
||||
unreadReplies = 0
|
||||
unreadMentions = 0
|
||||
self.post = post
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: PostThreadKeys.self)
|
||||
try container.encode(self.id, forKey: .id)
|
||||
try container.encode(self.lastReplyAt, forKey: .lastReplyAt)
|
||||
try container.encode(self.lastViewedAt, forKey: .lastViewedAt)
|
||||
try container.encode(self.replyCount, forKey: .replyCount)
|
||||
try container.encode(self.unreadReplies, forKey: .unreadReplies)
|
||||
try container.encode(self.unreadMentions, forKey: .unreadMentions)
|
||||
try container.encodeIfPresent(self.post, forKey: .post)
|
||||
try container.encode(self.participants, forKey: .participants)
|
||||
try container.encode(self.isFollowing, forKey: .isFollowing)
|
||||
try container.encode(self.deleteAt, forKey: .deleteAt)
|
||||
}
|
||||
}
|
||||
88
ios/Gekidou/Sources/Gekidou/DataTypes/Team.swift
Normal file
88
ios/Gekidou/Sources/Gekidou/DataTypes/Team.swift
Normal file
@@ -0,0 +1,88 @@
|
||||
import Foundation
|
||||
|
||||
public struct Team: Codable {
|
||||
let id: String
|
||||
let allowOpenInvite: Bool
|
||||
let allowedDomains: String
|
||||
let cloudLimitsArchived: Bool
|
||||
let companyName: String
|
||||
let createAt: Double
|
||||
let deleteAt: Double
|
||||
let description: String
|
||||
let displayName: String
|
||||
let email: String
|
||||
let groupConstrained: Bool
|
||||
let inviteId: String
|
||||
let lastTeamIconUpdate: Double
|
||||
let name: String
|
||||
let policyId: String
|
||||
let schemeId: String?
|
||||
let type: String
|
||||
let updateAt: Double
|
||||
|
||||
|
||||
public enum TeamKeys: String, CodingKey {
|
||||
case id
|
||||
case allowOpenInvite = "allow_open_invite"
|
||||
case allowedDomains = "allowed_domains"
|
||||
case cloudLimitsArchived = "cloud_limits_archive"
|
||||
case companyName = "company_name"
|
||||
case createAt = "create_at"
|
||||
case deleteAt = "delete_at"
|
||||
case description
|
||||
case displayName = "display_name"
|
||||
case email
|
||||
case groupConstrained = "group_constrained"
|
||||
case inviteId = "invite_id"
|
||||
case lastTeamIconUpdate = "last_team_icon_update"
|
||||
case name
|
||||
case policyId = "policy_id"
|
||||
case schemeId = "scheme_id"
|
||||
case type
|
||||
case updateAt = "update_at"
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let values = try decoder.container(keyedBy: TeamKeys.self)
|
||||
id = try values.decode(String.self, forKey: .id)
|
||||
allowOpenInvite = values.decodeIfPresent(forKey: .allowOpenInvite, defaultValue: true)
|
||||
allowedDomains = values.decodeIfPresent(forKey: .allowedDomains, defaultValue: "")
|
||||
cloudLimitsArchived = values.decodeIfPresent(forKey: .cloudLimitsArchived, defaultValue: false)
|
||||
companyName = values.decodeIfPresent(forKey: .companyName, defaultValue: "")
|
||||
createAt = values.decodeIfPresent(forKey: .createAt, defaultValue: 0)
|
||||
deleteAt = values.decodeIfPresent(forKey: .deleteAt, defaultValue: 0)
|
||||
description = values.decodeIfPresent(forKey: .description, defaultValue: "")
|
||||
displayName = values.decodeIfPresent(forKey: .displayName, defaultValue: "")
|
||||
email = values.decodeIfPresent(forKey: .email, defaultValue: "")
|
||||
groupConstrained = values.decodeIfPresent(forKey: .groupConstrained, defaultValue: false)
|
||||
inviteId = values.decodeIfPresent(forKey: .inviteId, defaultValue: "")
|
||||
lastTeamIconUpdate = values.decodeIfPresent(forKey: .lastTeamIconUpdate, defaultValue: 0)
|
||||
name = values.decodeIfPresent(forKey: .name, defaultValue: "")
|
||||
policyId = values.decodeIfPresent(forKey: .policyId, defaultValue: "")
|
||||
schemeId = values.decodeIfPresent(forKey: .schemeId, defaultValue: "")
|
||||
type = values.decodeIfPresent(forKey: .type, defaultValue: "O")
|
||||
updateAt = values.decodeIfPresent(forKey: .updateAt, defaultValue: 0)
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: TeamKeys.self)
|
||||
try container.encode(self.id, forKey: .id)
|
||||
try container.encode(self.allowOpenInvite, forKey: .allowOpenInvite)
|
||||
try container.encode(self.allowedDomains, forKey: .allowedDomains)
|
||||
try container.encode(self.cloudLimitsArchived, forKey: .cloudLimitsArchived)
|
||||
try container.encode(self.companyName, forKey: .companyName)
|
||||
try container.encode(self.createAt, forKey: .createAt)
|
||||
try container.encode(self.deleteAt, forKey: .deleteAt)
|
||||
try container.encode(self.description, forKey: .description)
|
||||
try container.encode(self.displayName, forKey: .displayName)
|
||||
try container.encode(self.email, forKey: .email)
|
||||
try container.encode(self.groupConstrained, forKey: .groupConstrained)
|
||||
try container.encode(self.inviteId, forKey: .inviteId)
|
||||
try container.encode(self.lastTeamIconUpdate, forKey: .lastTeamIconUpdate)
|
||||
try container.encode(self.name, forKey: .name)
|
||||
try container.encode(self.policyId, forKey: .policyId)
|
||||
try container.encodeIfPresent(self.schemeId, forKey: .schemeId)
|
||||
try container.encode(self.type, forKey: .type)
|
||||
try container.encode(self.updateAt, forKey: .updateAt)
|
||||
}
|
||||
}
|
||||
43
ios/Gekidou/Sources/Gekidou/DataTypes/TeamMember.swift
Normal file
43
ios/Gekidou/Sources/Gekidou/DataTypes/TeamMember.swift
Normal file
@@ -0,0 +1,43 @@
|
||||
import Foundation
|
||||
|
||||
public struct TeamMember: Codable {
|
||||
let id: String
|
||||
let explicitRoles: String
|
||||
let roles: String
|
||||
let schemeAdmin: Bool
|
||||
let schemeGuest: Bool
|
||||
let schemeUser: Bool
|
||||
let userId: String
|
||||
|
||||
public enum TeamMemberKeys: String, CodingKey {
|
||||
case id = "team_id"
|
||||
case explicitRoles = "explicit_roles"
|
||||
case roles
|
||||
case schemeAdmin = "scheme_admin"
|
||||
case schemeGuest = "scheme_guest"
|
||||
case schemeUser = "scheme_user"
|
||||
case userId = "user_id"
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let values = try decoder.container(keyedBy: TeamMemberKeys.self)
|
||||
id = try values.decode(String.self, forKey: .id)
|
||||
userId = try values.decode(String.self, forKey: .userId)
|
||||
explicitRoles = values.decodeIfPresent(forKey: .explicitRoles, defaultValue: "")
|
||||
roles = values.decodeIfPresent(forKey: .roles, defaultValue: "")
|
||||
schemeAdmin = values.decodeIfPresent(forKey: .schemeAdmin, defaultValue: false)
|
||||
schemeGuest = values.decodeIfPresent(forKey: .schemeGuest, defaultValue: false)
|
||||
schemeUser = values.decodeIfPresent(forKey: .schemeUser, defaultValue: true)
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: TeamMemberKeys.self)
|
||||
try container.encode(self.id, forKey: .id)
|
||||
try container.encode(self.explicitRoles, forKey: .explicitRoles)
|
||||
try container.encode(self.roles, forKey: .roles)
|
||||
try container.encode(self.schemeAdmin, forKey: .schemeAdmin)
|
||||
try container.encode(self.schemeGuest, forKey: .schemeGuest)
|
||||
try container.encode(self.schemeUser, forKey: .schemeUser)
|
||||
try container.encode(self.userId, forKey: .userId)
|
||||
}
|
||||
}
|
||||
98
ios/Gekidou/Sources/Gekidou/DataTypes/User.swift
Normal file
98
ios/Gekidou/Sources/Gekidou/DataTypes/User.swift
Normal file
@@ -0,0 +1,98 @@
|
||||
import Foundation
|
||||
|
||||
public struct User: Codable, Hashable {
|
||||
let id: String
|
||||
let authService: String
|
||||
let updateAt: Double
|
||||
let deleteAt: Double
|
||||
let email: String
|
||||
let firstName: String
|
||||
let isBot: Bool
|
||||
let isGuest: Bool
|
||||
let lastName: String
|
||||
let lastPictureUpdate: Double
|
||||
let locale: String
|
||||
let nickname: String
|
||||
let position: String
|
||||
let roles: String
|
||||
let status: String
|
||||
let username: String
|
||||
let notifyProps: String
|
||||
let props: String
|
||||
let timezone: String
|
||||
|
||||
public enum UserKeys: String, CodingKey {
|
||||
case id, email, locale, nickname, position, roles, username, props, timezone, status
|
||||
case authService = "auth_service"
|
||||
case updateAt = "update_at"
|
||||
case deleteAt = "delete_at"
|
||||
case firstName = "first_name"
|
||||
case isBot = "is_bot"
|
||||
case lastName = "last_name"
|
||||
case lastPictureUpdate = "last_picture_update"
|
||||
case notifyProps = "notify_props"
|
||||
case isGuest = "is_guest"
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let values = try decoder.container(keyedBy: UserKeys.self)
|
||||
id = try values.decode(String.self, forKey: .id)
|
||||
username = try values.decode(String.self, forKey: .username)
|
||||
authService = values.decodeIfPresent(forKey: .authService, defaultValue: "")
|
||||
updateAt = values.decodeIfPresent(forKey: .updateAt, defaultValue: 0)
|
||||
deleteAt = values.decodeIfPresent(forKey: .deleteAt, defaultValue: 0)
|
||||
email = values.decodeIfPresent(forKey: .email, defaultValue: "")
|
||||
firstName = values.decodeIfPresent(forKey: .firstName, defaultValue: "")
|
||||
isBot = values.decodeIfPresent(forKey: .isBot, defaultValue: false)
|
||||
roles = values.decodeIfPresent(forKey: .roles, defaultValue: "")
|
||||
lastName = values.decodeIfPresent(forKey: .lastName, defaultValue: "")
|
||||
lastPictureUpdate = values.decodeIfPresent(forKey: .lastPictureUpdate, defaultValue: 0)
|
||||
locale = values.decodeIfPresent(forKey: .locale, defaultValue: "en")
|
||||
nickname = values.decodeIfPresent(forKey: .nickname, defaultValue: "")
|
||||
position = values.decodeIfPresent(forKey: .position, defaultValue: "")
|
||||
|
||||
isGuest = roles.contains("system_guest")
|
||||
status = "offline"
|
||||
|
||||
if let notifyPropsData = try? values.decodeIfPresent([String: String].self, forKey: .notifyProps) {
|
||||
notifyProps = Database.default.json(from: notifyPropsData) ?? "{}"
|
||||
} else {
|
||||
notifyProps = "{}"
|
||||
}
|
||||
|
||||
if let propsData = try? values.decodeIfPresent([String: String].self, forKey: .props) {
|
||||
props = Database.default.json(from: propsData) ?? "{}"
|
||||
} else {
|
||||
props = "{}"
|
||||
}
|
||||
|
||||
if let timezoneData = try? values.decodeIfPresent([String: String].self, forKey: .timezone) {
|
||||
timezone = Database.default.json(from: timezoneData) ?? "{}"
|
||||
} else {
|
||||
timezone = "{}"
|
||||
}
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: UserKeys.self)
|
||||
try container.encode(self.id, forKey: .id)
|
||||
try container.encode(self.authService, forKey: .authService)
|
||||
try container.encode(self.updateAt, forKey: .updateAt)
|
||||
try container.encode(self.deleteAt, forKey: .deleteAt)
|
||||
try container.encode(self.email, forKey: .email)
|
||||
try container.encode(self.firstName, forKey: .firstName)
|
||||
try container.encode(self.isBot, forKey: .isBot)
|
||||
try container.encode(self.isGuest, forKey: .isGuest)
|
||||
try container.encode(self.lastName, forKey: .lastName)
|
||||
try container.encode(self.lastPictureUpdate, forKey: .lastPictureUpdate)
|
||||
try container.encode(self.locale, forKey: .locale)
|
||||
try container.encode(self.nickname, forKey: .nickname)
|
||||
try container.encode(self.position, forKey: .position)
|
||||
try container.encode(self.roles, forKey: .roles)
|
||||
try container.encode(self.status, forKey: .status)
|
||||
try container.encode(self.username, forKey: .username)
|
||||
try container.encode(self.notifyProps, forKey: .notifyProps)
|
||||
try container.encode(self.props, forKey: .props)
|
||||
try container.encode(self.timezone, forKey: .timezone)
|
||||
}
|
||||
}
|
||||
@@ -16,9 +16,3 @@ extension Date {
|
||||
self = Date(timeIntervalSince1970: TimeInterval(milliseconds) / 1000)
|
||||
}
|
||||
}
|
||||
|
||||
extension StringProtocol {
|
||||
public subscript(offset: Int) -> Character {
|
||||
self[index(startIndex, offsetBy: offset)]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import Foundation
|
||||
|
||||
public typealias CategoriesHandler = (_ categoriesWithOrder: CategoriesWithOrder?) -> Void
|
||||
|
||||
extension Network {
|
||||
public func fetchCategories(withTeamId teamId: String, forServerUrl serverUrl: String, completionHandler: @escaping CategoriesHandler) {
|
||||
var categoriesWithOrder: CategoriesWithOrder?
|
||||
let channelUrl = buildApiUrl(serverUrl, "/users/me/teams/\(teamId)/channels/categories")
|
||||
request(channelUrl, usingMethod: "GET", forServerUrl: serverUrl) { data, response, error in
|
||||
if let data = data {
|
||||
categoriesWithOrder = try? JSONDecoder().decode(CategoriesWithOrder.self, from: data)
|
||||
}
|
||||
completionHandler(categoriesWithOrder)
|
||||
}
|
||||
}
|
||||
}
|
||||
92
ios/Gekidou/Sources/Gekidou/Networking/Network+Channel.swift
Normal file
92
ios/Gekidou/Sources/Gekidou/Networking/Network+Channel.swift
Normal file
@@ -0,0 +1,92 @@
|
||||
import Foundation
|
||||
|
||||
extension Network {
|
||||
public func fetchMyChannel(withId channelId: String, forServerUrl serverUrl: String, completionHandler: @escaping ((_ channel: Channel?, _ myChannel: ChannelMember?, _ profiles: [User]?) -> Void)) {
|
||||
let group = DispatchGroup()
|
||||
var channel: Channel? = nil
|
||||
var tempChannel: Channel? = nil
|
||||
var myChannel: ChannelMember? = nil
|
||||
var profiles: [User]? = nil
|
||||
|
||||
group.enter()
|
||||
let channelUrl = buildApiUrl(serverUrl, "/channels/\(channelId)")
|
||||
request(channelUrl, usingMethod: "GET", forServerUrl: serverUrl) { data, response, error in
|
||||
if let data = data {
|
||||
tempChannel = try? JSONDecoder().decode(Channel.self, from: data)
|
||||
}
|
||||
group.leave()
|
||||
}
|
||||
|
||||
group.enter()
|
||||
let myChannelUrl = buildApiUrl(serverUrl, "/channels/\(channelId)/members/me")
|
||||
request(myChannelUrl, usingMethod: "GET", forServerUrl: serverUrl) { data, response, error in
|
||||
if let data = data {
|
||||
myChannel = try? JSONDecoder().decode(ChannelMember.self, from: data)
|
||||
}
|
||||
group.leave()
|
||||
}
|
||||
|
||||
group.notify(queue: .main) {
|
||||
if let tempChannel = tempChannel,
|
||||
(tempChannel.type == "D" || tempChannel.type == "G")
|
||||
&& !Database.default.queryChannelExists(withId: channelId, forServerUrl: serverUrl) {
|
||||
let displayNameSetting = Database.default.getTeammateDisplayNameSetting(serverUrl)
|
||||
Network.default.fetchProfiles(inChannelId: channelId, forServerUrl: serverUrl) {[weak self] data, response, error in
|
||||
if let data = data,
|
||||
let currentUserId = try? Database.default.queryCurrentUserId(serverUrl),
|
||||
let users = try? JSONDecoder().decode([User].self, from: data) {
|
||||
if !users.isEmpty {
|
||||
profiles = users.filter{ $0.id != currentUserId}
|
||||
if tempChannel.type == "D",
|
||||
let profiles = profiles,
|
||||
let user = profiles.first,
|
||||
let displayName = self?.displayUsername(user, displayNameSetting) {
|
||||
var chan = tempChannel
|
||||
chan.displayName = displayName
|
||||
channel = chan
|
||||
} else if let profiles = profiles {
|
||||
let locale = Database.default.getCurrentUserLocale(serverUrl)
|
||||
if let displayName = self?.displayGroupMessageName(profiles, locale: locale, displayNameSetting: displayNameSetting) {
|
||||
var chan = tempChannel
|
||||
chan.displayName = displayName
|
||||
channel = chan
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
completionHandler(channel, myChannel, profiles)
|
||||
}
|
||||
} else {
|
||||
channel = tempChannel
|
||||
completionHandler(channel, myChannel, profiles)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func displayUsername(_ user: User, _ displayNameSetting: String) -> String {
|
||||
switch (displayNameSetting) {
|
||||
case "nickname_full_name":
|
||||
if !user.nickname.isEmpty {
|
||||
return user.nickname
|
||||
}
|
||||
return "\(user.firstName) \(user.lastName)".trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
case "full_name":
|
||||
return "\(user.firstName) \(user.lastName)".trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
default:
|
||||
return user.username
|
||||
}
|
||||
}
|
||||
|
||||
private func displayGroupMessageName(_ users: [User], locale: String, displayNameSetting: String) -> String {
|
||||
var names = [String]()
|
||||
for user in users {
|
||||
names.append(displayUsername(user, displayNameSetting))
|
||||
}
|
||||
|
||||
let sorted = names.sorted { (lhs: String, rhs: String) -> Bool in
|
||||
return lhs.compare(rhs, options: [.caseInsensitive], locale: Locale(identifier: locale)) == .orderedAscending
|
||||
}
|
||||
|
||||
return sorted.joined(separator: ", ").trimmingCharacters(in: .whitespaces)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import Foundation
|
||||
|
||||
extension Network: URLSessionDelegate, URLSessionTaskDelegate {
|
||||
public func urlSession(_ session: URLSession,
|
||||
task: URLSessionTask,
|
||||
didReceive challenge: URLAuthenticationChallenge,
|
||||
completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
|
||||
var credential: URLCredential? = nil
|
||||
var disposition: URLSession.AuthChallengeDisposition = .performDefaultHandling
|
||||
|
||||
let authMethod = challenge.protectionSpace.authenticationMethod
|
||||
if authMethod == NSURLAuthenticationMethodClientCertificate {
|
||||
let host = task.currentRequest!.url!.host!
|
||||
if let (identity, certificate) = try? Keychain.default.getClientIdentityAndCertificate(for: host) {
|
||||
credential = URLCredential(identity: identity,
|
||||
certificates: [certificate],
|
||||
persistence: URLCredential.Persistence.permanent)
|
||||
}
|
||||
disposition = .useCredential
|
||||
}
|
||||
|
||||
completionHandler(disposition, credential)
|
||||
}
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
public struct ChannelMemberData: Codable {
|
||||
let channel_id: String
|
||||
let mention_count: Int
|
||||
let mention_count_root: Int
|
||||
let user_id: String
|
||||
let roles: String
|
||||
let last_viewed_at: Int64
|
||||
let last_update_at: Int64
|
||||
|
||||
public enum ChannelMemberKeys: String, CodingKey {
|
||||
case channel_id, mention_count, mention_count_root, user_id, roles, last_viewed_at, last_update_at
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: ChannelMemberKeys.self)
|
||||
channel_id = try container.decode(String.self, forKey: .channel_id)
|
||||
mention_count = try container.decode(Int.self, forKey: .mention_count)
|
||||
let mentions_root = try? container.decode(Int?.self, forKey: .mention_count_root)
|
||||
mention_count_root = mentions_root ?? 0
|
||||
user_id = try container.decode(String.self, forKey: .user_id)
|
||||
roles = try container.decode(String.self, forKey: .roles)
|
||||
last_update_at = try container.decode(Int64.self, forKey: .last_update_at)
|
||||
last_viewed_at = try container.decode(Int64.self, forKey: .last_viewed_at)
|
||||
}
|
||||
}
|
||||
|
||||
public struct ThreadData: Codable {
|
||||
let id: String
|
||||
let last_reply_at: Int64
|
||||
let last_viewed_at: Int64
|
||||
let reply_count: Int
|
||||
let unread_replies: Int
|
||||
let unread_mentions: Int
|
||||
|
||||
public enum ThreadKeys: String, CodingKey {
|
||||
case id, last_reply_at, last_viewed_at, reply_count, unread_replies, unread_mentions
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: ThreadKeys.self)
|
||||
id = try container.decode(String.self, forKey: .id)
|
||||
last_viewed_at = try container.decode(Int64.self, forKey: .last_viewed_at)
|
||||
last_reply_at = try container.decode(Int64.self, forKey: .last_reply_at)
|
||||
reply_count = try container.decode(Int.self, forKey: .reply_count)
|
||||
unread_replies = try container.decode(Int.self, forKey: .unread_replies)
|
||||
unread_mentions = try container.decode(Int.self, forKey: .unread_mentions)
|
||||
}
|
||||
}
|
||||
|
||||
extension Network {
|
||||
public func fetchChannelMentions(channelId: String, withServerUrl serverUrl: String, completionHandler: @escaping ResponseHandler) {
|
||||
let endpoint = "/channels/\(channelId)/members/me"
|
||||
let url = buildApiUrl(serverUrl, endpoint)
|
||||
return request(url, withMethod: "GET", withServerUrl: serverUrl, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
public func fetchThreadMentions(teamId: String, threadId: String, withServerUrl serverUrl: String, completionHandler: @escaping ResponseHandler) {
|
||||
let endpoint = "/users/me/teams/\(teamId)/threads/\(threadId)"
|
||||
let url = buildApiUrl(serverUrl, endpoint)
|
||||
return request(url, withMethod: "GET", withServerUrl: serverUrl, completionHandler: completionHandler)
|
||||
}
|
||||
}
|
||||
@@ -7,49 +7,9 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
let POST_CHUNK_SIZE = 60
|
||||
|
||||
public struct PostData: Codable {
|
||||
let order: [String]
|
||||
let posts: [Post]
|
||||
let next_post_id: String
|
||||
let prev_post_id: String
|
||||
|
||||
public enum PostDataKeys: String, CodingKey {
|
||||
case order, posts, next_post_id, prev_post_id
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: PostDataKeys.self)
|
||||
order = try container.decode([String].self, forKey: .order)
|
||||
next_post_id = try container.decode(String.self, forKey: .next_post_id)
|
||||
prev_post_id = try container.decode(String.self, forKey: .prev_post_id)
|
||||
|
||||
let decodedPosts = try container.decode([String:Post].self, forKey: .posts)
|
||||
posts = Array(decodedPosts.values)
|
||||
}
|
||||
}
|
||||
public typealias PostsHandler = (_ postResponse: PostResponse?, _ threads: [PostThread]?, _ users: [User]?) -> Void
|
||||
|
||||
extension Network {
|
||||
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)
|
||||
}
|
||||
|
||||
public func createPost(serverUrl: String, channelId: String, message: String, fileIds: [String], completionHandler: @escaping ResponseHandler) {
|
||||
do {
|
||||
if !message.isEmpty || !fileIds.isEmpty {
|
||||
@@ -66,8 +26,8 @@ extension Network {
|
||||
url,
|
||||
withMethod: "POST",
|
||||
withBody: data,
|
||||
withHeaders: headers,
|
||||
withServerUrl: serverUrl,
|
||||
andHeaders: headers,
|
||||
forServerUrl: serverUrl,
|
||||
completionHandler: completionHandler
|
||||
)
|
||||
}
|
||||
@@ -75,4 +35,131 @@ extension Network {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public func fetchPosts(forChannelId channelId: String, andRootId rootId: String, havingCRTEnabled isCRTEnabled: Bool, withAlreadyLoadedProfiles loadedProfiles: [User], forServerUrl serverUrl: String, completionHandler: @escaping PostsHandler) {
|
||||
let additionalParams = isCRTEnabled ? "&collapsedThreads=true&collapsedThreadsExtended=true" : ""
|
||||
let receivingThreads = isCRTEnabled && !rootId.isEmpty
|
||||
let endpoint: String
|
||||
|
||||
let alreadyLoadedUserIds = loadedProfiles.map { $0.id }
|
||||
var postResponse: PostResponse? = nil
|
||||
|
||||
if receivingThreads {
|
||||
let since = rootId.isEmpty ? nil : Database.default.queryLastPostInThread(withRootId: rootId, forServerUrl: serverUrl)
|
||||
let queryParams = since == nil ? "?perPage=60&fromCreateAt=0&direction=up" : "?fromCreateAt=\(Int64(since!))&direction=down"
|
||||
endpoint = "/posts/\(rootId)/thread\(queryParams)\(additionalParams)"
|
||||
} else {
|
||||
let since = Database.default.queryPostsSinceForChannel(withId: channelId, forServerUrl: serverUrl)
|
||||
let queryParams = since == nil ? "?page=0&per_page=60" : "?since=\(Int64(since!))"
|
||||
endpoint = "/channels/\(channelId)/posts\(queryParams)\(additionalParams)"
|
||||
}
|
||||
|
||||
let url = buildApiUrl(serverUrl, endpoint)
|
||||
request(url, usingMethod: "GET", forServerUrl: serverUrl) {data, response, error in
|
||||
if let data = data {
|
||||
postResponse = try? JSONDecoder().decode(PostResponse.self, from: data)
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.processPostsFetched(postResponse, andAlreadyLoadedProfilesIds: alreadyLoadedUserIds,
|
||||
usingCRT: isCRTEnabled, forServerUrl: serverUrl,
|
||||
completionHandler: completionHandler)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func processPostsFetched(_ postResponse: PostResponse?, andAlreadyLoadedProfilesIds alreadyLoadedUserIds: [String],
|
||||
usingCRT isCRTEnabled: Bool, forServerUrl serverUrl: String, completionHandler: @escaping PostsHandler) {
|
||||
guard let currentUserRow = try? Database.default.queryCurrentUser(serverUrl),
|
||||
let currentUser = Database.default.getUserFromRow(currentUserRow)
|
||||
else {
|
||||
completionHandler(nil, nil, nil)
|
||||
return
|
||||
}
|
||||
|
||||
var users: [User]? = nil
|
||||
var threads: [PostThread]? = nil
|
||||
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
|
||||
var userIdsToLoad: Set<String> = Set()
|
||||
var usernamesToLoad: Set<String> = Set()
|
||||
|
||||
if let postsWithKeys = postResponse?.posts {
|
||||
let posts = Array(postsWithKeys.values)
|
||||
for post in posts {
|
||||
let authorId = post.userId
|
||||
let message = post.message
|
||||
|
||||
if isCRTEnabled && post.rootId.isEmpty {
|
||||
if threads == nil {
|
||||
threads = [PostThread]()
|
||||
}
|
||||
threads?.append(PostThread(from: post))
|
||||
}
|
||||
|
||||
if let participants = post.participants {
|
||||
for participant in participants {
|
||||
let userId = participant.id
|
||||
let username = participant.username
|
||||
if userId != currentUser.id && !alreadyLoadedUserIds.contains(userId) && !threadParticipantUserIds.contains(userId) {
|
||||
threadParticipantUserIds.insert(userId)
|
||||
if threadParticipantUsers[userId] == nil {
|
||||
threadParticipantUsers[userId] = participant
|
||||
}
|
||||
}
|
||||
|
||||
if !username.isEmpty && username != currentUser.username && !threadParticipantUsernames.contains(username) {
|
||||
threadParticipantUsernames.insert(username)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (authorId != currentUser.id && !alreadyLoadedUserIds.contains(authorId) && !threadParticipantUserIds.contains(authorId) && !userIdsToLoad.contains(authorId)) {
|
||||
userIdsToLoad.insert(authorId)
|
||||
}
|
||||
|
||||
if !message.isEmpty {
|
||||
for username in self.matchUsername(in: message) {
|
||||
if username != currentUser.username && !threadParticipantUsernames.contains(username) && !usernamesToLoad.contains(username) {
|
||||
usernamesToLoad.insert(username)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !threadParticipantUsers.isEmpty || !usernamesToLoad.isEmpty || !userIdsToLoad.isEmpty {
|
||||
users = [User]()
|
||||
}
|
||||
|
||||
if !usernamesToLoad.isEmpty || !userIdsToLoad.isEmpty,
|
||||
let profiles = Network.default.fetchNeededUsers(userIds: userIdsToLoad, usernames: usernamesToLoad, forServerUrl: serverUrl),
|
||||
!profiles.isEmpty {
|
||||
users?.append(contentsOf: profiles)
|
||||
}
|
||||
|
||||
if !threadParticipantUsers.isEmpty {
|
||||
let storedParticipantsById = Database.default.queryUsers(byIds: threadParticipantUserIds, forServerUrl: serverUrl)
|
||||
let storedParticipantsByUsername = Database.default.queryUsers(byUsernames: threadParticipantUsernames, forServerUrl: serverUrl)
|
||||
let participantUsers = Array(threadParticipantUsers.values).filter{ !storedParticipantsById.contains($0.id) && !storedParticipantsByUsername.contains($0.username) }
|
||||
if !participantUsers.isEmpty {
|
||||
users?.append(contentsOf: participantUsers)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
completionHandler(postResponse, threads, users)
|
||||
}
|
||||
|
||||
private func matchUsername(in message: String) -> [String] {
|
||||
let specialMentions = Set(["all", "here", "channel"])
|
||||
if 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))
|
||||
if !results.isEmpty {
|
||||
let username = results.map({ String(message[Range($0.range, in: message)!]).removePrefix("@") }).filter({ !specialMentions.contains($0)})
|
||||
return username
|
||||
}
|
||||
}
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
36
ios/Gekidou/Sources/Gekidou/Networking/Network+Team.swift
Normal file
36
ios/Gekidou/Sources/Gekidou/Networking/Network+Team.swift
Normal file
@@ -0,0 +1,36 @@
|
||||
import Foundation
|
||||
|
||||
extension Network {
|
||||
public func fetchTeamIfNeeded(withId teamId: String, forServerUrl serverUrl: String, completionHandler: @escaping ((_ team: Team?, _ myTeam: TeamMember?) -> Void)) {
|
||||
let group = DispatchGroup()
|
||||
var team: Team? = nil
|
||||
var myTeam: TeamMember? = nil
|
||||
|
||||
if !Database.default.queryTeamExists(withId: teamId, forServerUrl: serverUrl) {
|
||||
group.enter()
|
||||
|
||||
let url = buildApiUrl(serverUrl, "/teams/\(teamId)")
|
||||
request(url, usingMethod: "GET", forServerUrl: serverUrl) { data, response, error in
|
||||
if let data = data {
|
||||
team = try? JSONDecoder().decode(Team.self, from: data)
|
||||
}
|
||||
group.leave()
|
||||
}
|
||||
}
|
||||
|
||||
if !Database.default.queryMyTeamExists(withId: teamId, forServerUrl: serverUrl) {
|
||||
group.enter()
|
||||
let url = buildApiUrl(serverUrl, "/teams/\(teamId)/members/me")
|
||||
request(url, usingMethod: "GET", forServerUrl: serverUrl) { data, response, error in
|
||||
if let data = data {
|
||||
myTeam = try? JSONDecoder().decode(TeamMember.self, from: data)
|
||||
}
|
||||
group.leave()
|
||||
}
|
||||
}
|
||||
|
||||
group.notify(queue: .main) {
|
||||
completionHandler(team, myTeam)
|
||||
}
|
||||
}
|
||||
}
|
||||
22
ios/Gekidou/Sources/Gekidou/Networking/Network+Thread.swift
Normal file
22
ios/Gekidou/Sources/Gekidou/Networking/Network+Thread.swift
Normal file
@@ -0,0 +1,22 @@
|
||||
import Foundation
|
||||
|
||||
public typealias ThreadResponse = (_ threads: PostThread?) -> Void
|
||||
|
||||
extension Network {
|
||||
public func fetchThread(byId threadId: String, belongingToTeamId teamId: String, forServerUrl serverUrl: String, completionHandler: @escaping ThreadResponse) {
|
||||
var thread: PostThread? = nil
|
||||
guard let currentUserId = try? Database.default.queryCurrentUserId(serverUrl),
|
||||
let threadTeamId = teamId.isEmpty ? Database.default.queryCurrentTeamId(serverUrl) : teamId
|
||||
else {
|
||||
completionHandler(nil)
|
||||
return
|
||||
}
|
||||
let url = buildApiUrl(serverUrl, "/users/\(currentUserId)/teams/\(threadTeamId)/threads/\(threadId)")
|
||||
request(url, usingMethod: "GET", forServerUrl: serverUrl) {data, response, error in
|
||||
if let data = data {
|
||||
thread = try? JSONDecoder().decode(PostThread.self, from: data)
|
||||
}
|
||||
completionHandler(thread)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,26 +8,71 @@
|
||||
import Foundation
|
||||
|
||||
extension Network {
|
||||
public func fetchUsers(byIds userIds: [String], withServerUrl serverUrl: String, completionHandler: @escaping ResponseHandler) {
|
||||
public func fetchNeededUsers(userIds: Set<String>, usernames: Set<String>, forServerUrl serverUrl: String) -> [User]? {
|
||||
let group = DispatchGroup()
|
||||
var users: [User]? = nil
|
||||
if !userIds.isEmpty || !usernames.isEmpty {
|
||||
// remove existing users in the database
|
||||
users = [User]()
|
||||
let storedUserIds = Database.default.queryUsers(byIds: userIds, forServerUrl: serverUrl)
|
||||
if !(userIds.filter{ !storedUserIds.contains($0) }).isEmpty {
|
||||
group.enter()
|
||||
DispatchQueue.global(qos: .default).async {
|
||||
self.fetchUsers(byIds: Array(userIds), forServerUrl: serverUrl) {data, response, error in
|
||||
if let data = data,
|
||||
let profiles = try? JSONDecoder().decode([User].self, from: data) {
|
||||
users?.append(contentsOf: profiles)
|
||||
}
|
||||
group.leave()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let storedUsernames = Database.default.queryUsers(byUsernames: usernames, forServerUrl: serverUrl)
|
||||
if !(usernames.filter{ !storedUsernames.contains($0) }).isEmpty {
|
||||
group.enter()
|
||||
DispatchQueue.global(qos: .default).async {
|
||||
self.fetchUsers(byUsernames: Array(usernames), forServerUrl: serverUrl) {data, response, error in
|
||||
if let data = data,
|
||||
let profiles = try? JSONDecoder().decode([User].self, from: data) {
|
||||
users?.append(contentsOf: profiles)
|
||||
}
|
||||
group.leave()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
group.wait()
|
||||
return users
|
||||
}
|
||||
|
||||
public func fetchUsers(byIds userIds: [String], forServerUrl 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)
|
||||
return request(url, withMethod: "POST", withBody: data, andHeaders: nil, forServerUrl: serverUrl, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
public func fetchUsers(byUsernames usernames: [String], withServerUrl serverUrl: String, completionHandler: @escaping ResponseHandler) {
|
||||
public func fetchUsers(byUsernames usernames: [String], forServerUrl 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)
|
||||
return request(url, withMethod: "POST", withBody: data, andHeaders: nil, forServerUrl: serverUrl, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
public func fetchUserProfilePicture(userId: String, lastUpdateAt: Double, withServerUrl serverUrl: String, completionHandler: @escaping ResponseHandler) {
|
||||
public func fetchProfiles(inChannelId channelId: String, forServerUrl serverUrl: String, completionHandler: @escaping ResponseHandler) {
|
||||
let endpoint = "/users?in_channel=\(channelId)&page=0&per_page=8&sort="
|
||||
let url = buildApiUrl(serverUrl, endpoint)
|
||||
request(url, usingMethod: "GET", forServerUrl: serverUrl, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
public func fetchUserProfilePicture(userId: String, lastUpdateAt: Double, forServerUrl serverUrl: String, completionHandler: @escaping ResponseHandler) {
|
||||
let endpoint = "/users/\(userId)/image?lastPictureUpdate=\(lastUpdateAt)"
|
||||
let url = buildApiUrl(serverUrl, endpoint)
|
||||
|
||||
return request(url, withMethod: "GET", withServerUrl: serverUrl, completionHandler: completionHandler)
|
||||
return request(url, usingMethod: "GET", forServerUrl: serverUrl, completionHandler: completionHandler)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ public class Network: NSObject {
|
||||
return (response as? HTTPURLResponse)?.statusCode == 200
|
||||
}
|
||||
|
||||
internal func buildURLRequest(for url: URL, withMethod method: String, withBody body: Data?, withHeaders headers: [String:String]?, withServerUrl serverUrl: String) -> URLRequest {
|
||||
internal func buildURLRequest(for url: URL, usingMethod method: String, withBody body: Data?, andHeaders headers: [String:String]?, forServerUrl serverUrl: String) -> URLRequest {
|
||||
let request = NSMutableURLRequest(url: url)
|
||||
request.httpMethod = method
|
||||
|
||||
@@ -62,12 +62,12 @@ public class Network: NSObject {
|
||||
return request as URLRequest
|
||||
}
|
||||
|
||||
internal func request(_ url: URL, withMethod method: String, withServerUrl serverUrl: String, completionHandler: @escaping ResponseHandler) {
|
||||
return request(url, withMethod: method, withBody: nil, withHeaders: nil, withServerUrl: serverUrl, completionHandler: completionHandler)
|
||||
internal func request(_ url: URL, usingMethod method: String, forServerUrl serverUrl: String, completionHandler: @escaping ResponseHandler) {
|
||||
return request(url, withMethod: method, withBody: nil, andHeaders: nil, forServerUrl: serverUrl, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
internal func request(_ url: URL, withMethod method: String, withBody body: Data?, withHeaders headers: [String:String]?, withServerUrl serverUrl: String, completionHandler: @escaping ResponseHandler) {
|
||||
let urlRequest = buildURLRequest(for: url, withMethod: method, withBody: body, withHeaders: headers, withServerUrl: serverUrl)
|
||||
internal func request(_ url: URL, withMethod method: String, withBody body: Data?, andHeaders headers: [String:String]?, forServerUrl serverUrl: String, completionHandler: @escaping ResponseHandler) {
|
||||
let urlRequest = buildURLRequest(for: url, usingMethod: method, withBody: body, andHeaders: headers, forServerUrl: serverUrl)
|
||||
|
||||
let task = session!.dataTask(with: urlRequest) { data, response, error in
|
||||
completionHandler(data, response, error)
|
||||
@@ -76,26 +76,3 @@ public class Network: NSObject {
|
||||
task.resume()
|
||||
}
|
||||
}
|
||||
|
||||
extension Network: URLSessionDelegate, URLSessionTaskDelegate {
|
||||
public func urlSession(_ session: URLSession,
|
||||
task: URLSessionTask,
|
||||
didReceive challenge: URLAuthenticationChallenge,
|
||||
completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
|
||||
var credential: URLCredential? = nil
|
||||
var disposition: URLSession.AuthChallengeDisposition = .performDefaultHandling
|
||||
|
||||
let authMethod = challenge.protectionSpace.authenticationMethod
|
||||
if authMethod == NSURLAuthenticationMethodClientCertificate {
|
||||
let host = task.currentRequest!.url!.host!
|
||||
if let (identity, certificate) = try? Keychain.default.getClientIdentityAndCertificate(for: host) {
|
||||
credential = URLCredential(identity: identity,
|
||||
certificates: [certificate],
|
||||
persistence: URLCredential.Persistence.permanent)
|
||||
}
|
||||
disposition = .useCredential
|
||||
}
|
||||
|
||||
completionHandler(disposition, credential)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,347 +0,0 @@
|
||||
import Foundation
|
||||
import UserNotifications
|
||||
import SQLite
|
||||
import os.log
|
||||
|
||||
public struct AckNotification: Codable {
|
||||
let type: String
|
||||
let id: String
|
||||
let postId: String?
|
||||
public let serverUrl: String
|
||||
public let isIdLoaded: Bool
|
||||
let receivedAt:Int
|
||||
let platform = "ios"
|
||||
|
||||
public enum AckNotificationKeys: String, CodingKey {
|
||||
case type = "type"
|
||||
case id = "ack_id"
|
||||
case postId = "post_id"
|
||||
case server_id = "server_id"
|
||||
case isIdLoaded = "id_loaded"
|
||||
case platform = "platform"
|
||||
}
|
||||
|
||||
public enum AckNotificationRequestKeys: String, CodingKey {
|
||||
case type = "type"
|
||||
case id = "id"
|
||||
case postId = "post_id"
|
||||
case isIdLoaded = "is_id_loaded"
|
||||
case receivedAt = "received_at"
|
||||
case platform = "platform"
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: AckNotificationKeys.self)
|
||||
id = try container.decode(String.self, forKey: .id)
|
||||
type = try container.decode(String.self, forKey: .type)
|
||||
postId = try? container.decode(String.self, forKey: .postId)
|
||||
if container.contains(.isIdLoaded) {
|
||||
isIdLoaded = (try? container.decode(Bool.self, forKey: .isIdLoaded)) == true
|
||||
} else {
|
||||
isIdLoaded = false
|
||||
}
|
||||
receivedAt = Date().millisecondsSince1970
|
||||
|
||||
if let decodedIdentifier = try? container.decode(String.self, forKey: .server_id) {
|
||||
serverUrl = try Database.default.getServerUrlForServer(decodedIdentifier)
|
||||
} else {
|
||||
serverUrl = try Database.default.getOnlyServerUrl()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
func toUrlSafeBase64Encode() -> String {
|
||||
return Data(
|
||||
self.replacingOccurrences(of: "/\\+/g", with: "-", options: .regularExpression)
|
||||
.replacingOccurrences(of: "/\\//g", with: "_", options: .regularExpression)
|
||||
.utf8
|
||||
).base64EncodedString()
|
||||
}
|
||||
}
|
||||
|
||||
extension Network {
|
||||
@objc public func postNotificationReceipt(_ userInfo: [AnyHashable:Any]) {
|
||||
if let jsonData = try? JSONSerialization.data(withJSONObject: userInfo),
|
||||
let ackNotification = try? JSONDecoder().decode(AckNotification.self, from: jsonData) {
|
||||
postNotificationReceipt(ackNotification, completionHandler: {_, _, _ in})
|
||||
}
|
||||
}
|
||||
|
||||
private func matchUsername(in message: String) -> [String] {
|
||||
let specialMentions = Set(["all", "here", "channel"])
|
||||
if 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))
|
||||
if !results.isEmpty {
|
||||
let username = results.map({ String(message[Range($0.range, in: message)!]).removePrefix("@") }).filter({ !specialMentions.contains($0)})
|
||||
return username
|
||||
}
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
public func fetchProfileImageSync(_ serverUrl: String, senderId: String, overrideIconUrl: String?, completionHandler: @escaping (_ data: Data?) -> Void) {
|
||||
var updatedAt: Double = 0
|
||||
func processResponse(data: Data?, response: URLResponse?, error: Error?) {
|
||||
if let httpResponse = response as? HTTPURLResponse {
|
||||
if (httpResponse.statusCode == 200 && error == nil) {
|
||||
ImageCache.default.insertImage(data, for: senderId, updatedAt: updatedAt, onServer: serverUrl)
|
||||
completionHandler(data)
|
||||
} else {
|
||||
os_log(
|
||||
OSLogType.default,
|
||||
"Mattermost Notifications: Request for profile image failed with status %{public}@ and error %{public}@",
|
||||
httpResponse.statusCode,
|
||||
(error?.localizedDescription ?? "")
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let overrideUrl = overrideIconUrl,
|
||||
let url = URL(string: overrideUrl) {
|
||||
request(url, withMethod: "GET", withServerUrl: "", completionHandler: processResponse)
|
||||
} else {
|
||||
if let lastUpdateAt = Database.default.getUserLastPictureAt(for: senderId, withServerUrl: serverUrl) {
|
||||
updatedAt = lastUpdateAt
|
||||
}
|
||||
if let image = ImageCache.default.image(for: senderId, updatedAt: updatedAt, onServer: serverUrl) {
|
||||
os_log(OSLogType.default, "Mattermost Notifications: cached image")
|
||||
completionHandler(image)
|
||||
} else {
|
||||
ImageCache.default.removeImage(for: senderId, onServer: serverUrl)
|
||||
os_log(OSLogType.default, "Mattermost Notifications: image not cached")
|
||||
fetchUserProfilePicture(userId: senderId, lastUpdateAt: updatedAt, withServerUrl: serverUrl, completionHandler: processResponse)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func postNotificationReceipt(_ ackNotification: AckNotification, completionHandler: @escaping ResponseHandler) {
|
||||
do {
|
||||
let jsonData = try JSONEncoder().encode(ackNotification)
|
||||
let headers = ["Content-Type": "application/json; charset=utf-8"]
|
||||
let endpoint = "/notifications/ack"
|
||||
let url = buildApiUrl(ackNotification.serverUrl, endpoint)
|
||||
request(url, withMethod: "POST", withBody: jsonData, withHeaders: headers, withServerUrl: ackNotification.serverUrl, completionHandler: completionHandler)
|
||||
} catch {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public func fetchAndStoreDataForPushNotification(_ notification: UNMutableNotificationContent, withContentHandler contentHandler: ((UNNotificationContent) -> Void)?) {
|
||||
let operation = BlockOperation {
|
||||
let group = DispatchGroup()
|
||||
|
||||
let channelId = notification.userInfo["channel_id"] as? String
|
||||
let rootId = notification.userInfo["root_id"] as? String ?? ""
|
||||
let serverUrl = notification.userInfo["server_url"] as? String
|
||||
let isCRTEnabled = notification.userInfo["is_crt_enabled"] as? Bool ?? false
|
||||
|
||||
guard let serverUrl = serverUrl,
|
||||
let channelId = channelId
|
||||
else { return }
|
||||
|
||||
let currentUser = try? Database.default.queryCurrentUser(serverUrl)
|
||||
let currentUserId = currentUser?[Expression<String>("id")]
|
||||
let currentUsername = currentUser?[Expression<String>("username")]
|
||||
|
||||
|
||||
var postData: PostData? = nil
|
||||
var myChannelData: ChannelMemberData? = nil
|
||||
var threadData: ThreadData? = nil
|
||||
var threads: [Post] = []
|
||||
var userIdsToLoad: Set<String> = Set()
|
||||
var usernamesToLoad: Set<String> = Set()
|
||||
var users: Set<User> = Set()
|
||||
|
||||
if isCRTEnabled && !rootId.isEmpty {
|
||||
// Fetch the thread mentions
|
||||
let teamId = Gekidou.Database.default.queryTeamIdForChannel(withId: channelId, withServerUrl: serverUrl) ?? ""
|
||||
|
||||
if !teamId.isEmpty {
|
||||
group.enter()
|
||||
self.fetchThreadMentions(teamId: teamId, threadId: rootId, withServerUrl: serverUrl, completionHandler: {data, response, error in
|
||||
if self.responseOK(response), let data = data {
|
||||
threadData = try? JSONDecoder().decode(ThreadData.self, from: data)
|
||||
}
|
||||
group.leave()
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// Fetch the channel mentions
|
||||
group.enter()
|
||||
self.fetchChannelMentions(channelId: channelId, withServerUrl: serverUrl, completionHandler: { data, response, error in
|
||||
if self.responseOK(response), let data = data {
|
||||
myChannelData = try? JSONDecoder().decode(ChannelMemberData.self, from: data)
|
||||
}
|
||||
group.leave()
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
group.enter()
|
||||
let since = try? Database.default.queryPostsSinceForChannel(withId: channelId, withServerUrl: serverUrl)
|
||||
self.fetchPostsForChannel(withId: channelId, withSince: since, withServerUrl: serverUrl, withIsCRTEnabled: isCRTEnabled, withRootId: rootId) { data, response, error in
|
||||
if self.responseOK(response), let data = data,
|
||||
let jsonData = try? JSONDecoder().decode(PostData.self, from: data) {
|
||||
postData = jsonData
|
||||
if jsonData.posts.count > 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
|
||||
|
||||
jsonData.posts.forEach{post in
|
||||
if (currentUserId != nil && post.user_id != currentUserId) {
|
||||
authorIds.insert(post.user_id)
|
||||
}
|
||||
self.matchUsername(in: post.message).forEach{
|
||||
if ($0 != currentUsername) {
|
||||
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
|
||||
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) {
|
||||
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()
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
group.leave()
|
||||
}
|
||||
|
||||
group.wait()
|
||||
|
||||
group.enter()
|
||||
if let data = postData,
|
||||
let posts = data.posts as [Post]?,
|
||||
let db = try? Database.default.getDatabaseForServer(serverUrl),
|
||||
posts.count > 0 {
|
||||
let receivingThreads = isCRTEnabled && !rootId.isEmpty
|
||||
try? db.transaction {
|
||||
try? Database.default.handlePostData(db, data, channelId, since != nil, receivingThreads)
|
||||
|
||||
if threads.count > 0 {
|
||||
try? Database.default.handleThreads(db, threads)
|
||||
}
|
||||
|
||||
if users.count > 0 {
|
||||
try? Database.default.insertUsers(db, users)
|
||||
}
|
||||
|
||||
if let myChannel = myChannelData {
|
||||
try? Database.default.handleMyChannelMentions(db, myChannel, withCRTEnabled: isCRTEnabled)
|
||||
}
|
||||
|
||||
if let threads = threadData {
|
||||
try? Database.default.handleThreadMentions(db, threads)
|
||||
}
|
||||
}
|
||||
}
|
||||
group.leave()
|
||||
|
||||
if let contentHandler = contentHandler {
|
||||
// Get the total mentions from all databases and set the badge icon
|
||||
notification.badge = Gekidou.Database.default.getTotalMentions() as NSNumber
|
||||
contentHandler(notification)
|
||||
}
|
||||
}
|
||||
|
||||
queue.addOperation(operation)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import Foundation
|
||||
|
||||
public struct AckNotification: Codable {
|
||||
let type: String
|
||||
let id: String
|
||||
let postId: String?
|
||||
public let serverUrl: String
|
||||
public let isIdLoaded: Bool
|
||||
let receivedAt:Int
|
||||
let platform = "ios"
|
||||
|
||||
public enum AckNotificationKeys: String, CodingKey {
|
||||
case type = "type"
|
||||
case id = "ack_id"
|
||||
case postId = "post_id"
|
||||
case server_id = "server_id"
|
||||
case isIdLoaded = "id_loaded"
|
||||
case platform = "platform"
|
||||
}
|
||||
|
||||
public enum AckNotificationRequestKeys: String, CodingKey {
|
||||
case type = "type"
|
||||
case id = "id"
|
||||
case postId = "post_id"
|
||||
case isIdLoaded = "is_id_loaded"
|
||||
case receivedAt = "received_at"
|
||||
case platform = "platform"
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: AckNotificationKeys.self)
|
||||
id = try container.decode(String.self, forKey: .id)
|
||||
type = try container.decode(String.self, forKey: .type)
|
||||
postId = try? container.decode(String.self, forKey: .postId)
|
||||
if container.contains(.isIdLoaded) {
|
||||
isIdLoaded = (try? container.decode(Bool.self, forKey: .isIdLoaded)) == true
|
||||
} else {
|
||||
isIdLoaded = false
|
||||
}
|
||||
receivedAt = Date().millisecondsSince1970
|
||||
|
||||
if let decodedIdentifier = try? container.decode(String.self, forKey: .server_id) {
|
||||
serverUrl = try Database.default.getServerUrlForServer(decodedIdentifier)
|
||||
} else {
|
||||
serverUrl = try Database.default.getOnlyServerUrl()
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import Foundation
|
||||
|
||||
extension PushNotification {
|
||||
func addChannelToDefaultCategoryIfNeeded(_ channel: Channel, forServerUrl serverUrl: String) -> [CategoryChannel]? {
|
||||
var categoryChannels = [CategoryChannel]()
|
||||
if channel.type == "D" || channel.type == "G" {
|
||||
if let teamIds = Database.default.queryAllMyTeamIds(serverUrl) {
|
||||
for teamId in teamIds {
|
||||
if let item = categoryChannelForTeam(channelId: channel.id, teamId: teamId, type: "direct_messages", forServerUrl: serverUrl) {
|
||||
categoryChannels.append(item)
|
||||
}
|
||||
}
|
||||
return categoryChannels
|
||||
}
|
||||
} else if let item = categoryChannelForTeam(channelId: channel.id, teamId: channel.teamId, type: "channels", forServerUrl: serverUrl) {
|
||||
categoryChannels.append(item)
|
||||
return categoryChannels
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private func categoryChannelForTeam(channelId: String, teamId: String, type: String, forServerUrl serverUrl: String) -> CategoryChannel? {
|
||||
guard !teamId.isEmpty,
|
||||
let categoryId = Database.default.queryCategoryId(inTeamId: teamId, type: type, forServerUrl: serverUrl) else { return nil }
|
||||
|
||||
let cc = Database.default.queryCategoryChannelId(inCategoryId: categoryId, channelId: channelId, forServerUrl: serverUrl)
|
||||
if cc == nil {
|
||||
return CategoryChannel(
|
||||
id: "\(teamId)_\(channelId)",
|
||||
categoryId: categoryId,
|
||||
channelId: channelId
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
import Foundation
|
||||
import os.log
|
||||
import SQLite
|
||||
import UserNotifications
|
||||
|
||||
public struct PushNotificationData: Encodable {
|
||||
public var categories: CategoriesWithOrder? = nil
|
||||
public var categoryChannels: [CategoryChannel]? = nil
|
||||
public var channel: Channel? = nil
|
||||
public var myChannel: ChannelMember? = nil
|
||||
public var team: Team? = nil
|
||||
public var myTeam: TeamMember? = nil
|
||||
public var posts: PostResponse? = nil
|
||||
public var users = [User]()
|
||||
public var threads: [PostThread]? = nil
|
||||
|
||||
public enum PushNotificationDataKeys: String, CodingKey {
|
||||
case categories, categoryChannels, channel, myChannel, team, myTeam, posts, users, threads
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: PushNotificationDataKeys.self)
|
||||
try container.encodeIfPresent(self.categories, forKey: .categories)
|
||||
try container.encodeIfPresent(self.categoryChannels, forKey: .categoryChannels)
|
||||
try container.encodeIfPresent(self.channel, forKey: .channel)
|
||||
try container.encodeIfPresent(self.myChannel, forKey: .myChannel)
|
||||
try container.encodeIfPresent(self.team, forKey: .team)
|
||||
try container.encodeIfPresent(self.myTeam, forKey: .myTeam)
|
||||
try container.encodeIfPresent(self.posts, forKey: .posts)
|
||||
try container.encode(self.users, forKey: .users)
|
||||
try container.encodeIfPresent(self.threads, forKey: .threads)
|
||||
}
|
||||
}
|
||||
|
||||
extension PushNotification {
|
||||
public func fetchDataForPushNotification(_ notification: [AnyHashable:Any], withContentHandler contentHander: @escaping ((_ data: PushNotificationData) -> Void)) {
|
||||
let operation = BlockOperation {
|
||||
os_log(OSLogType.default, "Mattermost Notifications: Fetch notification data.")
|
||||
let fetchGroup = DispatchGroup()
|
||||
|
||||
let teamId = notification["team_id"] as? String ?? ""
|
||||
let channelId = notification["channel_id"] as? String
|
||||
let postId = notification["post_id"] as? String
|
||||
let rootId = notification["root_id"] as? String ?? ""
|
||||
var serverUrl = notification["server_url"] as? String
|
||||
let serverId = notification["server_id"] as? String
|
||||
let isCRTEnabled = notification["is_crt_enabled"] as? Bool ?? false
|
||||
|
||||
if let serverId = serverId,
|
||||
serverUrl == nil {
|
||||
serverUrl = try? Database.default.getServerUrlForServer(serverId)
|
||||
}
|
||||
|
||||
guard let serverUrl = serverUrl,
|
||||
let channelId = channelId,
|
||||
let _ = postId
|
||||
else { return }
|
||||
|
||||
var notificationData = PushNotificationData()
|
||||
if !teamId.isEmpty {
|
||||
fetchGroup.enter()
|
||||
Network.default.fetchTeamIfNeeded(withId: teamId, forServerUrl: serverUrl) { team, myTeam in
|
||||
notificationData.team = team
|
||||
notificationData.myTeam = myTeam
|
||||
fetchGroup.leave()
|
||||
}
|
||||
}
|
||||
|
||||
fetchGroup.enter()
|
||||
Network.default.fetchMyChannel(withId: channelId, forServerUrl: serverUrl) { channel, myChannel, profiles in
|
||||
notificationData.channel = channel
|
||||
notificationData.myChannel = myChannel
|
||||
|
||||
if let profiles = profiles {
|
||||
notificationData.users.append(contentsOf: profiles)
|
||||
}
|
||||
fetchGroup.leave()
|
||||
}
|
||||
|
||||
if let _ = notificationData.myTeam,
|
||||
!teamId.isEmpty {
|
||||
fetchGroup.enter()
|
||||
Network.default.fetchCategories(withTeamId: teamId, forServerUrl: serverUrl) { categoriesWithOrder in
|
||||
if let categoriesWithOrder = categoriesWithOrder {
|
||||
notificationData.categories = categoriesWithOrder
|
||||
}
|
||||
fetchGroup.leave()
|
||||
}
|
||||
} else if let channel = notificationData.channel {
|
||||
if let categoryChannels = self.addChannelToDefaultCategoryIfNeeded(channel, forServerUrl: serverUrl) {
|
||||
notificationData.categoryChannels = categoryChannels
|
||||
}
|
||||
}
|
||||
|
||||
fetchGroup.enter()
|
||||
Network.default.fetchPosts(forChannelId: channelId, andRootId: rootId, havingCRTEnabled: isCRTEnabled, withAlreadyLoadedProfiles: notificationData.users, forServerUrl: serverUrl) { postResponse, threads, users in
|
||||
notificationData.posts = postResponse
|
||||
notificationData.threads = threads
|
||||
if let users = users {
|
||||
notificationData.users.append(contentsOf: users)
|
||||
}
|
||||
fetchGroup.leave()
|
||||
}
|
||||
|
||||
fetchGroup.notify(queue: DispatchQueue.main) {
|
||||
if isCRTEnabled && !rootId.isEmpty {
|
||||
Network.default.fetchThread(byId: rootId, belongingToTeamId: teamId, forServerUrl: serverUrl) { thread in
|
||||
if let thread = thread {
|
||||
if notificationData.threads == nil {
|
||||
notificationData.threads = [thread]
|
||||
}
|
||||
if let threads = notificationData.threads,
|
||||
let index = threads.firstIndex(where: { $0.id == thread.id }) {
|
||||
var copy = threads[index]
|
||||
copy.unreadMentions = thread.unreadMentions
|
||||
copy.unreadReplies = thread.unreadReplies
|
||||
copy.lastReplyAt = thread.lastReplyAt
|
||||
copy.lastViewedAt = thread.lastViewedAt
|
||||
notificationData.threads?[index] = copy
|
||||
}
|
||||
}
|
||||
contentHander(notificationData);
|
||||
}
|
||||
} else {
|
||||
contentHander(notificationData);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
queue.addOperation(operation)
|
||||
}
|
||||
|
||||
public func fetchAndStoreDataForPushNotification(_ notification: UNMutableNotificationContent, withContentHandler contentHandler: @escaping ((UNNotificationContent) -> Void)) {
|
||||
guard let serverUrl = notification.userInfo["server_url"] as? String,
|
||||
let channelId = notification.userInfo["channel_id"] as? String
|
||||
else {
|
||||
contentHandler(notification)
|
||||
return
|
||||
}
|
||||
|
||||
let isCRTEnabled = notification.userInfo["is_crt_enabled"] as? Bool ?? false
|
||||
let rootId = notification.userInfo["root_id"] as? String ?? ""
|
||||
let teamId = notification.userInfo["team_id"] as? String ?? ""
|
||||
|
||||
fetchDataForPushNotification(notification.userInfo) { data in
|
||||
if let db = try? Database.default.getDatabaseForServer(serverUrl) {
|
||||
try? db.transaction {
|
||||
let receivingThreads = isCRTEnabled && !rootId.isEmpty
|
||||
if let team = data.team {
|
||||
try? Database.default.insertTeam(db, team)
|
||||
}
|
||||
|
||||
if let myTeam = data.myTeam {
|
||||
try? Database.default.insertMyTeam(db, myTeam)
|
||||
}
|
||||
|
||||
if let categories = data.categories {
|
||||
try? Database.default.insertCategoriesWithChannels(db, categories.categories)
|
||||
}
|
||||
|
||||
if let categoryChannels = data.categoryChannels,
|
||||
!categoryChannels.isEmpty {
|
||||
try? Database.default.insertChannelToDefaultCategory(db, categoryChannels)
|
||||
}
|
||||
|
||||
if let channel = data.channel,
|
||||
!Database.default.queryChannelExists(withId: channel.id, forServerUrl: serverUrl) {
|
||||
try? Database.default.insertChannel(db, channel)
|
||||
}
|
||||
|
||||
if var myChannel = data.myChannel {
|
||||
var lastFetchedAt: Double = 0
|
||||
if let postResponse = data.posts, !receivingThreads {
|
||||
let posts = Array(postResponse.posts.values)
|
||||
if let fetchedAt = posts.map({max($0.createAt, $0.updateAt, $0.deleteAt)}).max() {
|
||||
lastFetchedAt = fetchedAt
|
||||
}
|
||||
}
|
||||
var lastPostAt: Double = 0
|
||||
if let channel = data.channel {
|
||||
lastPostAt = isCRTEnabled ? channel.lastRootPostAt : channel.lastPostAt
|
||||
myChannel.internalMsgCount = channel.totalMsgCount - myChannel.msgCount
|
||||
myChannel.internalMsgCountRoot = channel.totalMsgCountRoot - myChannel.msgCountRoot
|
||||
}
|
||||
try? Database.default.insertOrUpdateMyChannel(db, myChannel, isCRTEnabled, lastFetchedAt, lastPostAt)
|
||||
}
|
||||
|
||||
if let posts = data.posts {
|
||||
try? Database.default.handlePostData(db, posts, channelId, receivingThreads)
|
||||
}
|
||||
|
||||
if let threads = data.threads {
|
||||
try? Database.default.handleThreads(db, threads, forTeamId: teamId)
|
||||
}
|
||||
|
||||
if !data.users.isEmpty {
|
||||
try? Database.default.insertUsers(db, data.users)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
notification.badge = Gekidou.Database.default.getTotalMentions() as NSNumber
|
||||
contentHandler(notification)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import Foundation
|
||||
import os.log
|
||||
|
||||
extension PushNotification {
|
||||
public func fetchProfileImageSync(_ serverUrl: String, senderId: String, overrideIconUrl: String?, completionHandler: @escaping (_ data: Data?) -> Void) {
|
||||
var updatedAt: Double = 0
|
||||
func processResponse(data: Data?, response: URLResponse?, error: Error?) {
|
||||
if let httpResponse = response as? HTTPURLResponse {
|
||||
if (httpResponse.statusCode == 200 && error == nil) {
|
||||
ImageCache.default.insertImage(data, for: senderId, updatedAt: updatedAt, forServer: serverUrl)
|
||||
completionHandler(data)
|
||||
} else {
|
||||
os_log(
|
||||
OSLogType.default,
|
||||
"Mattermost Notifications: Request for profile image failed with status %{public}@ and error %{public}@",
|
||||
httpResponse.statusCode,
|
||||
(error?.localizedDescription ?? "")
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let overrideUrl = overrideIconUrl,
|
||||
let url = URL(string: overrideUrl) {
|
||||
Network.default.request(url, usingMethod: "GET", forServerUrl: "", completionHandler: processResponse)
|
||||
} else {
|
||||
if let lastUpdateAt = Database.default.getUserLastPictureAt(for: senderId, forServerUrl: serverUrl) {
|
||||
updatedAt = lastUpdateAt
|
||||
}
|
||||
if let image = ImageCache.default.image(for: senderId, updatedAt: updatedAt, forServer: serverUrl) {
|
||||
os_log(OSLogType.default, "Mattermost Notifications: cached image")
|
||||
completionHandler(image)
|
||||
} else {
|
||||
ImageCache.default.removeImage(for: senderId, forServer: serverUrl)
|
||||
os_log(OSLogType.default, "Mattermost Notifications: image not cached")
|
||||
Network.default.fetchUserProfilePicture(
|
||||
userId: senderId, lastUpdateAt: updatedAt,
|
||||
forServerUrl: serverUrl, completionHandler: processResponse
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
import Foundation
|
||||
import UserNotifications
|
||||
import os.log
|
||||
|
||||
public class PushNotification: NSObject {
|
||||
@objc public static let `default` = PushNotification()
|
||||
let fibonacciBackoffsInSeconds = [1.0, 2.0, 3.0, 5.0, 8.0]
|
||||
private var retryIndex = 0
|
||||
let queue = OperationQueue()
|
||||
|
||||
public override init() {
|
||||
queue.maxConcurrentOperationCount = 1
|
||||
}
|
||||
|
||||
@objc public func postNotificationReceipt(_ userInfo: [AnyHashable:Any]) {
|
||||
let notification = UNMutableNotificationContent()
|
||||
notification.userInfo = userInfo
|
||||
postNotificationReceipt(notification, completionHandler: {_ in})
|
||||
}
|
||||
|
||||
public func postNotificationReceipt(_ notification: UNMutableNotificationContent, completionHandler: @escaping (_ notification: UNMutableNotificationContent?) -> Void) {
|
||||
if let jsonData = try? JSONSerialization.data(withJSONObject: notification.userInfo),
|
||||
let ackNotification = try? JSONDecoder().decode(AckNotification.self, from: jsonData) {
|
||||
postNotificationReceiptWithRetry(ackNotification) { data in
|
||||
os_log(
|
||||
OSLogType.default,
|
||||
"Mattermost Notifications: process receipt response for serverUrl %{public}@",
|
||||
ackNotification.serverUrl
|
||||
)
|
||||
|
||||
guard let data = data else {
|
||||
os_log(
|
||||
OSLogType.default,
|
||||
"Mattermost Notifications: process receipt response for serverUrl %{public}@ does not contain data",
|
||||
ackNotification.serverUrl
|
||||
)
|
||||
completionHandler(notification)
|
||||
return
|
||||
}
|
||||
notification.userInfo["server_url"] = ackNotification.serverUrl
|
||||
if let idLoadedNotification = self.parseIdLoadedNotification(data) {
|
||||
if let body = idLoadedNotification["message"] as? String {
|
||||
notification.body = body
|
||||
}
|
||||
|
||||
if let title = idLoadedNotification["channel_name"] as? String {
|
||||
notification.title = title
|
||||
}
|
||||
|
||||
for (key, value) in idLoadedNotification {
|
||||
notification.userInfo[key] = value
|
||||
}
|
||||
}
|
||||
completionHandler(notification)
|
||||
}
|
||||
return
|
||||
}
|
||||
os_log(OSLogType.default, "Mattermost Notifications: Could not parse ACK notification")
|
||||
completionHandler(nil)
|
||||
}
|
||||
|
||||
private func postNotificationReceiptWithRetry(_ ackNotification: AckNotification, completionHandler: @escaping (_ data: Data?) -> Void) {
|
||||
if (self.retryIndex >= self.fibonacciBackoffsInSeconds.count) {
|
||||
os_log(OSLogType.default, "Mattermost Notifications: max retries reached. Will call sendMessageIntent")
|
||||
completionHandler(nil)
|
||||
return
|
||||
}
|
||||
|
||||
do {
|
||||
let jsonData = try JSONEncoder().encode(ackNotification)
|
||||
let headers = ["Content-Type": "application/json; charset=utf-8"]
|
||||
let endpoint = "/notifications/ack"
|
||||
let url = Network.default.buildApiUrl(ackNotification.serverUrl, endpoint)
|
||||
Network.default.request(
|
||||
url, withMethod: "POST", withBody: jsonData,
|
||||
andHeaders: headers, forServerUrl: ackNotification.serverUrl) { data, response, error in
|
||||
if (error != nil && ackNotification.isIdLoaded) {
|
||||
let backoffInSeconds = self.fibonacciBackoffsInSeconds[self.retryIndex]
|
||||
self.retryIndex += 1
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + backoffInSeconds, execute: {[weak self] in
|
||||
os_log(
|
||||
OSLogType.default,
|
||||
"Mattermost Notifications: receipt retrieval failed. Retry %{public}@",
|
||||
String(describing: self?.retryIndex)
|
||||
)
|
||||
self?.postNotificationReceiptWithRetry(ackNotification, completionHandler: completionHandler)
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
completionHandler(data)
|
||||
}
|
||||
} catch {
|
||||
os_log(OSLogType.default, "Mattermost Notifications: receipt failed %{public}@", error.localizedDescription)
|
||||
}
|
||||
}
|
||||
|
||||
private func parseIdLoadedNotification(_ data: Data?) -> [AnyHashable:Any]? {
|
||||
if let data = data,
|
||||
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
|
||||
{
|
||||
os_log(OSLogType.default, "Mattermost Notifications: parsed json response")
|
||||
var userInfo = [AnyHashable:Any]()
|
||||
let userInfoKeys = ["channel_name", "team_id", "sender_id", "sender_name", "root_id", "override_username", "override_icon_url", "from_webhook", "message"]
|
||||
for key in userInfoKeys {
|
||||
if let value = json[key] as? String {
|
||||
userInfo[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
return userInfo
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
111
ios/Gekidou/Sources/Gekidou/Storage/Database+Category.swift
Normal file
111
ios/Gekidou/Sources/Gekidou/Storage/Database+Category.swift
Normal file
@@ -0,0 +1,111 @@
|
||||
import Foundation
|
||||
import SQLite
|
||||
|
||||
extension Database {
|
||||
public func queryCategoryId(inTeamId teamId: String, type: String, forServerUrl serverUrl: String) -> String? {
|
||||
if let db = try? getDatabaseForServer(serverUrl) {
|
||||
let idCol = Expression<String>("id")
|
||||
let teamIdCol = Expression<String>("team_id")
|
||||
let typeCol = Expression<String>("type")
|
||||
let query = categoryTable.where(teamIdCol == teamId && typeCol == type)
|
||||
if let result = try? db.pluck(query) {
|
||||
return try? result.get(idCol)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
public func queryCategoryChannelId(inCategoryId categoryId: String, channelId: String, forServerUrl serverUrl: String) -> String? {
|
||||
if let db = try? getDatabaseForServer(serverUrl) {
|
||||
let idCol = Expression<String>("id")
|
||||
let categoryIdCol = Expression<String>("category_id")
|
||||
let channelIdCol = Expression<String>("channel_id")
|
||||
let query = categoryChannelTable.where(categoryIdCol == categoryId && channelIdCol == channelId)
|
||||
if let result = try? db.pluck(query) {
|
||||
return try? result.get(idCol)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
public func insertCategoriesWithChannels(_ db: Connection, _ categoriesWithChannels: [Category]) throws {
|
||||
let categories = createCategoriesSetter(from: categoriesWithChannels)
|
||||
let categoryChannels = createCategoryChannelsSetter(from: categoriesWithChannels)
|
||||
try db.run(categoryTable.insertMany(or: .replace, categories))
|
||||
try db.run(categoryChannelTable.insertMany(or: .replace, categoryChannels))
|
||||
}
|
||||
|
||||
public func insertChannelToDefaultCategory(_ db: Connection, _ categoryChannels: [CategoryChannel]) throws {
|
||||
let categoryId = Expression<String>("category_id")
|
||||
for cc in categoryChannels {
|
||||
let count = (try? db.scalar(categoryChannelTable.where(categoryId == cc.categoryId).count)) ?? 0
|
||||
let setter = createCategoryChannelsSetter(from: cc, index: count > 0 ? count + 1 : 0)
|
||||
try db.run(categoryChannelTable.insert(or: .replace, setter))
|
||||
}
|
||||
}
|
||||
|
||||
private func createCategoriesSetter(from categories: [Category]) -> [[Setter]] {
|
||||
let id = Expression<String>("id")
|
||||
let collapsed = Expression<Bool>("collapsed")
|
||||
let displayName = Expression<String>("display_name")
|
||||
let muted = Expression<Bool>("muted")
|
||||
let sortOrder = Expression<Int>("sort_order")
|
||||
let sorting = Expression<String>("sorting")
|
||||
let teamId = Expression<String>("team_id")
|
||||
let type = Expression<String>("type")
|
||||
|
||||
var setters = [[Setter]]()
|
||||
for category in categories {
|
||||
var setter = [Setter]()
|
||||
setter.append(id <- category.id)
|
||||
setter.append(collapsed <- category.collapsed)
|
||||
setter.append(displayName <- category.displayName)
|
||||
setter.append(muted <- category.muted)
|
||||
setter.append(sortOrder <- category.sortOrder / 10)
|
||||
setter.append(sorting <- category.sorting)
|
||||
setter.append(teamId <- category.teamId)
|
||||
setter.append(type <- category.type)
|
||||
setters.append(setter)
|
||||
}
|
||||
|
||||
return setters
|
||||
}
|
||||
|
||||
private func createCategoryChannelsSetter(from categories: [Category]) -> [[Setter]] {
|
||||
let id = Expression<String>("id")
|
||||
let categoryId = Expression<String>("category_id")
|
||||
let channelId = Expression<String>("channel_id")
|
||||
let sortOrder = Expression<Int>("sort_order")
|
||||
|
||||
var setters = [[Setter]]()
|
||||
for category in categories {
|
||||
for (index, chId) in category.channelIds.enumerated() {
|
||||
var setter = [Setter]()
|
||||
setter.append(id <- "\(category.teamId)_\(chId)")
|
||||
setter.append(categoryId <- category.id)
|
||||
setter.append(channelId <- chId)
|
||||
setter.append(sortOrder <- index)
|
||||
setters.append(setter)
|
||||
}
|
||||
}
|
||||
|
||||
return setters
|
||||
}
|
||||
|
||||
private func createCategoryChannelsSetter(from categoryChannel: CategoryChannel, index: Int = 0) -> [Setter] {
|
||||
let id = Expression<String>("id")
|
||||
let categoryId = Expression<String>("category_id")
|
||||
let channelId = Expression<String>("channel_id")
|
||||
let sortOrder = Expression<Int>("sort_order")
|
||||
|
||||
var setter = [Setter]()
|
||||
setter.append(id <- categoryChannel.id)
|
||||
setter.append(categoryId <- categoryChannel.categoryId)
|
||||
setter.append(channelId <- categoryChannel.channelId)
|
||||
setter.append(sortOrder <- index)
|
||||
|
||||
return setter
|
||||
}
|
||||
}
|
||||
@@ -40,6 +40,27 @@ extension Database {
|
||||
}
|
||||
}
|
||||
|
||||
public func queryChannelExists(withId channelId: String, forServerUrl serverUrl: String) -> Bool {
|
||||
if let db = try? getDatabaseForServer(serverUrl) {
|
||||
let idCol = Expression<String>("id")
|
||||
let query = channelTable.where(idCol == channelId)
|
||||
if let _ = try? db.pluck(query) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
public func hasMyChannel(_ db: Connection, channelId: String) -> Bool {
|
||||
let idCol = Expression<String>("id")
|
||||
let query = myChannelTable.where(idCol == channelId)
|
||||
if let _ = try? db.pluck(query) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
public func getCurrentChannelWithTeam<T: Codable>(_ serverUrl: String) -> T? {
|
||||
do {
|
||||
let channelId = try queryCurrentChannelId(serverUrl)
|
||||
@@ -186,4 +207,131 @@ extension Database {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
public func insertChannel(_ db: Connection, _ channel: Channel) throws {
|
||||
let setter = createChannelSetter(from: channel)
|
||||
try db.run(channelTable.insert(or: .replace, setter))
|
||||
let channelInfo = createChannelInfoSetter(from: channel)
|
||||
try db.run(channelInfoTable.insert(or: .replace, channelInfo))
|
||||
}
|
||||
|
||||
public func insertOrUpdateMyChannel(_ db: Connection, _ myChannel: ChannelMember, _ isCRTEnabled: Bool, _ lastFetchedAt: Double, _ lastPostAt: Double) throws {
|
||||
let idCol = Expression<String>("id")
|
||||
let messageCountCol = Expression<Int>("message_count")
|
||||
let mentionsCol = Expression<Int>("mentions_count")
|
||||
let isUnreadCol = Expression<Bool>("is_unread")
|
||||
let lastFetchedAtCol = Expression<Double>("last_fetched_at")
|
||||
let lastPostAtCol = Expression<Double>("last_post_at")
|
||||
let mentionsCount = isCRTEnabled ? myChannel.mentionCountRoot : myChannel.mentionCount
|
||||
let messageCount = isCRTEnabled ? myChannel.internalMsgCountRoot : myChannel.internalMsgCount
|
||||
let isUnread = messageCount > 0
|
||||
|
||||
if hasThread(db, threadId: myChannel.id) {
|
||||
let updateQuery = myChannelTable.where(idCol == myChannel.id)
|
||||
.update(
|
||||
messageCountCol <- messageCount,
|
||||
mentionsCol <- mentionsCount,
|
||||
isUnreadCol <- isUnreadCol,
|
||||
lastPostAtCol <- lastPostAt,
|
||||
lastFetchedAtCol <- lastFetchedAt
|
||||
)
|
||||
let _ = try db.run(updateQuery)
|
||||
} else {
|
||||
let rolesCol = Expression<String>("roles")
|
||||
let manuallyUnreadCol = Expression<Bool>("manually_unread")
|
||||
let lastViewedAtCol = Expression<Double>("last_viewed_at")
|
||||
let viewedAtCol = Expression<Double>("viewed_at")
|
||||
|
||||
let setter: [Setter] = [
|
||||
idCol <- myChannel.id,
|
||||
mentionsCol <- mentionsCount,
|
||||
messageCountCol <- messageCount,
|
||||
lastPostAtCol <- lastPostAt,
|
||||
lastViewedAtCol <- myChannel.lastViewedAt,
|
||||
viewedAtCol <- 0,
|
||||
lastFetchedAtCol <- lastFetchedAt,
|
||||
isUnreadCol <- isUnread,
|
||||
manuallyUnreadCol <- false,
|
||||
rolesCol <- myChannel.roles,
|
||||
]
|
||||
let _ = try db.run(myChannelTable.insert(or: .replace, setter))
|
||||
try insertMyChannelSettings(db, myChannel)
|
||||
try insertChannelMember(db, myChannel)
|
||||
}
|
||||
}
|
||||
|
||||
private func insertMyChannelSettings(_ db: Connection, _ myChannel: ChannelMember) throws {
|
||||
let id = Expression<String>("id")
|
||||
let notifyProps = Expression<String>("notify_props")
|
||||
|
||||
let setter: [Setter] = [
|
||||
id <- myChannel.id,
|
||||
notifyProps <- myChannel.notifyProps,
|
||||
]
|
||||
|
||||
let _ = try db.run(myChannelSettingsTable.insert(or: .replace, setter))
|
||||
}
|
||||
|
||||
private func insertChannelMember(_ db: Connection, _ member: ChannelMember) throws {
|
||||
let id = Expression<String>("id")
|
||||
let channelId = Expression<String>("channel_id")
|
||||
let userId = Expression<String>("user_id")
|
||||
let schemeAdmin = Expression<Bool>("scheme_admin")
|
||||
|
||||
let setter: [Setter] = [
|
||||
id <- "\(member.id)-\(member.userId)",
|
||||
channelId <- member.id,
|
||||
userId <- member.userId,
|
||||
schemeAdmin <- member.schemeAdmin,
|
||||
]
|
||||
|
||||
let _ = try db.run(channelMembershipTable.insert(or: .replace, setter))
|
||||
}
|
||||
|
||||
private func createChannelSetter(from channel: Channel) -> [Setter] {
|
||||
let id = Expression<String>("id")
|
||||
let createAt = Expression<Double>("create_at")
|
||||
let deleteAt = Expression<Double>("delete_at")
|
||||
let updateAt = Expression<Double>("update_at")
|
||||
let creatorId = Expression<String>("creator_id")
|
||||
let displayName = Expression<String>("display_name")
|
||||
let name = Expression<String>("name")
|
||||
let teamId = Expression<String>("team_id")
|
||||
let type = Expression<String>("type")
|
||||
let isGroupConstrained = Expression<Bool>("group_constrained")
|
||||
let shared = Expression<Bool>("shared")
|
||||
|
||||
var setter = [Setter]()
|
||||
setter.append(id <- channel.id)
|
||||
setter.append(createAt <- channel.createAt)
|
||||
setter.append(deleteAt <- channel.deleteAt)
|
||||
setter.append(updateAt <- channel.updateAt)
|
||||
setter.append(creatorId <- channel.creatorId)
|
||||
setter.append(displayName <- channel.displayName)
|
||||
setter.append(name <- channel.name)
|
||||
setter.append(teamId <- channel.teamId)
|
||||
setter.append(type <- channel.type)
|
||||
setter.append(isGroupConstrained <- channel.groupConstrained)
|
||||
setter.append(shared <- channel.shared)
|
||||
|
||||
return setter
|
||||
}
|
||||
|
||||
private func createChannelInfoSetter(from channel: Channel) -> [Setter] {
|
||||
let id = Expression<String>("id")
|
||||
let header = Expression<String>("header")
|
||||
let purpose = Expression<String>("purpose")
|
||||
let guestCount = Expression<Int>("guest_count")
|
||||
let memberCount = Expression<Int>("member_count")
|
||||
let pinnedPostCount = Expression<Int>("pinned_post_count")
|
||||
|
||||
var setter = [Setter]()
|
||||
setter.append(id <- channel.id)
|
||||
setter.append(header <- channel.header)
|
||||
setter.append(purpose <- channel.purpose)
|
||||
setter.append(guestCount <- 0)
|
||||
setter.append(memberCount <- 0)
|
||||
setter.append(pinnedPostCount <- 0)
|
||||
return setter
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,26 +2,6 @@ import Foundation
|
||||
import SQLite
|
||||
|
||||
extension Database {
|
||||
public func hasMyChannel(_ db: Connection, channelId: String) -> Bool {
|
||||
let idCol = Expression<String>("id")
|
||||
let query = myChannelTable.where(idCol == channelId)
|
||||
if let _ = try? db.pluck(query) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
public func hasThread(_ db: Connection, threadId: String) -> Bool {
|
||||
let idCol = Expression<String>("id")
|
||||
let query = threadTable.where(idCol == threadId)
|
||||
if let _ = try? db.pluck(query) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
public func getTotalMentions() -> Int {
|
||||
let serverUrls = getAllActiveServerUrls()
|
||||
var mentions = 0
|
||||
@@ -58,85 +38,6 @@ extension Database {
|
||||
return Int(mentions ?? 0)
|
||||
}
|
||||
|
||||
public func handleMyChannelMentions(_ db: Connection, _ channelMemberData: ChannelMemberData, withCRTEnabled crtEnabled: Bool) throws {
|
||||
let idCol = Expression<String>("id")
|
||||
let mentionsCol = Expression<Int>("mentions_count")
|
||||
let isUnreadCol = Expression<Bool>("is_unread")
|
||||
let mentions = crtEnabled ? channelMemberData.mention_count_root : channelMemberData.mention_count
|
||||
|
||||
if hasMyChannel(db, channelId: channelMemberData.channel_id) {
|
||||
let updateQuery = myChannelTable
|
||||
.where(idCol == channelMemberData.channel_id)
|
||||
.update(mentionsCol <- mentions,
|
||||
isUnreadCol <- true
|
||||
)
|
||||
let _ = try db.run(updateQuery)
|
||||
} else {
|
||||
let msgCol = Expression<Int>("message_count")
|
||||
let lastPostAtCol = Expression<Int64>("last_post_at")
|
||||
let lastViewedAtCol = Expression<Int64>("last_viewed_at")
|
||||
let viewedAtCol = Expression<Int64>("viewed_at")
|
||||
let lastFetchedAtCol = Expression<Int64>("last_fetched_at")
|
||||
let manuallyUnreadCol = Expression<Bool>("manually_unread")
|
||||
let rolesCol = Expression<String>("roles")
|
||||
let statusCol = Expression<String>("status")
|
||||
|
||||
let setters: [Setter] = [
|
||||
idCol <- channelMemberData.channel_id,
|
||||
mentionsCol <- mentions,
|
||||
msgCol <- mentions,
|
||||
lastPostAtCol <- channelMemberData.last_update_at,
|
||||
lastViewedAtCol <- channelMemberData.last_viewed_at,
|
||||
viewedAtCol <- 0,
|
||||
lastFetchedAtCol <- 0,
|
||||
isUnreadCol <- true,
|
||||
manuallyUnreadCol <- false,
|
||||
rolesCol <- channelMemberData.roles,
|
||||
statusCol <- "created"
|
||||
]
|
||||
|
||||
let insertQuery = myChannelTable.insert(setters)
|
||||
let _ = try db.run(insertQuery)
|
||||
}
|
||||
}
|
||||
|
||||
public func handleThreadMentions(_ db: Connection, _ threadData: ThreadData) throws {
|
||||
let idCol = Expression<String>("id")
|
||||
let unreadMentionsCol = Expression<Int>("unread_mentions")
|
||||
|
||||
if hasThread(db, threadId: threadData.id) {
|
||||
let updateQuery = threadTable
|
||||
.where(idCol == threadData.id)
|
||||
.update(unreadMentionsCol <- threadData.unread_mentions)
|
||||
let _ = try db.run(updateQuery)
|
||||
} else {
|
||||
let lastReplyAtCol = Expression<Int64>("last_reply_at")
|
||||
let lastViewedAtCol = Expression<Int64>("last_viewed_at")
|
||||
let viewedAtCol = Expression<Int64>("viewed_at")
|
||||
let lastFetchedAtCol = Expression<Int64>("last_fetched_at")
|
||||
let isFollowingCol = Expression<Bool>("is_following")
|
||||
let unreadRepliesCol = Expression<Int>("unread_replies")
|
||||
let replyCountCol = Expression<Int>("reply_count")
|
||||
let statusCol = Expression<String>("status")
|
||||
|
||||
let setters: [Setter] = [
|
||||
idCol <- threadData.id,
|
||||
unreadMentionsCol <- threadData.unread_mentions,
|
||||
lastReplyAtCol <- threadData.last_reply_at,
|
||||
lastViewedAtCol <- threadData.last_viewed_at,
|
||||
viewedAtCol <- 0,
|
||||
lastFetchedAtCol <- 0,
|
||||
isFollowingCol <- true,
|
||||
unreadRepliesCol <- threadData.unread_replies,
|
||||
replyCountCol <- threadData.reply_count,
|
||||
statusCol <- "created"
|
||||
]
|
||||
|
||||
let insertQuery = threadTable.insert(setters)
|
||||
let _ = try db.run(insertQuery)
|
||||
}
|
||||
}
|
||||
|
||||
public func resetMyChannelMentions(_ serverUrl: String, _ channelId: String) throws {
|
||||
if let db = try? getDatabaseForServer(serverUrl) {
|
||||
let idCol = Expression<String>("id")
|
||||
|
||||
@@ -8,80 +8,6 @@
|
||||
import Foundation
|
||||
import SQLite
|
||||
|
||||
public struct Post: Codable {
|
||||
let id: String
|
||||
let create_at: Int64
|
||||
let update_at: Int64
|
||||
let edit_at: Int64
|
||||
let delete_at: Int64
|
||||
let is_pinned: Bool
|
||||
let user_id: String
|
||||
let channel_id: String
|
||||
let root_id: String
|
||||
let original_id: String
|
||||
let message: String
|
||||
let type: String
|
||||
let props: String
|
||||
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"
|
||||
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"
|
||||
// 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 {
|
||||
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) ?? "{}"
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
struct MetadataSetters {
|
||||
let metadata: String
|
||||
let reactionSetters: [[Setter]]
|
||||
@@ -104,110 +30,99 @@ struct ThreadSetters {
|
||||
}
|
||||
|
||||
extension Database {
|
||||
public func queryLastPostCreateAt(withId channelId: String, withServerUrl serverUrl: String) throws -> Int64? {
|
||||
let db = try getDatabaseForServer(serverUrl)
|
||||
|
||||
let earliestCol = Expression<Int64>("earliest")
|
||||
let latestCol = Expression<Int64>("latest")
|
||||
let channelIdCol = Expression<String>("channel_id")
|
||||
let earliestLatestQuery = postsInChannelTable
|
||||
.select(earliestCol, latestCol)
|
||||
.where(channelIdCol == channelId)
|
||||
.order(latestCol.desc)
|
||||
.limit(1)
|
||||
|
||||
var earliest: Int64?
|
||||
var latest: Int64?
|
||||
if let result = try? db.pluck(earliestLatestQuery) {
|
||||
earliest = try? result.get(earliestCol)
|
||||
latest = try? result.get(latestCol)
|
||||
} else {
|
||||
return nil
|
||||
public func queryLastPostInThread(withRootId rootId: String, forServerUrl serverUrl: String) -> Double? {
|
||||
if let db = try? getDatabaseForServer(serverUrl) {
|
||||
let createAtCol = Expression<Double>("create_at")
|
||||
let rootIdCol = Expression<String>("root_id")
|
||||
let query = postTable
|
||||
.select(createAtCol)
|
||||
.where(rootIdCol == rootId)
|
||||
.order(createAtCol.desc)
|
||||
.limit(1)
|
||||
if let result = try? db.pluck(query) {
|
||||
return try? result.get(createAtCol)
|
||||
}
|
||||
}
|
||||
|
||||
let createAtCol = Expression<Int64>("create_at")
|
||||
let deleteAtCol = Expression<Int64>("delete_at")
|
||||
var postQuery = postTable
|
||||
.select(createAtCol)
|
||||
.where(channelIdCol == channelId && deleteAtCol == 0)
|
||||
|
||||
if let earliest = earliest, let latest = latest {
|
||||
postQuery = postQuery.filter(earliest...latest ~= createAtCol)
|
||||
}
|
||||
postQuery = postQuery.order(createAtCol.desc).limit(1)
|
||||
|
||||
if let result = try db.pluck(postQuery) {
|
||||
return try result.get(createAtCol)
|
||||
return nil
|
||||
}
|
||||
|
||||
public func queryLastPostCreateAt(withId channelId: String, forServerUrl serverUrl: String) -> Double? {
|
||||
if let db = try? getDatabaseForServer(serverUrl) {
|
||||
let earliestCol = Expression<Double>("earliest")
|
||||
let latestCol = Expression<Double>("latest")
|
||||
let channelIdCol = Expression<String>("channel_id")
|
||||
let earliestLatestQuery = postsInChannelTable
|
||||
.select(earliestCol, latestCol)
|
||||
.where(channelIdCol == channelId)
|
||||
.order(latestCol.desc)
|
||||
.limit(1)
|
||||
|
||||
var earliest: Double?
|
||||
var latest: Double?
|
||||
if let result = try? db.pluck(earliestLatestQuery) {
|
||||
earliest = try? result.get(earliestCol)
|
||||
latest = try? result.get(latestCol)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let createAtCol = Expression<Double>("create_at")
|
||||
let deleteAtCol = Expression<Double>("delete_at")
|
||||
var postQuery = postTable
|
||||
.select(createAtCol)
|
||||
.where(channelIdCol == channelId && deleteAtCol == 0)
|
||||
|
||||
if let earliest = earliest, let latest = latest {
|
||||
postQuery = postQuery.filter(earliest...latest ~= createAtCol)
|
||||
}
|
||||
postQuery = postQuery.order(createAtCol.desc).limit(1)
|
||||
|
||||
if let result = try? db.pluck(postQuery) {
|
||||
return try? result.get(createAtCol)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
public func queryPostsSinceForChannel(withId channelId: String, withServerUrl serverUrl: String) throws -> Int64? {
|
||||
let db = try getDatabaseForServer(serverUrl)
|
||||
|
||||
let idCol = Expression<String>("id")
|
||||
let lastFetchedAtColAsDouble = Expression<Double?>("last_fetched_at")
|
||||
let lastFetchedAtColAsInt64 = Expression<Int64?>("last_fetched_at")
|
||||
let query = myChannelTable.where(idCol == channelId)
|
||||
|
||||
if let result = try? db.pluck(query) {
|
||||
let lastFetchAtInt64 = result[lastFetchedAtColAsInt64]
|
||||
if lastFetchAtInt64 != nil,
|
||||
lastFetchAtInt64! > 0 {
|
||||
return lastFetchAtInt64
|
||||
}
|
||||
if let last = result[lastFetchedAtColAsDouble],
|
||||
last > 0 {
|
||||
return Int64(last)
|
||||
public func queryPostsSinceForChannel(withId channelId: String, forServerUrl serverUrl: String) -> Double? {
|
||||
if let db = try? getDatabaseForServer(serverUrl) {
|
||||
let idCol = Expression<String>("id")
|
||||
let lastFetchedAtColAsDouble = Expression<Double?>("last_fetched_at")
|
||||
let query = myChannelTable.where(idCol == channelId)
|
||||
|
||||
if let result = try? db.pluck(query) {
|
||||
if let last = result[lastFetchedAtColAsDouble],
|
||||
last > 0 {
|
||||
return last
|
||||
}
|
||||
}
|
||||
|
||||
return queryLastPostCreateAt(withId: channelId, forServerUrl: serverUrl)
|
||||
}
|
||||
|
||||
return try queryLastPostCreateAt(withId: channelId, withServerUrl: serverUrl)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
private func updateMyChannelLastFetchedAt(_ db: Connection, _ channelId: String, _ latest: Int64) throws {
|
||||
let idCol = Expression<String>("id")
|
||||
let lastFetchedAtCol = Expression<Int64>("last_fetched_at")
|
||||
let statusCol = Expression<String>("_status")
|
||||
|
||||
let query = myChannelTable
|
||||
.where(idCol == channelId)
|
||||
.update(lastFetchedAtCol <- latest, statusCol <- "updated")
|
||||
|
||||
try db.run(query)
|
||||
}
|
||||
|
||||
public func handlePostData(_ db: Connection, _ postData: PostData, _ channelId: String, _ usedSince: Bool = false, _ receivingThreads: Bool = false) throws {
|
||||
public func handlePostData(_ db: Connection, _ postData: PostResponse, _ channelId: String, _ receivingThreads: Bool = false) throws {
|
||||
let sortedChainedPosts = chainAndSortPosts(postData)
|
||||
try insertOrUpdatePosts(db, sortedChainedPosts, channelId)
|
||||
let sortedAndNotDeletedPosts = sortedChainedPosts.filter({$0.delete_at == 0})
|
||||
let sortedAndNotDeletedPosts = sortedChainedPosts.filter({$0.deleteAt == 0})
|
||||
|
||||
if (!receivingThreads) {
|
||||
if !sortedAndNotDeletedPosts.isEmpty {
|
||||
let earliest = sortedAndNotDeletedPosts.first!.create_at
|
||||
let latest = sortedAndNotDeletedPosts.last!.create_at
|
||||
try handlePostsInChannel(db, channelId, earliest, latest, usedSince)
|
||||
let earliest = sortedAndNotDeletedPosts.first!.createAt
|
||||
let latest = sortedAndNotDeletedPosts.last!.createAt
|
||||
try handlePostsInChannel(db, channelId, earliest, latest)
|
||||
}
|
||||
|
||||
let lastFetchedAt = postData.posts.map({max($0.create_at, $0.update_at, $0.delete_at)}).max()
|
||||
try updateMyChannelLastFetchedAt(db, channelId, lastFetchedAt ?? 0)
|
||||
}
|
||||
try handlePostsInThread(db, postData.posts)
|
||||
try handlePostsInThread(db, Array(postData.posts.values))
|
||||
}
|
||||
|
||||
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)
|
||||
} else {
|
||||
let updated = try updatePostsInChannelEarliestAndLatest(db, channelId, earliest, latest)
|
||||
if (!updated) {
|
||||
try? insertPostsInChannel(db, channelId, earliest, latest)
|
||||
}
|
||||
private func handlePostsInChannel(_ db: Connection, _ channelId: String, _ earliest: Double, _ latest: Double) throws {
|
||||
let updated = try updatePostsInChannelEarliestAndLatest(db, channelId, earliest, latest)
|
||||
if (!updated) {
|
||||
try insertPostsInChannel(db, channelId, earliest, latest)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -219,17 +134,17 @@ extension Database {
|
||||
}
|
||||
}
|
||||
|
||||
private func chainAndSortPosts(_ postData: PostData) -> [Post] {
|
||||
private func chainAndSortPosts(_ postData: PostResponse) -> [Post] {
|
||||
let order = postData.order
|
||||
let posts = postData.posts
|
||||
let posts = Array(postData.posts.values)
|
||||
var prevPostId = ""
|
||||
|
||||
return posts.sorted(by: {$0.create_at < $1.create_at}).enumerated().map { (index, post) in
|
||||
return posts.sorted(by: {$0.createAt < $1.createAt}).enumerated().map { (index, post) in
|
||||
var modified = post
|
||||
if (index == 0) {
|
||||
modified.prev_post_id = postData.prev_post_id
|
||||
modified.prevPostId = postData.prevPostId
|
||||
} else {
|
||||
modified.prev_post_id = prevPostId
|
||||
modified.prevPostId = prevPostId
|
||||
}
|
||||
|
||||
if (order.contains(post.id)) {
|
||||
@@ -240,25 +155,11 @@ extension Database {
|
||||
}
|
||||
}
|
||||
|
||||
private func updatePostsInChannelLatestOnly(_ db: Connection, _ channelId: String, _ latest: Int64) throws {
|
||||
let channelIdCol = Expression<String>("channel_id")
|
||||
let latestCol = Expression<Int64>("latest")
|
||||
let statusCol = Expression<String>("_status")
|
||||
|
||||
let query = postsInChannelTable
|
||||
.where(channelIdCol == channelId)
|
||||
.order(latestCol.desc)
|
||||
.limit(1)
|
||||
.update(latestCol <- latest, statusCol <- "updated")
|
||||
|
||||
try db.run(query)
|
||||
}
|
||||
|
||||
private func updatePostsInChannelEarliestAndLatest(_ db: Connection, _ channelId: String, _ earliest: Int64, _ latest: Int64) throws -> Bool {
|
||||
private func updatePostsInChannelEarliestAndLatest(_ db: Connection, _ channelId: String, _ earliest: Double, _ latest: Double) throws -> Bool {
|
||||
let idCol = Expression<String>("id")
|
||||
let channelIdCol = Expression<String>("channel_id")
|
||||
let earliestCol = Expression<Int64>("earliest")
|
||||
let latestCol = Expression<Int64>("latest")
|
||||
let earliestCol = Expression<Double>("earliest")
|
||||
let latestCol = Expression<Double>("latest")
|
||||
let statusCol = Expression<String>("_status")
|
||||
|
||||
let query = postsInChannelTable
|
||||
@@ -283,11 +184,11 @@ extension Database {
|
||||
return false
|
||||
}
|
||||
|
||||
private func insertPostsInChannel(_ db: Connection, _ channelId: String, _ earliest: Int64, _ latest: Int64) throws {
|
||||
private func insertPostsInChannel(_ db: Connection, _ channelId: String, _ earliest: Double, _ latest: Double) throws {
|
||||
let idCol = Expression<String>("id")
|
||||
let channelIdCol = Expression<String>("channel_id")
|
||||
let earliestCol = Expression<Int64>("earliest")
|
||||
let latestCol = Expression<Int64>("latest")
|
||||
let earliestCol = Expression<Double>("earliest")
|
||||
let latestCol = Expression<Double>("latest")
|
||||
let statusCol = Expression<String>("_status")
|
||||
let id = generateId()
|
||||
|
||||
@@ -331,29 +232,12 @@ 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")
|
||||
let updateAt = Expression<Int64>("update_at")
|
||||
let editAt = Expression<Int64>("edit_at")
|
||||
let deleteAt = Expression<Int64>("delete_at")
|
||||
let createAt = Expression<Double>("create_at")
|
||||
let updateAt = Expression<Double>("update_at")
|
||||
let editAt = Expression<Double>("edit_at")
|
||||
let deleteAt = Expression<Double>("delete_at")
|
||||
let isPinned = Expression<Bool>("is_pinned")
|
||||
let userId = Expression<String>("user_id")
|
||||
let channelId = Expression<String>("channel_id")
|
||||
@@ -373,20 +257,20 @@ extension Database {
|
||||
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)
|
||||
setter.append(editAt <- post.edit_at)
|
||||
setter.append(deleteAt <- post.delete_at)
|
||||
setter.append(isPinned <- post.is_pinned)
|
||||
setter.append(userId <- post.user_id)
|
||||
setter.append(channelId <- post.channel_id)
|
||||
setter.append(rootId <- post.root_id)
|
||||
setter.append(originalId <- post.original_id)
|
||||
setter.append(createAt <- post.createAt)
|
||||
setter.append(updateAt <- post.updateAt)
|
||||
setter.append(editAt <- post.editAt)
|
||||
setter.append(deleteAt <- post.deleteAt)
|
||||
setter.append(isPinned <- post.isPinned)
|
||||
setter.append(userId <- post.userId)
|
||||
setter.append(channelId <- post.channelId)
|
||||
setter.append(rootId <- post.rootId)
|
||||
setter.append(originalId <- post.originalId)
|
||||
setter.append(message <- post.message)
|
||||
setter.append(metadata <- metadataSetters.metadata)
|
||||
setter.append(type <- post.type)
|
||||
setter.append(pendingPostId <- post.pending_post_id)
|
||||
setter.append(prevPostId <- post.prev_post_id)
|
||||
setter.append(pendingPostId <- post.pendingPostId)
|
||||
setter.append(prevPostId <- post.prevPostId)
|
||||
setter.append(props <- post.props)
|
||||
setter.append(statusCol <- "created")
|
||||
|
||||
@@ -408,13 +292,13 @@ extension Database {
|
||||
let userId = Expression<String>("user_id")
|
||||
let postId = Expression<String>("post_id")
|
||||
let emojiName = Expression<String>("emoji_name")
|
||||
let createAt = Expression<Int64>("create_at")
|
||||
let createAt = Expression<Double>("create_at")
|
||||
let name = Expression<String>("name")
|
||||
let ext = Expression<String>("extension")
|
||||
let size = Expression<Int64>("size")
|
||||
let size = Expression<Double>("size")
|
||||
let mimeType = Expression<String>("mime_type")
|
||||
let width = Expression<Int64>("width")
|
||||
let height = Expression<Int64>("height")
|
||||
let width = Expression<Double>("width")
|
||||
let height = Expression<Double>("height")
|
||||
let localPath = Expression<String?>("local_path")
|
||||
let imageThumbnail = Expression<String?>("image_thumbnail")
|
||||
let statusCol = Expression<String>("_status")
|
||||
@@ -435,7 +319,7 @@ extension Database {
|
||||
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(createAt <- r["create_at"] as! Double)
|
||||
reactionSetter.append(statusCol <- "created")
|
||||
|
||||
reactionSetters.append(reactionSetter)
|
||||
@@ -453,10 +337,10 @@ extension Database {
|
||||
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(size <- f["size"] as! Double)
|
||||
fileSetter.append(mimeType <- f["mime_type"] as! String)
|
||||
fileSetter.append(width <- (f["width"] as? Int64 ?? 0))
|
||||
fileSetter.append(height <- (f["height"] as? Int64 ?? 0))
|
||||
fileSetter.append(width <- (f["width"] as? Double ?? 0))
|
||||
fileSetter.append(height <- (f["height"] as? Double ?? 0))
|
||||
fileSetter.append(localPath <- "")
|
||||
fileSetter.append(imageThumbnail <- (f["mini_preview"] as? String ?? ""))
|
||||
fileSetter.append(statusCol <- "created")
|
||||
@@ -496,118 +380,52 @@ 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")
|
||||
let lastFetchAtCol = Expression<Int64>("last_fetched_at")
|
||||
|
||||
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(lastFetchAtCol <- 0)
|
||||
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]]()
|
||||
|
||||
for post in posts {
|
||||
if !post.root_id.isEmpty && post.delete_at == 0 {
|
||||
var threadPosts = postsInThread[post.root_id] ?? [Post]()
|
||||
if !post.rootId.isEmpty && post.deleteAt == 0 {
|
||||
var threadPosts = postsInThread[post.rootId] ?? [Post]()
|
||||
threadPosts.append(post)
|
||||
|
||||
postsInThread.updateValue(threadPosts, forKey: post.root_id)
|
||||
postsInThread.updateValue(threadPosts, forKey: post.rootId)
|
||||
}
|
||||
}
|
||||
|
||||
let rootIdCol = Expression<String>("root_id")
|
||||
let earliestCol = Expression<Int64>("earliest")
|
||||
let latestCol = Expression<Int64>("latest")
|
||||
let earliestCol = Expression<Double>("earliest")
|
||||
let latestCol = Expression<Double>("latest")
|
||||
let statusCol = Expression<String>("_status")
|
||||
|
||||
for (rootId, posts) in postsInThread {
|
||||
let sortedPosts = posts.sorted(by: { $0.create_at < $1.create_at })
|
||||
let earliest = sortedPosts.first!.create_at
|
||||
let latest = sortedPosts.last!.create_at
|
||||
|
||||
let query = postsInThreadTable
|
||||
.where(rootIdCol == rootId)
|
||||
.order(latestCol.desc)
|
||||
.limit(1)
|
||||
if let row = try? db.pluck(query) {
|
||||
let rowEarliest = try row.get(earliestCol)
|
||||
let rowLatest = try row.get(latestCol)
|
||||
let sortedPosts = posts.sorted(by: { $0.createAt < $1.createAt })
|
||||
if let earliest = sortedPosts.first?.createAt,
|
||||
let latest = sortedPosts.last?.createAt {
|
||||
|
||||
let updateQuery = postsInThreadTable
|
||||
.where(rootIdCol == rootId && earliestCol == rowEarliest && latestCol == rowLatest)
|
||||
.update(earliestCol <- min(earliest, rowEarliest),
|
||||
latestCol <- max(latest, rowLatest), statusCol <- "updated")
|
||||
try db.run(updateQuery)
|
||||
} else {
|
||||
var setter = [Setter]()
|
||||
setter.append(Expression<String>("id") <- generateId())
|
||||
setter.append(rootIdCol <- rootId)
|
||||
setter.append(earliestCol <- earliest)
|
||||
setter.append(latestCol <- latest)
|
||||
setter.append(statusCol <- "created")
|
||||
|
||||
setters.append(setter)
|
||||
let query = postsInThreadTable
|
||||
.where(rootIdCol == rootId)
|
||||
.order(latestCol.desc)
|
||||
.limit(1)
|
||||
if let row = try? db.pluck(query) {
|
||||
let rowEarliest = try row.get(earliestCol)
|
||||
let rowLatest = try row.get(latestCol)
|
||||
|
||||
let updateQuery = postsInThreadTable
|
||||
.where(rootIdCol == rootId && earliestCol == rowEarliest && latestCol == rowLatest)
|
||||
.update(earliestCol <- min(earliest, rowEarliest),
|
||||
latestCol <- max(latest, rowLatest), statusCol <- "updated")
|
||||
try db.run(updateQuery)
|
||||
} else {
|
||||
var setter = [Setter]()
|
||||
setter.append(Expression<String>("id") <- generateId())
|
||||
setter.append(rootIdCol <- rootId)
|
||||
setter.append(earliestCol <- earliest)
|
||||
setter.append(latestCol <- latest)
|
||||
setter.append(statusCol <- "created")
|
||||
|
||||
setters.append(setter)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import Foundation
|
||||
import SQLite
|
||||
|
||||
extension Database {
|
||||
public func getTeammateDisplayNameSetting(_ serverUrl: String) -> String {
|
||||
do {
|
||||
if let displayName = geConfigDisplayNameSetting(serverUrl) {
|
||||
return displayName
|
||||
}
|
||||
|
||||
let db = try getDatabaseForServer(serverUrl)
|
||||
let category = Expression<String>("category")
|
||||
let name = Expression<String>("name")
|
||||
let value = Expression<String>("value")
|
||||
let query = preferenceTable.select(value).filter(category == "display_settings" && name == "name_format")
|
||||
if let result = try db.pluck(query) {
|
||||
let val = try result.get(value)
|
||||
return val
|
||||
}
|
||||
} catch {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
return "username"
|
||||
}
|
||||
}
|
||||
@@ -11,19 +11,41 @@ import SQLite
|
||||
|
||||
extension Database {
|
||||
public func getConfig(_ serverUrl: String, _ key: String) -> String? {
|
||||
do {
|
||||
let db = try getDatabaseForServer(serverUrl)
|
||||
if let db = try? getDatabaseForServer(serverUrl) {
|
||||
let id = Expression<String>("id")
|
||||
let value = Expression<String>("value")
|
||||
let query = configTable.select(value).filter(id == key)
|
||||
if let result = try db.pluck(query) {
|
||||
let val = try result.get(value)
|
||||
return val
|
||||
if let result = try? db.pluck(query) {
|
||||
return try? result.get(value)
|
||||
}
|
||||
|
||||
return nil
|
||||
} catch {
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
public func getLicense(_ serverUrl: String) -> String? {
|
||||
if let db = try? getDatabaseForServer(serverUrl) {
|
||||
let id = Expression<String>("id")
|
||||
let value = Expression<String>("value")
|
||||
let query = systemTable.select(value).filter(id == "license")
|
||||
if let result = try? db.pluck(query) {
|
||||
return try? result.get(value)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
public func geConfigDisplayNameSetting(_ serverUrl: String) -> String? {
|
||||
let licenseValue = getLicense(serverUrl)
|
||||
guard let licenseData = licenseValue?.data(using: .utf8),
|
||||
let license = try? JSONSerialization.jsonObject(with: licenseData) as? Dictionary<String,String>,
|
||||
let lockDisplayName = getConfig(serverUrl, "LockTeammateNameDisplay")
|
||||
else { return nil }
|
||||
|
||||
let displayName = getConfig(serverUrl, "TeammateNameDisplay") ?? "full_name"
|
||||
let licenseLock = license["LockTeammateNameDisplay"] ?? "false"
|
||||
if licenseLock == "true" && lockDisplayName == "true" {
|
||||
return displayName
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,13 +2,12 @@ import Foundation
|
||||
import SQLite
|
||||
|
||||
extension Database {
|
||||
internal func queryCurrentTeamId(_ serverUrl: String) -> String? {
|
||||
public func queryCurrentTeamId(_ serverUrl: String) -> String? {
|
||||
if let db = try? getDatabaseForServer(serverUrl) {
|
||||
let idCol = Expression<String>("id")
|
||||
let valueCol = Expression<String>("value")
|
||||
let query = systemTable.where(idCol == "currentTeamId")
|
||||
|
||||
if let result = try? db.pluck(query) {
|
||||
if let result = try? db.pluck(systemTable.where(idCol == "currentTeamId")) {
|
||||
return try? result.get(valueCol).replacingOccurrences(of: "\"", with: "")
|
||||
}
|
||||
}
|
||||
@@ -16,7 +15,7 @@ extension Database {
|
||||
return nil
|
||||
}
|
||||
|
||||
public func queryTeamIdForChannel(withId channelId: String, withServerUrl serverUrl: String) -> String? {
|
||||
public func queryTeamIdForChannel(withId channelId: String, forServerUrl serverUrl: String) -> String? {
|
||||
if let db = try? getDatabaseForServer(serverUrl) {
|
||||
let idCol = Expression<String>("id")
|
||||
let teamIdCol = Expression<String?>("team_id")
|
||||
@@ -33,4 +32,106 @@ extension Database {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
public func queryTeamExists(withId teamId: String, forServerUrl serverUrl: String) -> Bool {
|
||||
if let db = try? getDatabaseForServer(serverUrl) {
|
||||
let idCol = Expression<String>("id")
|
||||
let query = teamTable.where(idCol == teamId)
|
||||
if let _ = try? db.pluck(query) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
public func queryMyTeamExists(withId teamId: String, forServerUrl serverUrl: String) -> Bool {
|
||||
if let db = try? getDatabaseForServer(serverUrl) {
|
||||
let idCol = Expression<String>("id")
|
||||
let query = myTeamTable.where(idCol == teamId)
|
||||
if let _ = try? db.pluck(query) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
public func queryAllMyTeamIds(_ serverUrl: String) -> [String]? {
|
||||
if let db = try? getDatabaseForServer(serverUrl) {
|
||||
let idCol = Expression<String>("id")
|
||||
if let myTeams = try? db.prepare(myTeamTable.select(idCol)) {
|
||||
return myTeams.map { try! $0.get(idCol) }
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
public func insertTeam(_ db: Connection, _ team: Team) throws {
|
||||
let setter = createTeamSetter(from: team)
|
||||
let insertQuery = teamTable.insert(or: .replace, setter)
|
||||
try db.run(insertQuery)
|
||||
}
|
||||
|
||||
public func insertMyTeam(_ db: Connection, _ member: TeamMember) throws {
|
||||
let myTeam = createMyTeamSetter(from: member)
|
||||
let teamMember = createTeamMemberSetter(from: member)
|
||||
try db.run(myTeamTable.insert(or: .replace, myTeam))
|
||||
try db.run(teamMembershipTable.insert(or: .replace, teamMember))
|
||||
}
|
||||
|
||||
private func createTeamSetter(from team: Team) -> [Setter] {
|
||||
let id = Expression<String>("id")
|
||||
let isAllowOpenInvite = Expression<Bool>("is_allow_open_invite")
|
||||
let updateAt = Expression<Double>("update_at")
|
||||
let description = Expression<String>("description")
|
||||
let displayName = Expression<String>("display_name")
|
||||
let isGroupeConstrained = Expression<Bool>("is_group_constrained")
|
||||
let lastTeamIconUpdatedAt = Expression<Double>("last_team_icon_updated_at")
|
||||
let name = Expression<String>("name")
|
||||
let type = Expression<String>("type")
|
||||
let allowedDomains = Expression<String>("allowed_domains")
|
||||
let inviteId = Expression<String>("invite_id")
|
||||
|
||||
let setter: [Setter] = [
|
||||
id <- team.id,
|
||||
isAllowOpenInvite <- team.allowOpenInvite,
|
||||
updateAt <- team.updateAt,
|
||||
description <- team.description,
|
||||
displayName <- team.displayName,
|
||||
isGroupeConstrained <- team.groupConstrained,
|
||||
lastTeamIconUpdatedAt <- team.lastTeamIconUpdate,
|
||||
name <- team.name,
|
||||
type <- team.type,
|
||||
allowedDomains <- team.allowedDomains,
|
||||
inviteId <- team.inviteId,
|
||||
]
|
||||
return setter
|
||||
}
|
||||
|
||||
private func createMyTeamSetter(from member: TeamMember) -> [Setter] {
|
||||
let id = Expression<String>("id")
|
||||
let roles = Expression<String>("roles")
|
||||
|
||||
var setter = [Setter]()
|
||||
setter.append(id <- member.id)
|
||||
setter.append(roles <- member.roles)
|
||||
|
||||
return setter
|
||||
}
|
||||
|
||||
private func createTeamMemberSetter(from member: TeamMember) -> [Setter] {
|
||||
let id = Expression<String>("id")
|
||||
let teamId = Expression<String>("team_id")
|
||||
let userId = Expression<String>("user_id")
|
||||
let schemeAdmin = Expression<Bool>("scheme_admin")
|
||||
|
||||
let setter: [Setter] = [
|
||||
id <- "\(member.id)-\(member.userId)",
|
||||
teamId <- member.id,
|
||||
userId <- member.userId,
|
||||
schemeAdmin <- member.schemeAdmin,
|
||||
]
|
||||
|
||||
return setter
|
||||
}
|
||||
}
|
||||
|
||||
188
ios/Gekidou/Sources/Gekidou/Storage/Database+Thread.swift
Normal file
188
ios/Gekidou/Sources/Gekidou/Storage/Database+Thread.swift
Normal file
@@ -0,0 +1,188 @@
|
||||
import Foundation
|
||||
import SQLite
|
||||
|
||||
extension Database {
|
||||
public func hasThread(_ db: Connection, threadId: String) -> Bool {
|
||||
let idCol = Expression<String>("id")
|
||||
let query = threadTable.where(idCol == threadId)
|
||||
if let _ = try? db.pluck(query) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
public func getTeamThreadSync(_ db: Connection, teamId: String) -> Row? {
|
||||
let idCol = Expression<String>("id")
|
||||
let query = teamThreadsSyncTable.where(idCol == teamId)
|
||||
return try? db.pluck(query)
|
||||
}
|
||||
|
||||
public func handleThreads(_ db: Connection, _ threads: [PostThread], forTeamId teamId: String) throws {
|
||||
var teamIds = [String]()
|
||||
if teamId.isEmpty {
|
||||
let idCol = Expression<String>("id")
|
||||
if let myTeams = try? db.prepare(myTeamTable.select(idCol)) {
|
||||
if let ids = try? myTeams.map({ try $0.get(idCol) }) {
|
||||
teamIds.append(contentsOf: ids)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
teamIds.append(teamId)
|
||||
}
|
||||
|
||||
for thread in threads {
|
||||
handleThread(db, thread, forTeamIds: teamIds)
|
||||
}
|
||||
|
||||
handleTeamThreadSync(db, threads, forTeamIds: teamIds)
|
||||
}
|
||||
|
||||
public func handleThread(_ db: Connection, _ thread: PostThread, forTeamIds teamIds: [String]) {
|
||||
if hasThread(db, threadId: thread.id) {
|
||||
try? updateThread(db, thread)
|
||||
} else {
|
||||
try? insertThread(db, thread)
|
||||
}
|
||||
|
||||
try? syncParticipants(db, thread)
|
||||
|
||||
if thread.isFollowing {
|
||||
for teamId in teamIds {
|
||||
try? handleThreadInTeam(db, thread, forTeamId: teamId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func insertThread(_ db: Connection, _ thread: PostThread) throws {
|
||||
let id = Expression<String>("id")
|
||||
let isFollowing = Expression<Bool>("is_following")
|
||||
let lastViewedAt = Expression<Double>("last_viewed_at")
|
||||
let lastReplyAt = Expression<Double>("last_reply_at")
|
||||
let unreadReplies = Expression<Int>("unread_replies")
|
||||
let unreadMentions = Expression<Int>("unread_mentions")
|
||||
let replyCount = Expression<Int>("reply_count")
|
||||
|
||||
let setter: [Setter] = [
|
||||
id <- thread.id,
|
||||
isFollowing <- thread.isFollowing,
|
||||
lastViewedAt <- thread.lastViewedAt,
|
||||
lastReplyAt <- thread.lastReplyAt,
|
||||
unreadReplies <- thread.unreadReplies,
|
||||
unreadMentions <- thread.unreadMentions,
|
||||
replyCount <- thread.replyCount
|
||||
]
|
||||
|
||||
let _ = try db.run(threadTable.insert(or: .replace, setter))
|
||||
}
|
||||
|
||||
private func updateThread(_ db: Connection, _ thread: PostThread) throws {
|
||||
let id = Expression<String>("id")
|
||||
let isFollowing = Expression<Bool>("is_following")
|
||||
let lastViewedAt = Expression<Double>("last_viewed_at")
|
||||
let lastReplyAt = Expression<Double>("last_reply_at")
|
||||
let unreadReplies = Expression<Int>("unread_replies")
|
||||
let unreadMentions = Expression<Int>("unread_mentions")
|
||||
let replyCount = Expression<Int>("reply_count")
|
||||
|
||||
let setter: [Setter] = [
|
||||
isFollowing <- thread.isFollowing,
|
||||
lastViewedAt <- thread.lastViewedAt,
|
||||
lastReplyAt <- thread.lastReplyAt,
|
||||
unreadReplies <- thread.unreadReplies,
|
||||
unreadMentions <- thread.unreadMentions,
|
||||
replyCount <- thread.replyCount
|
||||
]
|
||||
|
||||
let _ = try db.run(threadTable.where(id == thread.id).update(setter))
|
||||
}
|
||||
|
||||
private func syncParticipants(_ db: Connection, _ thread: PostThread) throws {
|
||||
let threadIdCol = Expression<String>("thread_id")
|
||||
let deletePreviousThreadParticipants = threadParticipantTable.where(threadIdCol == thread.id).delete()
|
||||
try db.run(deletePreviousThreadParticipants)
|
||||
|
||||
let setters = createThreadParticipantSetters(from: thread)
|
||||
if !setters.isEmpty {
|
||||
try db.run(threadParticipantTable.insertMany(setters))
|
||||
}
|
||||
}
|
||||
|
||||
private func handleThreadInTeam(_ db: Connection, _ thread: PostThread, forTeamId teamId: String) throws {
|
||||
let idCol = Expression<String>("id")
|
||||
let threadIdCol = Expression<String>("thread_id")
|
||||
let teamIdCol = Expression<String>("team_id")
|
||||
let existing = try? db.pluck(threadsInTeamTable.where(threadIdCol == thread.id && teamIdCol == teamId))
|
||||
if existing == nil {
|
||||
let setter: [Setter] = [
|
||||
idCol <- generateId(),
|
||||
threadIdCol <- thread.id,
|
||||
teamIdCol <- teamId,
|
||||
]
|
||||
let _ = try db.run(threadsInTeamTable.insert(or: .replace, setter))
|
||||
}
|
||||
}
|
||||
|
||||
private func handleTeamThreadSync(_ db: Connection, _ threads: [PostThread], forTeamIds teamIds: [String]) {
|
||||
let sortedList = threads.filter({ $0.isFollowing }).sorted(by: { $0.lastReplyAt < $1.lastReplyAt}).map{ $0.lastReplyAt }
|
||||
if let earliest = sortedList.first,
|
||||
let latest = sortedList.last {
|
||||
for teamId in teamIds {
|
||||
if let existing = getTeamThreadSync(db, teamId: teamId) {
|
||||
try? updateTeamThreadSync(db, forTeamId: teamId, starting: earliest, ending: latest, currentRow: existing)
|
||||
} else {
|
||||
try? insertTeamThreadSync(db, forTeamId: teamId, starting: earliest, ending: latest)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func insertTeamThreadSync(_ db: Connection, forTeamId teamId: String, starting earliest: Double, ending latest: Double) throws {
|
||||
let idCol = Expression<String>("id")
|
||||
let earliestCol = Expression<Double>("earliest")
|
||||
let latestCol = Expression<Double>("latest")
|
||||
|
||||
let setter: [Setter] = [
|
||||
idCol <- teamId,
|
||||
earliestCol <- earliest,
|
||||
latestCol <- latest,
|
||||
]
|
||||
|
||||
let _ = try db.run(teamThreadsSyncTable.insert(or: .replace, setter))
|
||||
}
|
||||
|
||||
private func updateTeamThreadSync(_ db: Connection, forTeamId teamId: String, starting earliest: Double, ending latest: Double, currentRow existing: Row) throws {
|
||||
let idCol = Expression<String>("id")
|
||||
let earliestCol = Expression<Double>("earliest")
|
||||
let latestCol = Expression<Double>("latest")
|
||||
|
||||
let existingEarliest = (try? existing.get(earliestCol)) ?? 0
|
||||
let storeEarliest = min(earliest, existingEarliest)
|
||||
|
||||
let existingLatest = (try? existing.get(latestCol)) ?? 0
|
||||
let storeLatest = max(latest, existingLatest)
|
||||
|
||||
let _ = try db.run(
|
||||
teamThreadsSyncTable.where(idCol == teamId)
|
||||
.update(earliestCol <- storeEarliest, latestCol <- storeLatest)
|
||||
)
|
||||
}
|
||||
|
||||
private func createThreadParticipantSetters(from thread: PostThread) -> [[Setter]] {
|
||||
var participantSetters = [[Setter]]()
|
||||
|
||||
let id = Expression<String>("id")
|
||||
let userId = Expression<String>("user_id")
|
||||
let threadId = Expression<String>("thread_id")
|
||||
|
||||
for p in thread.participants {
|
||||
var participantSetter = [Setter]()
|
||||
participantSetter.append(id <- generateId() as String)
|
||||
participantSetter.append(userId <- p.id)
|
||||
participantSetter.append(threadId <- thread.id)
|
||||
participantSetters.append(participantSetter)
|
||||
}
|
||||
|
||||
return participantSetters
|
||||
}
|
||||
}
|
||||
@@ -9,90 +9,19 @@
|
||||
import Foundation
|
||||
import SQLite
|
||||
|
||||
public struct User: Codable, Hashable {
|
||||
let id: String
|
||||
let auth_service: String
|
||||
let update_at: Double
|
||||
let delete_at: Double
|
||||
let email: String
|
||||
let first_name: String
|
||||
let is_bot: Bool
|
||||
let is_guest: Bool
|
||||
let last_name: String
|
||||
let last_picture_update: Double
|
||||
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.decodeIfPresent(Double.self, forKey: .update_at)) ?? 0
|
||||
delete_at = (try? container.decodeIfPresent(Double.self, forKey: .delete_at)) ?? 0
|
||||
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.decodeIfPresent(Double.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)
|
||||
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 = "{}"
|
||||
extension Database {
|
||||
public func getUserFromRow(_ row: Row) -> User? {
|
||||
do {
|
||||
let decoder = row.decoder()
|
||||
let _ = try decoder.container(keyedBy: User.UserKeys.self)
|
||||
return try User(from: decoder)
|
||||
} catch {
|
||||
print(error.localizedDescription)
|
||||
}
|
||||
|
||||
let timezoneData = try? container.decodeIfPresent([String: String].self, forKey: .timezone)
|
||||
if (timezoneData != nil) {
|
||||
timezone = Database.default.json(from: timezoneData) ?? "{}"
|
||||
} else {
|
||||
timezone = "{}"
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
extension Database {
|
||||
public func queryCurrentUserId(_ serverUrl: String) throws -> String {
|
||||
let db = try getDatabaseForServer(serverUrl)
|
||||
|
||||
@@ -120,8 +49,17 @@ extension Database {
|
||||
throw DatabaseError.NoResults(query.expression.description)
|
||||
}
|
||||
|
||||
public func getUserLastPictureAt(for userId: String, withServerUrl serverUrl: String) -> Double? {
|
||||
let idCol = Expression<String>("id")
|
||||
public func getCurrentUserLocale(_ serverUrl: String) -> String {
|
||||
if let user = try? queryCurrentUser(serverUrl) {
|
||||
if let locale = try? user.get(Expression<String>("locale")) {
|
||||
return locale
|
||||
}
|
||||
}
|
||||
|
||||
return "en"
|
||||
}
|
||||
|
||||
public func getUserLastPictureAt(for userId: String, forServerUrl serverUrl: String) -> Double? {
|
||||
var updateAt: Double?
|
||||
do {
|
||||
let db = try getDatabaseForServer(serverUrl)
|
||||
@@ -129,7 +67,7 @@ extension Database {
|
||||
let stmtString = "SELECT * FROM User WHERE id='\(userId)'"
|
||||
|
||||
let results: [User] = try db.prepareRowIterator(stmtString).map {try $0.decode()}
|
||||
updateAt = results.first?.last_picture_update
|
||||
updateAt = results.first?.lastPictureUpdate
|
||||
|
||||
} catch {
|
||||
return nil
|
||||
@@ -138,51 +76,56 @@ extension Database {
|
||||
return updateAt
|
||||
}
|
||||
|
||||
public func queryUsers(byIds: Set<String>, withServerUrl: String) throws -> Set<String> {
|
||||
let db = try getDatabaseForServer(withServerUrl)
|
||||
|
||||
public func queryUsers(byIds userIds: Set<String>, forServerUrl serverUrl: String) -> Set<String> {
|
||||
var result: Set<String> = Set()
|
||||
let idCol = Expression<String>("id")
|
||||
for user in try db.prepare(
|
||||
userTable.select(idCol).filter(byIds.contains(idCol))
|
||||
) {
|
||||
result.insert(user[idCol])
|
||||
if let db = try? getDatabaseForServer(serverUrl) {
|
||||
|
||||
let idCol = Expression<String>("id")
|
||||
if let users = try? db.prepare(
|
||||
userTable.select(idCol).filter(userIds.contains(idCol))
|
||||
) {
|
||||
for user in users {
|
||||
result.insert(user[idCol])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
public func queryUsers(byUsernames: Set<String>, withServerUrl: String) throws -> Set<String> {
|
||||
let db = try getDatabaseForServer(withServerUrl)
|
||||
|
||||
public func queryUsers(byUsernames usernames: Set<String>, forServerUrl serverUrl: String) -> Set<String> {
|
||||
var result: Set<String> = Set()
|
||||
let usernameCol = Expression<String>("username")
|
||||
for user in try db.prepare(
|
||||
userTable.select(usernameCol).filter(byUsernames.contains(usernameCol))
|
||||
) {
|
||||
result.insert(user[usernameCol])
|
||||
if let db = try? getDatabaseForServer(serverUrl) {
|
||||
let usernameCol = Expression<String>("username")
|
||||
if let users = try? db.prepare(
|
||||
userTable.select(usernameCol).filter(usernames.contains(usernameCol))
|
||||
) {
|
||||
for user in users {
|
||||
result.insert(user[usernameCol])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
public func insertUsers(_ db: Connection, _ users: Set<User>) throws {
|
||||
let setters = createUserSettedrs(from: users)
|
||||
public func insertUsers(_ db: Connection, _ users: [User]) throws {
|
||||
let setters = createUserSetters(from: users)
|
||||
let insertQuery = userTable.insertMany(or: .replace, setters)
|
||||
try db.run(insertQuery)
|
||||
}
|
||||
|
||||
private func createUserSettedrs(from users: Set<User>) -> [[Setter]] {
|
||||
private func createUserSetters(from users: [User]) -> [[Setter]] {
|
||||
let id = Expression<String>("id")
|
||||
let authService = Expression<String>("auth_service")
|
||||
let updateAt = Expression<Int64>("update_at")
|
||||
let deleteAt = Expression<Int64>("delete_at")
|
||||
let updateAt = Expression<Double>("update_at")
|
||||
let deleteAt = Expression<Double>("delete_at")
|
||||
let email = Expression<String>("email")
|
||||
let firstName = Expression<String>("first_name")
|
||||
let isBot = Expression<Bool>("is_bot")
|
||||
let isGuest = Expression<Bool>("is_guest")
|
||||
let lastName = Expression<String>("last_name")
|
||||
let lastPictureUpdate = Expression<Int64>("last_picture_update")
|
||||
let lastPictureUpdate = Expression<Double>("last_picture_update")
|
||||
let locale = Expression<String>("locale")
|
||||
let nickname = Expression<String>("nickname")
|
||||
let position = Expression<String>("position")
|
||||
@@ -197,22 +140,22 @@ extension Database {
|
||||
for user in users {
|
||||
var setter = [Setter]()
|
||||
setter.append(id <- user.id)
|
||||
setter.append(authService <- user.auth_service)
|
||||
setter.append(updateAt <- Int64(user.update_at))
|
||||
setter.append(deleteAt <- Int64(user.delete_at))
|
||||
setter.append(authService <- user.authService)
|
||||
setter.append(updateAt <- user.updateAt)
|
||||
setter.append(deleteAt <- user.deleteAt)
|
||||
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 <- Int64(user.last_picture_update))
|
||||
setter.append(firstName <- user.firstName)
|
||||
setter.append(isBot <- user.isBot)
|
||||
setter.append(isGuest <- user.isGuest)
|
||||
setter.append(lastName <- user.lastName)
|
||||
setter.append(lastPictureUpdate <- user.lastPictureUpdate)
|
||||
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(notifyProps <- user.notifyProps)
|
||||
setter.append(props <- user.props)
|
||||
setter.append(timezone <- user.timezone)
|
||||
setters.append(setter)
|
||||
|
||||
@@ -43,6 +43,7 @@ public class Database: NSObject {
|
||||
internal var systemTable = Table("System")
|
||||
internal var teamTable = Table("Team")
|
||||
internal var myTeamTable = Table("MyTeam")
|
||||
internal var teamMembershipTable = Table("TeamMembership")
|
||||
internal var channelTable = Table("Channel")
|
||||
internal var channelInfoTable = Table("ChannelInfo")
|
||||
internal var channelMembershipTable = Table("ChannelMembership")
|
||||
@@ -58,7 +59,12 @@ public class Database: NSObject {
|
||||
internal var userTable = Table("User")
|
||||
internal var threadTable = Table("Thread")
|
||||
internal var threadParticipantTable = Table("ThreadParticipant")
|
||||
internal var threadsInTeamTable = Table("ThreadsInTeam")
|
||||
internal var teamThreadsSyncTable = Table("TeamThreadsSync")
|
||||
internal var configTable = Table("Config")
|
||||
internal var preferenceTable = Table("Preference")
|
||||
internal var categoryTable = Table("Category")
|
||||
internal var categoryChannelTable = Table("CategoryChannel")
|
||||
|
||||
@objc public static let `default` = Database()
|
||||
|
||||
|
||||
16
ios/Gekidou/Sources/Gekidou/String+Extensions.swift
Normal file
16
ios/Gekidou/Sources/Gekidou/String+Extensions.swift
Normal file
@@ -0,0 +1,16 @@
|
||||
import Foundation
|
||||
|
||||
extension String {
|
||||
func removePrefix(_ prefix: String) -> String {
|
||||
guard self.hasPrefix(prefix) else { return self }
|
||||
return String(self.dropFirst(prefix.count))
|
||||
}
|
||||
|
||||
func toUrlSafeBase64Encode() -> String {
|
||||
return Data(
|
||||
self.replacingOccurrences(of: "/\\+/g", with: "-", options: .regularExpression)
|
||||
.replacingOccurrences(of: "/\\//g", with: "_", options: .regularExpression)
|
||||
.utf8
|
||||
).base64EncodedString()
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,14 @@ import Gekidou
|
||||
@objc public static let `default` = GekidouWrapper()
|
||||
|
||||
@objc func postNotificationReceipt(_ userInfo: [AnyHashable:Any]) {
|
||||
Network.default.postNotificationReceipt(userInfo)
|
||||
PushNotification.default.postNotificationReceipt(userInfo)
|
||||
}
|
||||
|
||||
@objc func fetchDataForPushNotification(_ notification: [AnyHashable:Any], withContentHandler contentHander: @escaping ((_ data: Data?) -> Void)) {
|
||||
PushNotification.default.fetchDataForPushNotification(notification, withContentHandler: { data in
|
||||
let jsonData = try? JSONEncoder().encode(data)
|
||||
contentHander(jsonData)
|
||||
})
|
||||
}
|
||||
|
||||
@objc func attachSession(_ id: String, completionHandler: @escaping () -> Void) {
|
||||
|
||||
@@ -48,6 +48,7 @@ NSString* const NOTIFICATION_TEST_ACTION = @"test";
|
||||
}
|
||||
|
||||
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error: nil];
|
||||
[[GekidouWrapper default] setPreference:@"true" forKey:@"ApplicationIsRunning"];
|
||||
|
||||
[RNNotifications startMonitorNotifications];
|
||||
|
||||
@@ -91,10 +92,22 @@ NSString* const NOTIFICATION_TEST_ACTION = @"test";
|
||||
// When rootId is nil, clear channel's root post notifications or else clear all thread notifications
|
||||
[[NotificationHelper default] clearChannelOrThreadNotificationsWithUserInfo:userInfo];
|
||||
[[GekidouWrapper default] postNotificationReceipt:userInfo];
|
||||
[RNNotifications didReceiveBackgroundNotification:userInfo withCompletionHandler:completionHandler];
|
||||
return;
|
||||
}
|
||||
|
||||
if (state != UIApplicationStateActive || isClearAction) {
|
||||
[RNNotifications didReceiveBackgroundNotification:userInfo withCompletionHandler:completionHandler];
|
||||
if (state != UIApplicationStateActive) {
|
||||
[[GekidouWrapper default] fetchDataForPushNotification:userInfo withContentHandler:^(NSData * _Nullable data) {
|
||||
NSMutableDictionary *notification = [userInfo mutableCopy];
|
||||
NSError *jsonError;
|
||||
if (data != nil) {
|
||||
id json = [NSJSONSerialization JSONObjectWithData:data options:NULL error:&jsonError];
|
||||
if (!jsonError) {
|
||||
[notification setObject:json forKey:@"data"];
|
||||
}
|
||||
}
|
||||
[RNNotifications didReceiveBackgroundNotification:notification withCompletionHandler:completionHandler];
|
||||
}];
|
||||
} else {
|
||||
completionHandler(UIBackgroundFetchResultNewData);
|
||||
}
|
||||
@@ -130,6 +143,7 @@ NSString* const NOTIFICATION_TEST_ACTION = @"test";
|
||||
|
||||
-(void)applicationWillTerminate:(UIApplication *)application {
|
||||
[[GekidouWrapper default] setPreference:@"false" forKey:@"ApplicationIsForeground"];
|
||||
[[GekidouWrapper default] setPreference:@"false" forKey:@"ApplicationIsRunning"];
|
||||
}
|
||||
|
||||
- (UIInterfaceOrientationMask)application:(UIApplication *)application supportedInterfaceOrientationsForWindow:(UIWindow *)window {
|
||||
|
||||
@@ -91,7 +91,7 @@ class ShareViewModel: ObservableObject {
|
||||
return
|
||||
}
|
||||
|
||||
Gekidou.Network.default.fetchUserProfilePicture(userId: userId, lastUpdateAt: 0, withServerUrl: serverUrl, completionHandler: {data, response, error in
|
||||
Gekidou.Network.default.fetchUserProfilePicture(userId: userId, lastUpdateAt: 0, forServerUrl: serverUrl, completionHandler: {data, response, error in
|
||||
guard (response as? HTTPURLResponse)?.statusCode == 200 else {
|
||||
debugPrint("Error while fetching image \(String(describing: (response as? HTTPURLResponse)?.statusCode))")
|
||||
return
|
||||
|
||||
@@ -4,12 +4,8 @@ import Intents
|
||||
import os.log
|
||||
|
||||
class NotificationService: UNNotificationServiceExtension {
|
||||
let preferences = Gekidou.Preferences.default
|
||||
let fibonacciBackoffsInSeconds = [1.0, 2.0, 3.0, 5.0, 8.0]
|
||||
var contentHandler: ((UNNotificationContent) -> Void)?
|
||||
var bestAttemptContent: UNMutableNotificationContent?
|
||||
|
||||
var retryIndex = 0
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
@@ -20,68 +16,66 @@ class NotificationService: UNNotificationServiceExtension {
|
||||
self.contentHandler = contentHandler
|
||||
|
||||
bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)
|
||||
if let bestAttemptContent = bestAttemptContent,
|
||||
let jsonData = try? JSONSerialization.data(withJSONObject: bestAttemptContent.userInfo),
|
||||
let ackNotification = try? JSONDecoder().decode(AckNotification.self, from: jsonData) {
|
||||
fetchReceipt(ackNotification)
|
||||
} else {
|
||||
os_log(OSLogType.default, "Mattermost Notifications: bestAttemptContent seems to be empty, will call sendMessageIntent")
|
||||
sendMessageIntent(notification: request.content)
|
||||
}
|
||||
}
|
||||
|
||||
func processResponse(serverUrl: String, data: Data, bestAttemptContent: UNMutableNotificationContent) {
|
||||
bestAttemptContent.userInfo["server_url"] = serverUrl
|
||||
os_log(
|
||||
OSLogType.default,
|
||||
"Mattermost Notifications: process receipt response for serverUrl %{public}@",
|
||||
serverUrl
|
||||
)
|
||||
let json = try? JSONSerialization.jsonObject(with: data) as! [String: Any]
|
||||
os_log(
|
||||
OSLogType.default,
|
||||
"Mattermost Notifications: parsed json response %{public}@",
|
||||
String(describing: json != nil)
|
||||
)
|
||||
if let json = json {
|
||||
if let message = json["message"] as? String {
|
||||
bestAttemptContent.body = message
|
||||
}
|
||||
if let channelName = json["channel_name"] as? String {
|
||||
bestAttemptContent.title = channelName
|
||||
}
|
||||
|
||||
let userInfoKeys = ["channel_name", "team_id", "sender_id", "sender_name", "root_id", "override_username", "override_icon_url", "from_webhook", "message"]
|
||||
for key in userInfoKeys {
|
||||
if let value = json[key] as? String {
|
||||
bestAttemptContent.userInfo[key] = value
|
||||
if let bestAttemptContent = bestAttemptContent {
|
||||
PushNotification.default.postNotificationReceipt(bestAttemptContent, completionHandler: {[weak self] notification in
|
||||
if let notification = notification {
|
||||
self?.bestAttemptContent = notification
|
||||
if (Gekidou.Preferences.default.object(forKey: "ApplicationIsRunning") as? String != "true") {
|
||||
PushNotification.default.fetchAndStoreDataForPushNotification(bestAttemptContent, withContentHandler: {notification in
|
||||
os_log(OSLogType.default, "Mattermost Notifications: processed data for db. Will call sendMessageIntent")
|
||||
self?.sendMessageIntent()
|
||||
})
|
||||
} else {
|
||||
bestAttemptContent.badge = Gekidou.Database.default.getTotalMentions() as NSNumber
|
||||
os_log(OSLogType.default, "Mattermost Notifications: app in the foreground, no data processed. Will call sendMessageIntent")
|
||||
self?.sendMessageIntent()
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (preferences.object(forKey: "ApplicationIsForeground") as? String != "true") {
|
||||
Network.default.fetchAndStoreDataForPushNotification(bestAttemptContent, withContentHandler: {[weak self] notification in
|
||||
os_log(OSLogType.default, "Mattermost Notifications: processed data for db. Will call sendMessageIntent")
|
||||
self?.sendMessageIntent(notification: bestAttemptContent)
|
||||
|
||||
os_log(OSLogType.default, "Mattermost Notifications: notification receipt seems to be empty, will call sendMessageIntent")
|
||||
self?.sendMessageIntent()
|
||||
})
|
||||
} else {
|
||||
bestAttemptContent.badge = Gekidou.Database.default.getTotalMentions() as NSNumber
|
||||
os_log(OSLogType.default, "Mattermost Notifications: app in the foreground, no data processed. Will call sendMessageIntent")
|
||||
sendMessageIntent(notification: bestAttemptContent)
|
||||
os_log(OSLogType.default, "Mattermost Notifications: bestAttemptContent seems to be empty, will call sendMessageIntent")
|
||||
sendMessageIntent()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override func serviceExtensionTimeWillExpire() {
|
||||
// Called just before the extension will be terminated by the system.
|
||||
// Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used.
|
||||
os_log(OSLogType.default, "Mattermost Notifications: service extension time expired")
|
||||
if let bestAttemptContent = bestAttemptContent {
|
||||
os_log(OSLogType.default, "Mattermost Notifications: calling sendMessageIntent before expiration")
|
||||
sendMessageIntent(notification: bestAttemptContent)
|
||||
os_log(OSLogType.default, "Mattermost Notifications: calling sendMessageIntent before expiration")
|
||||
sendMessageIntent()
|
||||
}
|
||||
|
||||
private func sendMessageIntent() {
|
||||
guard let notification = bestAttemptContent else { return }
|
||||
if #available(iOSApplicationExtension 15.0, *) {
|
||||
let overrideUsername = notification.userInfo["override_username"] as? String
|
||||
let senderId = notification.userInfo["sender_id"] as? String
|
||||
let sender = overrideUsername ?? senderId
|
||||
|
||||
guard let serverUrl = notification.userInfo["server_url"] as? String,
|
||||
let sender = sender
|
||||
else {
|
||||
os_log(OSLogType.default, "Mattermost Notifications: No intent created. will call contentHandler to present notification")
|
||||
self.contentHandler?(notification)
|
||||
return
|
||||
}
|
||||
|
||||
let overrideIconUrl = notification.userInfo["override_icon_url"] as? String
|
||||
os_log(OSLogType.default, "Mattermost Notifications: Fetching profile Image in server %{public}@ for sender %{public}@", serverUrl, sender)
|
||||
|
||||
PushNotification.default.fetchProfileImageSync(serverUrl, senderId: sender, overrideIconUrl: overrideIconUrl) {[weak self] data in
|
||||
self?.sendMessageIntentCompletion(data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func sendMessageIntentCompletion(_ notification: UNNotificationContent, _ avatarData: Data?) {
|
||||
private func sendMessageIntentCompletion(_ avatarData: Data?) {
|
||||
guard let notification = bestAttemptContent else { return }
|
||||
if #available(iOSApplicationExtension 15.0, *),
|
||||
let imgData = avatarData,
|
||||
let channelId = notification.userInfo["channel_id"] as? String {
|
||||
@@ -139,79 +133,4 @@ class NotificationService: UNNotificationServiceExtension {
|
||||
self.contentHandler?(notification)
|
||||
}
|
||||
}
|
||||
|
||||
func sendMessageIntent(notification: UNNotificationContent) {
|
||||
if #available(iOSApplicationExtension 15.0, *) {
|
||||
let overrideUsername = notification.userInfo["override_username"] as? String
|
||||
let senderId = notification.userInfo["sender_id"] as? String
|
||||
let sender = overrideUsername ?? senderId
|
||||
|
||||
guard let serverUrl = notification.userInfo["server_url"] as? String,
|
||||
let sender = sender
|
||||
else {
|
||||
os_log(OSLogType.default, "Mattermost Notifications: No intent created. will call contentHandler to present notification")
|
||||
self.contentHandler?(notification)
|
||||
return
|
||||
}
|
||||
|
||||
let overrideIconUrl = notification.userInfo["override_icon_url"] as? String
|
||||
os_log(OSLogType.default, "Mattermost Notifications: Fetching profile Image in server %{public}@ for sender %{public}@", serverUrl, sender)
|
||||
|
||||
Network.default.fetchProfileImageSync(serverUrl, senderId: sender, overrideIconUrl: overrideIconUrl) {[weak self] data in
|
||||
self?.sendMessageIntentCompletion(notification, data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func fetchReceipt(_ ackNotification: AckNotification) -> Void {
|
||||
if (self.retryIndex >= self.fibonacciBackoffsInSeconds.count) {
|
||||
os_log(OSLogType.default, "Mattermost Notifications: max retries reached. Will call sendMessageIntent")
|
||||
sendMessageIntent(notification: bestAttemptContent!)
|
||||
return
|
||||
}
|
||||
|
||||
Network.default.postNotificationReceipt(ackNotification) {data, response, error in
|
||||
if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode != 200 {
|
||||
os_log(
|
||||
OSLogType.default,
|
||||
"Mattermost Notifications: notification receipt failed with status %{public}@. Will call sendMessageIntent",
|
||||
String(describing: httpResponse.statusCode)
|
||||
)
|
||||
self.sendMessageIntent(notification: self.bestAttemptContent!)
|
||||
return
|
||||
}
|
||||
|
||||
guard let data = data, error == nil else {
|
||||
if (ackNotification.isIdLoaded) {
|
||||
// Receipt retrieval failed. Kick off retries.
|
||||
let backoffInSeconds = self.fibonacciBackoffsInSeconds[self.retryIndex]
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + backoffInSeconds, execute: {
|
||||
os_log(
|
||||
OSLogType.default,
|
||||
"Mattermost Notifications: receipt retrieval failed. Retry %{public}@",
|
||||
String(describing: self.retryIndex)
|
||||
)
|
||||
self.fetchReceipt(ackNotification)
|
||||
})
|
||||
|
||||
self.retryIndex += 1
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
self.processResponse(serverUrl: ackNotification.serverUrl, data: data, bestAttemptContent: self.bestAttemptContent!)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension Date {
|
||||
var millisecondsSince1970: Int {
|
||||
return Int((self.timeIntervalSince1970 * 1000.0).rounded())
|
||||
}
|
||||
|
||||
init(milliseconds: Int) {
|
||||
self = Date(timeIntervalSince1970: TimeInterval(milliseconds) / 1000)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user