From 06dc849c97f0194f3c21c2431bb91ca44b7d22fc Mon Sep 17 00:00:00 2001 From: Miguel Alatzar Date: Tue, 31 Aug 2021 15:43:57 -0700 Subject: [PATCH] iOS - Fetch and store data on push notification receipt (#5645) * Gekidou package and postNotificationReceipt * Add back UploadAttachments functions * ios - Fetch and store Posts * ios - handle PostsInChannel * Use queue and group for fetchAndStoreDataForPushNotification * Handle channel and channel membership * Handle post props * Handle reactions * Handle files * Handle emojis * Handle images * Use notification data * Credential fixes * Lint fixes * Fixes * Call content handler after requests and db writes * Handle posts in thread * Remove comment --- app/init/credentials.ts | 14 +- .../DatabaseHelper.xcodeproj/project.pbxproj | 320 ---------- .../xcschemes/DatabaseHelper.xcscheme | 67 -- ios/Gekidou/.gitignore | 7 + ios/Gekidou/Package.resolved | 16 + ios/Gekidou/Package.swift | 32 + ios/Gekidou/README.md | 3 + .../Sources/Gekidou/Date+Extensions.swift | 18 + ios/Gekidou/Sources/Gekidou/Keychain.swift | 127 ++++ .../Gekidou/Networking/Network+Channels.swift | 24 + .../Gekidou/Networking/Network+Posts.swift | 43 ++ .../Gekidou/Networking/Network+Teams.swift | 24 + .../Sources/Gekidou/Networking/Network.swift | 99 +++ .../Gekidou/Networking/PushNotification.swift | 147 +++++ .../Gekidou/Storage/Database+Channels.swift | 204 ++++++ .../Gekidou/Storage/Database+Posts.swift | 596 ++++++++++++++++++ .../Gekidou/Storage/Database+Teams.swift | 100 +++ .../Sources/Gekidou/Storage/Database.swift | 135 ++++ .../Tests/GekidouTests/GekidouTests.swift | 10 + ios/Mattermost.xcodeproj/project.pbxproj | 165 +++-- .../xcshareddata/swiftpm/Package.resolved | 16 + ios/Mattermost/AppDelegate.m | 18 +- ...tificationEventHandler+HandleReplyAction.m | 6 +- ios/MattermostShare/ShareViewController.swift | 8 +- .../NotificationService.swift | 59 +- ios/Podfile.lock | 2 +- .../project.pbxproj | 2 + .../UploadAttachments/StoreManager.h | 3 +- .../UploadAttachments/StoreManager.m | 25 +- .../UploadAttachments/UploadSession.swift | 39 +- 30 files changed, 1738 insertions(+), 591 deletions(-) delete mode 100644 ios/DatabaseHelper/DatabaseHelper.xcodeproj/project.pbxproj delete mode 100644 ios/DatabaseHelper/DatabaseHelper.xcodeproj/xcshareddata/xcschemes/DatabaseHelper.xcscheme create mode 100644 ios/Gekidou/.gitignore create mode 100644 ios/Gekidou/Package.resolved create mode 100644 ios/Gekidou/Package.swift create mode 100644 ios/Gekidou/README.md create mode 100644 ios/Gekidou/Sources/Gekidou/Date+Extensions.swift create mode 100644 ios/Gekidou/Sources/Gekidou/Keychain.swift create mode 100644 ios/Gekidou/Sources/Gekidou/Networking/Network+Channels.swift create mode 100644 ios/Gekidou/Sources/Gekidou/Networking/Network+Posts.swift create mode 100644 ios/Gekidou/Sources/Gekidou/Networking/Network+Teams.swift create mode 100644 ios/Gekidou/Sources/Gekidou/Networking/Network.swift create mode 100644 ios/Gekidou/Sources/Gekidou/Networking/PushNotification.swift create mode 100644 ios/Gekidou/Sources/Gekidou/Storage/Database+Channels.swift create mode 100644 ios/Gekidou/Sources/Gekidou/Storage/Database+Posts.swift create mode 100644 ios/Gekidou/Sources/Gekidou/Storage/Database+Teams.swift create mode 100644 ios/Gekidou/Sources/Gekidou/Storage/Database.swift create mode 100644 ios/Gekidou/Tests/GekidouTests/GekidouTests.swift create mode 100644 ios/Mattermost.xcworkspace/xcshareddata/swiftpm/Package.resolved diff --git a/app/init/credentials.ts b/app/init/credentials.ts index 216e497f8..c87984b7f 100644 --- a/app/init/credentials.ts +++ b/app/init/credentials.ts @@ -77,7 +77,19 @@ export const setServerCredentials = (serverUrl: string, token: string) => { }; export const removeServerCredentials = async (serverUrl: string) => { - await KeyChain.resetInternetCredentials(serverUrl); + const options: KeyChain.Options = { + securityLevel: KeyChain.SECURITY_LEVEL.SECURE_SOFTWARE, + }; + + if (Platform.OS === 'ios') { + const appGroup = getIOSAppGroupDetails(); + options.accessGroup = appGroup.appGroupIdentifier; + + await KeyChain.resetInternetCredentials(serverUrl, options); + } else { + await KeyChain.resetGenericPassword(options); + } + await AsyncStorage.removeItem(ASYNC_STORAGE_CURRENT_SERVER_KEY); }; diff --git a/ios/DatabaseHelper/DatabaseHelper.xcodeproj/project.pbxproj b/ios/DatabaseHelper/DatabaseHelper.xcodeproj/project.pbxproj deleted file mode 100644 index 92d4f61e1..000000000 --- a/ios/DatabaseHelper/DatabaseHelper.xcodeproj/project.pbxproj +++ /dev/null @@ -1,320 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 50; - objects = { - -/* Begin PBXBuildFile section */ - 49415257267955890039D64E /* DatabaseHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49415256267955890039D64E /* DatabaseHelper.swift */; }; -/* End PBXBuildFile section */ - -/* Begin PBXCopyFilesBuildPhase section */ - 49415251267955890039D64E /* CopyFiles */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = "include/$(PRODUCT_NAME)"; - dstSubfolderSpec = 16; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXCopyFilesBuildPhase section */ - -/* Begin PBXFileReference section */ - 49415253267955890039D64E /* libDatabaseHelper.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libDatabaseHelper.a; sourceTree = BUILT_PRODUCTS_DIR; }; - 49415256267955890039D64E /* DatabaseHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseHelper.swift; sourceTree = ""; }; - 4941526826795A600039D64E /* DatabaseHelper-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "DatabaseHelper-Bridging-Header.h"; sourceTree = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 49415250267955890039D64E /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 4941524A267955890039D64E = { - isa = PBXGroup; - children = ( - 49415255267955890039D64E /* DatabaseHelper */, - 49415254267955890039D64E /* Products */, - ); - sourceTree = ""; - }; - 49415254267955890039D64E /* Products */ = { - isa = PBXGroup; - children = ( - 49415253267955890039D64E /* libDatabaseHelper.a */, - ); - name = Products; - sourceTree = ""; - }; - 49415255267955890039D64E /* DatabaseHelper */ = { - isa = PBXGroup; - children = ( - 49415256267955890039D64E /* DatabaseHelper.swift */, - 4941526826795A600039D64E /* DatabaseHelper-Bridging-Header.h */, - ); - path = DatabaseHelper; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - 49415252267955890039D64E /* DatabaseHelper */ = { - isa = PBXNativeTarget; - buildConfigurationList = 4941525A267955890039D64E /* Build configuration list for PBXNativeTarget "DatabaseHelper" */; - buildPhases = ( - 4941524F267955890039D64E /* Sources */, - 49415250267955890039D64E /* Frameworks */, - 49415251267955890039D64E /* CopyFiles */, - 4941527A26795C470039D64E /* ShellScript */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = DatabaseHelper; - productName = DatabaseHelper; - productReference = 49415253267955890039D64E /* libDatabaseHelper.a */; - productType = "com.apple.product-type.library.static"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - 4941524B267955890039D64E /* Project object */ = { - isa = PBXProject; - attributes = { - LastSwiftUpdateCheck = 1250; - LastUpgradeCheck = 1250; - TargetAttributes = { - 49415252267955890039D64E = { - CreatedOnToolsVersion = 12.5; - }; - }; - }; - buildConfigurationList = 4941524E267955890039D64E /* Build configuration list for PBXProject "DatabaseHelper" */; - compatibilityVersion = "Xcode 9.3"; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = 4941524A267955890039D64E; - productRefGroup = 49415254267955890039D64E /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - 49415252267955890039D64E /* DatabaseHelper */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXShellScriptBuildPhase section */ - 4941527A26795C470039D64E /* ShellScript */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - ); - outputFileListPaths = ( - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "# Type a script or drag a script file from your workspace to insert its path.\n\ntarget_dir=${BUILT_PRODUCTS_DIR}/include/${PRODUCT_MODULE_NAME}/\n\n# Ensure the target include path exists\nmkdir -p ${target_dir}\n\n# Copy any file that looks like a Swift generated header to the include path\ncp ${DERIVED_SOURCES_DIR}/*-Swift.h ${target_dir}\n"; - }; -/* End PBXShellScriptBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - 4941524F267955890039D64E /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 49415257267955890039D64E /* DatabaseHelper.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin XCBuildConfiguration section */ - 49415258267955890039D64E /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; - MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; - MTL_FAST_MATH = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = iphoneos; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - }; - name = Debug; - }; - 49415259267955890039D64E /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; - MTL_ENABLE_DEBUG_INFO = NO; - MTL_FAST_MATH = YES; - SDKROOT = iphoneos; - SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; - VALIDATE_PRODUCT = YES; - }; - name = Release; - }; - 4941525B267955890039D64E /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_STYLE = Automatic; - MODULEMAP_FILE = "$(SRCROOT)/DatabaseHelper/module.modulemap"; - OTHER_LDFLAGS = "-ObjC"; - PRODUCT_NAME = "$(TARGET_NAME)"; - SKIP_INSTALL = YES; - SWIFT_OBJC_BRIDGING_HEADER = "DatabaseHelper/DatabaseHelper-Bridging-Header.h"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; - 4941525C267955890039D64E /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_STYLE = Automatic; - MODULEMAP_FILE = "$(SRCROOT)/DatabaseHelper/module.modulemap"; - OTHER_LDFLAGS = "-ObjC"; - PRODUCT_NAME = "$(TARGET_NAME)"; - SKIP_INSTALL = YES; - SWIFT_OBJC_BRIDGING_HEADER = "DatabaseHelper/DatabaseHelper-Bridging-Header.h"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - 4941524E267955890039D64E /* Build configuration list for PBXProject "DatabaseHelper" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 49415258267955890039D64E /* Debug */, - 49415259267955890039D64E /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 4941525A267955890039D64E /* Build configuration list for PBXNativeTarget "DatabaseHelper" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 4941525B267955890039D64E /* Debug */, - 4941525C267955890039D64E /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - }; - rootObject = 4941524B267955890039D64E /* Project object */; -} diff --git a/ios/DatabaseHelper/DatabaseHelper.xcodeproj/xcshareddata/xcschemes/DatabaseHelper.xcscheme b/ios/DatabaseHelper/DatabaseHelper.xcodeproj/xcshareddata/xcschemes/DatabaseHelper.xcscheme deleted file mode 100644 index 1536083ef..000000000 --- a/ios/DatabaseHelper/DatabaseHelper.xcodeproj/xcshareddata/xcschemes/DatabaseHelper.xcscheme +++ /dev/null @@ -1,67 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/ios/Gekidou/.gitignore b/ios/Gekidou/.gitignore new file mode 100644 index 000000000..bb460e7be --- /dev/null +++ b/ios/Gekidou/.gitignore @@ -0,0 +1,7 @@ +.DS_Store +/.build +/Packages +/*.xcodeproj +xcuserdata/ +DerivedData/ +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata diff --git a/ios/Gekidou/Package.resolved b/ios/Gekidou/Package.resolved new file mode 100644 index 000000000..2877a162d --- /dev/null +++ b/ios/Gekidou/Package.resolved @@ -0,0 +1,16 @@ +{ + "object": { + "pins": [ + { + "package": "SQLite.swift", + "repositoryURL": "https://github.com/stephencelis/SQLite.swift.git", + "state": { + "branch": null, + "revision": "9af51e2edf491c0ea632e369a6566e09b65aa333", + "version": "0.13.0" + } + } + ] + }, + "version": 1 +} diff --git a/ios/Gekidou/Package.swift b/ios/Gekidou/Package.swift new file mode 100644 index 000000000..b96313226 --- /dev/null +++ b/ios/Gekidou/Package.swift @@ -0,0 +1,32 @@ +// swift-tools-version:5.3 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "Gekidou", + platforms: [.iOS(.v12)], + products: [ + // Products define the executables and libraries a package produces, and make them visible to other packages. + .library( + name: "Gekidou", + targets: ["Gekidou"]), + ], + dependencies: [ + // Dependencies declare other packages that this package depends on. + .package(url: "https://github.com/stephencelis/SQLite.swift.git", from: "0.13.0") + ], + targets: [ + // Targets are the basic building blocks of a package. A target can define a module or a test suite. + // Targets can depend on other targets in this package, and on products in packages this package depends on. + .target( + name: "Gekidou", + dependencies: [ + .product(name: "SQLite", package: "SQLite.swift") + ] + ), + .testTarget( + name: "GekidouTests", + dependencies: ["Gekidou"]), + ] +) diff --git a/ios/Gekidou/README.md b/ios/Gekidou/README.md new file mode 100644 index 000000000..ac1d02805 --- /dev/null +++ b/ios/Gekidou/README.md @@ -0,0 +1,3 @@ +# Gekidou + +A description of this package. diff --git a/ios/Gekidou/Sources/Gekidou/Date+Extensions.swift b/ios/Gekidou/Sources/Gekidou/Date+Extensions.swift new file mode 100644 index 000000000..2ccb60db2 --- /dev/null +++ b/ios/Gekidou/Sources/Gekidou/Date+Extensions.swift @@ -0,0 +1,18 @@ +// +// Date+Extensions.swift +// +// +// Created by Miguel Alatzar on 8/24/21. +// + +import Foundation + +extension Date { + var millisecondsSince1970: Int { + return Int((self.timeIntervalSince1970 * 1000.0).rounded()) + } + + init(milliseconds: Int) { + self = Date(timeIntervalSince1970: TimeInterval(milliseconds) / 1000) + } +} diff --git a/ios/Gekidou/Sources/Gekidou/Keychain.swift b/ios/Gekidou/Sources/Gekidou/Keychain.swift new file mode 100644 index 000000000..571811cc6 --- /dev/null +++ b/ios/Gekidou/Sources/Gekidou/Keychain.swift @@ -0,0 +1,127 @@ +// +// Keychain.swift +// Gekidou +// +// Created by Miguel Alatzar on 8/20/21. +// + +import Foundation + +enum KeychainError: Error { + case CertificateForIdentityNotFound + case IdentityNotFound + case InvalidServerUrl(_ serverUrl: String) + case InvalidHost(_ host: String) + case FailedSecIdentityCopyCertificate(_ status: OSStatus) + case FailedSecItemCopyMatching(_ status: OSStatus) +} + +extension KeychainError: LocalizedError { + var errorCode: Int32? { + switch self { + case .CertificateForIdentityNotFound: return -100 + case .IdentityNotFound: return -101 + case .InvalidServerUrl(_): return -106 + case .InvalidHost(_): return -107 + case .FailedSecIdentityCopyCertificate(status: let status): return status + case .FailedSecItemCopyMatching(status: let status): return status + } + } + + var errorDescription: String? { + switch self { + case .CertificateForIdentityNotFound: + return "Certificate for idendity not found" + case .IdentityNotFound: + return "Identity not found" + case .InvalidServerUrl(serverUrl: let serverUrl): + return "Invalid server URL: \(serverUrl)" + case .InvalidHost(host: let host): + return "Invalid host: \(host)" + case .FailedSecIdentityCopyCertificate(status: let status): + return "Failed to copy certificate: iOS code \(status)" + case .FailedSecItemCopyMatching(status: let status): + return "Failed to copy Keychain item: iOS code \(status)" + } + } +} + +public class Keychain: NSObject { + @objc public static let `default` = Keychain() + + public func getClientIdentityAndCertificate(for host: String) throws -> (SecIdentity, SecCertificate)? { + let query = try buildIdentityQuery(for: host) + + var result: AnyObject? + let identityStatus = SecItemCopyMatching(query as CFDictionary, &result) + guard identityStatus == errSecSuccess else { + if identityStatus == errSecItemNotFound { + throw KeychainError.IdentityNotFound + } + + throw KeychainError.FailedSecItemCopyMatching(identityStatus) + } + + let identity = result as! SecIdentity + var certificate: SecCertificate? + let certificateStatus = SecIdentityCopyCertificate(identity, &certificate) + guard certificateStatus == errSecSuccess else { + throw KeychainError.FailedSecIdentityCopyCertificate(certificateStatus) + } + guard certificate != nil else { + throw KeychainError.CertificateForIdentityNotFound + } + + return (identity, certificate!) + } + + @objc public func getTokenObjc(for serverUrl: String) -> String? { + return try? getToken(for: serverUrl) + } + + public func getToken(for serverUrl: String) throws -> String? { + var attributes = try buildTokenAttributes(for: serverUrl) + attributes[kSecMatchLimit] = kSecMatchLimitOne + attributes[kSecReturnData] = kCFBooleanTrue + + var result: AnyObject? + let status = SecItemCopyMatching(attributes as CFDictionary, &result) + let data = result as? Data + if status == errSecSuccess && data != nil { + return String(data: data!, encoding: .utf8) + } + + return nil + } + + private func buildIdentityQuery(for host: String) throws -> [CFString: Any] { + guard let hostData = host.data(using: .utf8) else { + throw KeychainError.InvalidHost(host) + } + + let query: [CFString:Any] = [ + kSecClass: kSecClassIdentity, + kSecAttrLabel: hostData, + kSecReturnRef: true + ] + + return query + } + + private func buildTokenAttributes(for serverUrl: String) throws -> [CFString: Any] { + guard let serverUrlData = serverUrl.data(using: .utf8) else { + throw KeychainError.InvalidServerUrl(serverUrl) + } + + var attributes: [CFString: Any] = [ + kSecClass: kSecClassInternetPassword, + kSecAttrServer: serverUrlData + ] + + if let accessGroup = Bundle.main.object(forInfoDictionaryKey: "AppGroupIdentifier") as! String? { + attributes[kSecAttrAccessGroup] = accessGroup + } + + return attributes + } +} diff --git a/ios/Gekidou/Sources/Gekidou/Networking/Network+Channels.swift b/ios/Gekidou/Sources/Gekidou/Networking/Network+Channels.swift new file mode 100644 index 000000000..222d9cf80 --- /dev/null +++ b/ios/Gekidou/Sources/Gekidou/Networking/Network+Channels.swift @@ -0,0 +1,24 @@ +// +// Network+Channels.swift +// +// +// Created by Miguel Alatzar on 8/27/21. +// + +import Foundation + +extension Network { + public func fetchChannel(withId channelId: String, withServerUrl serverUrl: String, completionHandler: @escaping ResponseHandler) { + let endpoint = "/channels/\(channelId)" + let url = buildApiUrl(serverUrl, endpoint) + + return request(url, withMethod: "GET", withServerUrl: serverUrl, completionHandler: completionHandler) + } + + public func fetchChannelMembership(withChannelId channelId: String, withUserId userId: String, withServerUrl serverUrl: String, completionHandler: @escaping ResponseHandler) { + let endpoint = "/channels/\(channelId)/members/\(userId)" + let url = buildApiUrl(serverUrl, endpoint) + + return request(url, withMethod: "GET", withServerUrl: serverUrl, completionHandler: completionHandler) + } +} diff --git a/ios/Gekidou/Sources/Gekidou/Networking/Network+Posts.swift b/ios/Gekidou/Sources/Gekidou/Networking/Network+Posts.swift new file mode 100644 index 000000000..1a63db583 --- /dev/null +++ b/ios/Gekidou/Sources/Gekidou/Networking/Network+Posts.swift @@ -0,0 +1,43 @@ +// +// Network+Posts.swift +// +// +// Created by Miguel Alatzar on 8/26/21. +// + +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) + } +} + +extension Network { + public func fetchPostsForChannel(withId channelId: String, withSince since: Int64?, withServerUrl serverUrl: String, completionHandler: @escaping ResponseHandler) { + let queryParams = since == nil ? + "?page=0&per_page=\(POST_CHUNK_SIZE)" : + "?since=\(since!)" + let endpoint = "/channels/\(channelId)/posts\(queryParams)" + let url = buildApiUrl(serverUrl, endpoint) + + return request(url, withMethod: "GET", withServerUrl: serverUrl, completionHandler: completionHandler) + } +} diff --git a/ios/Gekidou/Sources/Gekidou/Networking/Network+Teams.swift b/ios/Gekidou/Sources/Gekidou/Networking/Network+Teams.swift new file mode 100644 index 000000000..22ff2100a --- /dev/null +++ b/ios/Gekidou/Sources/Gekidou/Networking/Network+Teams.swift @@ -0,0 +1,24 @@ +// +// Network+Teams.swift +// +// +// Created by Miguel Alatzar on 8/27/21. +// + +import Foundation + +extension Network { + public func fetchTeam(withId teamId: String, withServerUrl serverUrl: String, completionHandler: @escaping ResponseHandler) { + let endpoint = "/teams/\(teamId)" + let url = buildApiUrl(serverUrl, endpoint) + + return request(url, withMethod: "GET", withServerUrl: serverUrl, completionHandler: completionHandler) + } + + public func fetchTeamMembership(withTeamId teamId: String, withUserId userId: String, withServerUrl serverUrl: String, completionHandler: @escaping ResponseHandler) { + let endpoint = "/teams/\(teamId)/members/\(userId)" + let url = buildApiUrl(serverUrl, endpoint) + + return request(url, withMethod: "GET", withServerUrl: serverUrl, completionHandler: completionHandler) + } +} diff --git a/ios/Gekidou/Sources/Gekidou/Networking/Network.swift b/ios/Gekidou/Sources/Gekidou/Networking/Network.swift new file mode 100644 index 000000000..44c0301e6 --- /dev/null +++ b/ios/Gekidou/Sources/Gekidou/Networking/Network.swift @@ -0,0 +1,99 @@ +// +// Network.swift +// Gekidou +// +// Created by Miguel Alatzar on 8/20/21. +// + +import Foundation + +public typealias ResponseHandler = (_ data: Data?, _ response: URLResponse?, _ error: Error?) -> Void + +public class Network: NSObject { + internal var session: URLSession? + internal let queue = OperationQueue() + internal let urlVersion = "/api/v4" + + override init() { + super.init() + + queue.maxConcurrentOperationCount = 1 + + let config = URLSessionConfiguration.default + config.httpAdditionalHeaders = ["X-Requested-With": "XMLHttpRequest"] + config.allowsCellularAccess = true + config.timeoutIntervalForRequest = 10 + config.timeoutIntervalForResource = 10 + config.httpMaximumConnectionsPerHost = 10 + + self.session = URLSession.init(configuration: config, delegate: self, delegateQueue: nil) + } + + @objc public static let `default` = Network() + + internal func buildApiUrl(_ serverUrl: String, _ endpoint: String) -> URL { + return URL(string: "\(serverUrl)\(urlVersion)\(endpoint)")! + } + + internal func responseOK(_ response: URLResponse?) -> Bool { + 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 { + let request = NSMutableURLRequest(url: url) + request.httpMethod = method + + if let body = body { + request.httpBody = body + } + + if let headers = headers { + for (key, value) in headers { + request.setValue(value, forHTTPHeaderField: key) + } + } + + if let token = try? Keychain.default.getToken(for: serverUrl) { + request.addValue("Bearer \(token)", forHTTPHeaderField: "Authorization") + } + + 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, 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) + + let task = session!.dataTask(with: urlRequest) { data, response, error in + completionHandler(data, response, error) + } + + 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) + } +} diff --git a/ios/Gekidou/Sources/Gekidou/Networking/PushNotification.swift b/ios/Gekidou/Sources/Gekidou/Networking/PushNotification.swift new file mode 100644 index 000000000..fe6b13d29 --- /dev/null +++ b/ios/Gekidou/Sources/Gekidou/Networking/PushNotification.swift @@ -0,0 +1,147 @@ +// +// File.swift +// +// +// Created by Miguel Alatzar on 8/26/21. +// + +import Foundation +import UserNotifications + +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 + case id = "ack_id" + case postId = "post_id" + case serverUrl = "server_url" + case isIdLoaded = "is_id_loaded" + + } + + 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) + isIdLoaded = (try? container.decode(Bool.self, forKey: .isIdLoaded)) == true + receivedAt = Date().millisecondsSince1970 + + if let decodedServerUrl = try? container.decode(String.self, forKey: .serverUrl) { + serverUrl = decodedServerUrl + } else { + serverUrl = try Database.default.getOnlyServerUrl() + } + } +} + +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}) + } + } + + 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)?) { + // TODO: All DB writes should be made in a single transaction + let operation = BlockOperation { + let group = DispatchGroup() + var channel: Channel? = nil + var channelMembership: ChannelMembership? + + let teamId = notification.userInfo["team_id"] as! String? + let channelId = notification.userInfo["channel_id"] as! String + let serverUrl = notification.userInfo["server_url"] as! String + let currentUserId = try! Database.default.queryCurrentUserId(serverUrl) + + if let teamId = teamId { + if try! !Database.default.hasMyTeam(withId: teamId, withServerUrl: serverUrl) { + group.enter() + self.fetchTeam(withId: teamId, withServerUrl: serverUrl) { data, response, error in + if self.responseOK(response), let data = data { + let team = try! JSONDecoder().decode(Team.self, from: data) + try! Database.default.insertTeam(team, serverUrl) + } + + group.leave() + } + + group.enter() + self.fetchTeamMembership(withTeamId: teamId, withUserId: currentUserId, withServerUrl: serverUrl) { data, response, error in + if self.responseOK(response), let data = data { + let teamMembership = try! JSONDecoder().decode(TeamMembership.self, from: data) + if teamMembership.user_id == currentUserId { + try! Database.default.insertMyTeam(teamMembership, serverUrl) + } + } + + group.leave() + } + } + } + + + group.enter() + self.fetchChannel(withId: channelId, withServerUrl: serverUrl) { data, response, error in + if self.responseOK(response), let data = data { + channel = try! JSONDecoder().decode(Channel.self, from: data) + } + + group.leave() + } + + group.enter() + self.fetchChannelMembership(withChannelId: channelId, withUserId: currentUserId, withServerUrl: serverUrl) { data, response, error in + if self.responseOK(response), let data = data { + channelMembership = try! JSONDecoder().decode(ChannelMembership.self, from: data) + } + + group.leave() + } + + group.enter() + let since = try! Database.default.queryPostsSinceForChannel(withId: channelId, withServerUrl: serverUrl) + self.fetchPostsForChannel(withId: channelId, withSince: since, withServerUrl: serverUrl) { data, response, error in + if self.responseOK(response), let data = data { + let postData = try! JSONDecoder().decode(PostData.self, from: data) + if postData.posts.count > 0 { + try! Database.default.handlePostData(postData, channelId, serverUrl, since != nil) + } + } + + group.leave() + } + + group.wait() + + try! Database.default.handleChannelAndMembership(channel, channelMembership, serverUrl) + + if let contentHandler = contentHandler { + contentHandler(notification) + } + } + + queue.addOperation(operation) + } +} diff --git a/ios/Gekidou/Sources/Gekidou/Storage/Database+Channels.swift b/ios/Gekidou/Sources/Gekidou/Storage/Database+Channels.swift new file mode 100644 index 000000000..32b581c20 --- /dev/null +++ b/ios/Gekidou/Sources/Gekidou/Storage/Database+Channels.swift @@ -0,0 +1,204 @@ +// +// Database+Channels.swift +// +// +// Created by Miguel Alatzar on 8/27/21. +// + +import Foundation +import SQLite + +public struct Channel: Codable { + let id: String + let create_at: Int64 + let update_at: Int64 + let delete_at: Int64 + let team_id: String + let type: String + let display_name: String + let name: String + let creator_id: String + let header: String + let purpose: String + let total_msg_count: Int64 +} + +public struct ChannelMembership: Codable { + let channel_id: String + let user_id: String + let roles: String + let last_viewed_at: Int64 + let mention_count: Int64 + let msg_count: Int64 + let notify_props: [String:String] +} + +extension Database { + public func queryChannel(withId channelId: String, withServerUrl serverUrl: String) throws -> Row? { + let db = try getDatabaseForServer(serverUrl) + + let idCol = Expression("id") + let query = channelTable.where(idCol == channelId) + + return try db.pluck(query) + } + + public func queryMyChannel(withId channelId: String, withServerUrl serverUrl: String) throws -> Row? { + let db = try getDatabaseForServer(serverUrl) + + let idCol = Expression("id") + let query = myChannelTable.where(idCol == channelId) + + return try db.pluck(query) + } + + public func insertChannel(_ channel: Channel, _ serverUrl: String) throws { + let db = try getDatabaseForServer(serverUrl) + + let setter = createChannelSetter(from: channel) + let query = channelTable.insert(or: .replace, setter) + try db.run(query) + } + + public func insertChannelInfo(_ channel: Channel, _ serverUrl: String) throws { + let db = try getDatabaseForServer(serverUrl) + + let setter = createChannelInfoSetter(from: channel) + let query = channelInfoTable.insert(or: .replace, setter) + try db.run(query) + } + + public func insertMyChannel(_ channelMembership: ChannelMembership, _ serverUrl: String) throws { + let db = try getDatabaseForServer(serverUrl) + + let setter = createMyChannelSetter(from: channelMembership) + let query = myChannelTable.insert(or: .replace, setter) + try db.run(query) + } + + public func insertMyChannelSettings(_ channelMembership: ChannelMembership, _ serverUrl: String) throws { + let db = try getDatabaseForServer(serverUrl) + + let setter = createMyChannelSettingsSetter(from: channelMembership) + let query = myChannelSettingsTable.insert(or: .replace, setter) + try db.run(query) + } + + public func insertChannelMembership(_ channelMembership: ChannelMembership, _ serverUrl: String) throws { + let db = try getDatabaseForServer(serverUrl) + + let setter = createChannelMembershipSetter(from: channelMembership) + let query = channelMembershipTable.insert(or: .replace, setter) + try db.run(query) + } + + public func handleChannelAndMembership(_ channel: Channel?, _ channelMembership: ChannelMembership?, _ serverUrl: String) throws { + if let channel = channel { + try insertChannel(channel, serverUrl) + try insertChannelInfo(channel, serverUrl) + } + + if let channelMembership = channelMembership { + try insertMyChannel(channelMembership, serverUrl) + try insertMyChannelSettings(channelMembership, serverUrl) + try insertChannelMembership(channelMembership, serverUrl) + } + + try updateMyChannelMessageCount(channel, channelMembership, serverUrl) + } + + private func updateMyChannelMessageCount(_ channel: Channel?, _ channelMembership: ChannelMembership?, _ serverUrl: String) throws { + if let channel = channel, let channelMembership = channelMembership { + let db = try getDatabaseForServer(serverUrl) + + let messageCount = channel.total_msg_count - channelMembership.msg_count + let idCol = Expression("id") + let messageCountCol = Expression("message_count") + let query = myChannelTable + .where(idCol == channel.id) + .update(messageCountCol <- messageCount) + + try db.run(query) + } + } + + private func createChannelSetter(from channel: Channel) -> [Setter] { + let id = Expression("id") + let createAt = Expression("create_at") + let updateAt = Expression("update_at") + let deleteAt = Expression("delete_at") + let teamId = Expression("team_id") + let type = Expression("type") + let displayName = Expression("display_name") + let name = Expression("name") + let creatorId = Expression("creator_id") + + var setter = [Setter]() + setter.append(id <- channel.id) + setter.append(createAt <- channel.create_at) + setter.append(updateAt <- channel.update_at) + setter.append(deleteAt <- channel.delete_at) + setter.append(teamId <- channel.team_id) + setter.append(type <- channel.type) + setter.append(displayName <- channel.display_name) + setter.append(name <- channel.name) + setter.append(creatorId <- channel.creator_id) + + return setter + } + + private func createChannelInfoSetter(from channel: Channel) -> [Setter] { + let id = Expression("id") + let header = Expression("header") + let purpose = Expression("purpose") + + var setter = [Setter]() + setter.append(id <- channel.id) + setter.append(header <- channel.header) + setter.append(purpose <- channel.purpose) + + return setter + } + + private func createMyChannelSetter(from channelMembership: ChannelMembership) -> [Setter] { + let id = Expression("id") + let roles = Expression("roles") + let lastViewedAt = Expression("last_viewed_at") + let mentionsCount = Expression("mentions_count") + + var setter = [Setter]() + setter.append(id <- channelMembership.channel_id) + setter.append(roles <- channelMembership.roles) + setter.append(lastViewedAt <- channelMembership.last_viewed_at) + setter.append(mentionsCount <- channelMembership.mention_count) + + return setter + } + + private func createMyChannelSettingsSetter(from channelMembership: ChannelMembership) -> [Setter] { + let id = Expression("id") + let notifyProps = Expression("notify_props") + + let notifyPropsJSON = try! JSONSerialization.data(withJSONObject: channelMembership.notify_props, options: []) + let notifyPropsString = String(data: notifyPropsJSON, encoding: String.Encoding.utf8)! + + var setter = [Setter]() + setter.append(id <- channelMembership.channel_id) + setter.append(notifyProps <- notifyPropsString) + + return setter + } + + private func createChannelMembershipSetter(from channelMembership: ChannelMembership) -> [Setter] { + let id = Expression("id") + let channelId = Expression("channel_id") + let userId = Expression("user_id") + + var setter = [Setter]() + setter.append(id <- channelMembership.channel_id) + setter.append(channelId <- channelMembership.channel_id) + setter.append(userId <- channelMembership.user_id) + + return setter + } +} diff --git a/ios/Gekidou/Sources/Gekidou/Storage/Database+Posts.swift b/ios/Gekidou/Sources/Gekidou/Storage/Database+Posts.swift new file mode 100644 index 000000000..7aac545c5 --- /dev/null +++ b/ios/Gekidou/Sources/Gekidou/Storage/Database+Posts.swift @@ -0,0 +1,596 @@ +// +// Database+Posts.swift +// +// +// Created by Miguel Alatzar on 8/26/21. +// + +import Foundation +import SQLite + +public struct EmbedMedia: Codable { + let type: String? + let url: String? + let secure_url: String? + let width: Int64? + let height: Int64? +} + +public struct EmbedData: Codable { + let type: String? + let url: String? + let title: String? + let description: String? + let determiner: String? + let site_name: String? + let locale: String? + let locales_alternate: String? + let images: [EmbedMedia]? + let audios: [EmbedMedia]? + let videos: [EmbedMedia]? +} + +public struct Embed: Codable { + let type: String? + let url: String? + let data: EmbedData? +} + +public struct Emoji: Codable { + let id: String + let name: String +} + +public struct File: Codable { + let id: String + let post_id: String + let name: String + let `extension`: String + let size: Int64 + let mime_type: String + let width: Int64 + let height: Int64 + let local_path: String? + let mini_preview: String? +} + +public struct Reaction: Codable { + let user_id: String + let post_id: String + let emoji_name: String + let create_at: Int64 +} + +public struct Image: Codable { + let width: Int64 + let height: Int64 + let format: String + let frame_count: Int64 +} + +public struct PostMetadata: Codable { + let embeds: [Embed]? + let images: [String:Image]? + var emojis: [Emoji]? + var files: [File]? + var reactions: [Reaction]? +} + +public struct PostPropsAddChannelMember: Codable { + let post_id: String + let usernames: String + let not_in_channel_usernames: String + let user_ids: String + let not_in_channel_user_ids: String + let not_in_groups_usernames: String + let not_in_groups_user_ids: String +} + +public struct PostPropsAttachment: Codable { + let id: Int64 + let fallback: String + let color: String + let pretext: String + let author_name: String + let author_link: String + let author_icon: String + let title: String + let title_link: String + let text: String + let image_url: String + let thumb_url: String + let footer: String + let footer_icon: String + + // TODO: + // fields + // timestamp + // actions +} + +public struct PostProps: Codable { + let userId: String? + let username: String? + let addedUserId: String? + let removedUserId: String? + let removedUsername: String? + let deleteBy: String? + let old_header: String? + let new_header: String? + let old_purpose: String? + let new_purpose: String? + let old_displayname: String? + let new_displayname: String? + let mentionHighlightDisabled: Bool? + let disable_group_highlight: Bool? + let override_username: Bool? + let from_webhook: Bool? + let override_icon_url: Bool? + let override_icon_emoji: Bool? + let add_channel_member: PostPropsAddChannelMember? + let attachments: [PostPropsAttachment]? + + // TODO: + // appBindings +} + +public struct Post: Codable { + let id: String + let create_at: Int64 + 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 + var props: PostProps? + let hashtag: String? + let pending_post_id: String? + let reply_count: Int64 + let file_ids: [String]? + let metadata: PostMetadata? + let last_reply_at: Int64? + let failed: Bool? + let ownPost: Bool? + let participants: [String]? + var prev_post_id: String? +} + +struct MetadataSetters { + let postMetadataSetters: [[Setter]] + let reactionSetters: [[Setter]] + let fileSetters: [[Setter]] + let emojiSetters: [[Setter]] +} + +extension Database { + public func queryPostsSinceForChannel(withId channelId: String, withServerUrl serverUrl: String) throws -> Int64? { + let db = try getDatabaseForServer(serverUrl) + + let earliestCol = Expression("earliest") + let latestCol = Expression("latest") + let channelIdCol = Expression("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 + } + + let createAtCol = Expression("create_at") + let deleteAtCol = Expression("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 + } + + private func queryPostsInChannelEarliestAndLatest(_ serverUrl: String, _ channelId: String) throws -> (Int64, Int64) { + let db = try getDatabaseForServer(serverUrl) + let earliest = Expression("earliest") + let latest = Expression("latest") + let id = Expression("channel_id") + let query = postsInChannelTable + .select(earliest, latest) + .where(id == channelId) + .order(latest.desc) + .limit(1) + + + for result in try db.prepare(query) { + return (try result.get(earliest), + try result.get(latest)) + } + + return (0, 0) + } + + public func handlePostData(_ postData: PostData, _ channelId: String, _ serverUrl: String, _ usedSince: Bool = false) throws { + try insertAndDeletePosts(postData.posts, channelId, serverUrl) + try handlePostsInChannel(postData, channelId, serverUrl, usedSince) + try handlePostMetadata(postData.posts, channelId, serverUrl) + try handlePostsInThread(postData.posts, serverUrl) + } + + private func handlePostsInChannel(_ postData: PostData, _ channelId: String, _ serverUrl: String, _ usedSince: Bool = false) throws { + let sortedChainedPosts = chainAndSortPosts(postData) + let earliest = sortedChainedPosts.first!.create_at + let latest = sortedChainedPosts.last!.create_at + + if usedSince { + try updatePostsInChannelLatestOnly(latest, channelId, serverUrl) + } else { + let updated = try updatePostsInChannelEarliestAndLatest(earliest, latest, channelId, serverUrl) + if (!updated) { + try insertPostsInChannel(earliest, latest, channelId, serverUrl) + } + } + } + + private func handlePostsInThread(_ posts: [Post], _ serverUrl: String) throws { + let db = try getDatabaseForServer(serverUrl) + + let postsInThreadSetters = try createPostsInThreadSetters(from: posts, withServerUrl: serverUrl) + if !postsInThreadSetters.isEmpty { + let insertQuery = postsInThreadTable.insertMany(or: .replace, postsInThreadSetters) + try db.run(insertQuery) + } + } + + private func chainAndSortPosts(_ postData: PostData) -> [Post] { + let order = postData.order + let prevPostId = postData.prev_post_id + let posts = postData.posts + + return order.enumerated().reduce([Post]()) { (chainedPosts: [Post], current) in + let index = current.0 + let postId = current.1 + + var post = posts.first(where: {$0.id == postId})! + post.prev_post_id = index == order.count - 1 ? + prevPostId : + order[index + 1] + + return chainedPosts + [post] + }.sorted(by: { $0.create_at < $1.create_at }) + } + + private func updatePostsInChannelLatestOnly(_ latest: Int64, _ channelId: String, _ serverUrl: String) throws { + let db = try getDatabaseForServer(serverUrl) + + let channelIdCol = Expression("channel_id") + let latestCol = Expression("latest") + + let query = postsInChannelTable + .where(channelIdCol == channelId) + .order(latestCol.desc) + .limit(1) + .update(latestCol <- latest) + + try db.run(query) + } + + private func updatePostsInChannelEarliestAndLatest(_ earliest: Int64, _ latest: Int64, _ channelId: String, _ serverUrl: String) throws -> Bool { + let db = try getDatabaseForServer(serverUrl) + + let idCol = Expression("id") + let channelIdCol = Expression("channel_id") + let earliestCol = Expression("earliest") + let latestCol = Expression("latest") + + let query = postsInChannelTable + .where(channelIdCol == channelId && (earliestCol <= earliest || latestCol >= latest)) + .order(latestCol.desc) + .limit(1) + + if let record = try db.pluck(query) { + let recordId = try record.get(idCol) + let recordEarliest = try record.get(earliestCol) + let recordLatest = try record.get(latestCol) + + let updateQuery = postsInChannelTable + .filter(idCol == recordId) + .update(earliestCol <- min(earliest, recordEarliest), latestCol <- max(latest, recordLatest)) + + try db.run(updateQuery) + + return true + } + + return false + } + + private func insertPostsInChannel(_ earliest: Int64, _ latest: Int64, _ channelId: String, _ serverUrl: String) throws { + let db = try getDatabaseForServer(serverUrl) + + let rowIdCol = Expression("rowid") + let channelIdCol = Expression("channel_id") + let earliestCol = Expression("earliest") + let latestCol = Expression("latest") + + let query = postsInChannelTable + .insert(channelIdCol <- channelId, earliestCol <- earliest, latestCol <- latest) + let newRecordId = try db.run(query) + + let deleteQuery = postsInChannelTable + .where(rowIdCol != newRecordId && + channelIdCol == channelId && + earliestCol >= earliest && + latestCol <= latest) + .delete() + + try db.run(deleteQuery) + } + + private func insertAndDeletePosts(_ posts: [Post], _ channelId: String, _ serverUrl: String) throws { + let db = try getDatabaseForServer(serverUrl) + + let setters = createPostSetters(from: posts) + let insertQuery = postTable.insertMany(or: .replace, setters) + try db.run(insertQuery) + + let deleteIds = posts.reduce([String]()) { (accumulated, post) in + if let deleteId = post.pending_post_id { + return accumulated + [deleteId] + } + + return accumulated + } + let id = Expression("id") + let deleteQuery = postTable + .filter(deleteIds.contains(id)) + .delete() + try db.run(deleteQuery) + } + + private func handlePostMetadata(_ posts: [Post], _ channelId: String, _ serverUrl: String) throws { + let db = try getDatabaseForServer(serverUrl) + + let setters = createPostMetadataSetters(from: posts) + + if !setters.postMetadataSetters.isEmpty { + let insertQuery = postMetadataTable.insertMany(or: .replace, setters.postMetadataSetters) + try db.run(insertQuery) + } + + if !setters.reactionSetters.isEmpty { + let insertQuery = reactionTable.insertMany(or: .replace, setters.reactionSetters) + try db.run(insertQuery) + } + + if !setters.fileSetters.isEmpty { + let insertQuery = fileTable.insertMany(or: .replace, setters.fileSetters) + try db.run(insertQuery) + } + + if !setters.emojiSetters.isEmpty { + let insertQuery = emojiTable.insertMany(or: .replace, setters.emojiSetters) + try db.run(insertQuery) + } + } + + private func createPostSetters(from posts: [Post]) -> [[Setter]] { + let id = Expression("id") + let createAt = Expression("create_at") + let updateAt = Expression("update_at") + let editAt = Expression("edit_at") + let deleteAt = Expression("delete_at") + let isPinned = Expression("is_pinned") + let userId = Expression("user_id") + let channelId = Expression("channel_id") + let rootId = Expression("root_id") + let originalId = Expression("original_id") + let message = Expression("message") + let type = Expression("type") +// let hashtag = Expression("hashtag") + let pendingPostId = Expression("pending_post_id") +// let replyCount = Expression("reply_count") +// let fileIds = Expression("file_ids") +// let lastReplyAt = Expression("last_reply_at") +// let failed = Expression("failed") +// let ownPost = Expression("ownPost") + let prevPostId = Expression("previous_post_id") +// let participants = Expression("participants") + let props = Expression("props") + + var setters = [[Setter]]() + for post in posts { + var setter = [Setter]() + 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(message <- post.message) + setter.append(type <- post.type) +// setter.append(hashtag <- post.hashtag) + setter.append(pendingPostId <- post.pending_post_id) +// setter.append(replyCount <- post.reply_count) +// setter.append(fileIds <- json(from: post.file_ids)) +// setter.append(lastReplyAt <- post.last_reply_at) +// setter.append(failed <- post.failed) +// setter.append(ownPost <- post.ownPost) + setter.append(prevPostId <- post.prev_post_id) +// setter.append(participants <- json(from: post.participants)) + + if let postProps = post.props { + let propsJSON = try! JSONEncoder().encode(postProps) + let propsString = String(data: propsJSON, encoding: String.Encoding.utf8) + setter.append(props <- propsString) + } + + setters.append(setter) + } + + return setters + } + + private func createPostMetadataSetters(from posts: [Post]) -> MetadataSetters { + let id = Expression("id") + let data = Expression("data") + let userId = Expression("user_id") + let postId = Expression("post_id") + let emojiName = Expression("emoji_name") + let createAt = Expression("create_at") + let name = Expression("name") + let ext = Expression("extension") + let size = Expression("size") + let mimeType = Expression("mime_type") + let width = Expression("width") + let height = Expression("height") + let localPath = Expression("local_path") + let imageThumbnail = Expression("image_thumbnail") + + var postMetadataSetters = [[Setter]]() + var reactionSetters = [[Setter]]() + var fileSetters = [[Setter]]() + var emojiSetters = [[Setter]]() + + for post in posts { + if var metadata = post.metadata { + // Reaction setters + if let reactions = metadata.reactions { + for reaction in reactions { + var reactionSetter = [Setter]() + reactionSetter.append(userId <- reaction.user_id) + reactionSetter.append(postId <- reaction.post_id) + reactionSetter.append(emojiName <- reaction.emoji_name) + reactionSetter.append(createAt <- reaction.create_at) + + reactionSetters.append(reactionSetter) + } + + metadata.reactions = nil + } + + // File setters + if let files = metadata.files { + for file in files { + var fileSetter = [Setter]() + fileSetter.append(id <- file.id) + fileSetter.append(postId <- file.post_id) + fileSetter.append(name <- file.name) + fileSetter.append(ext <- file.`extension`) + fileSetter.append(size <- file.size) + fileSetter.append(mimeType <- file.mime_type) + fileSetter.append(width <- file.width) + fileSetter.append(height <- file.height) + fileSetter.append(localPath <- file.local_path) + fileSetter.append(imageThumbnail <- file.mini_preview) + + fileSetters.append(fileSetter) + } + + metadata.files = nil + } + + // Emoji setters + if let emojis = metadata.emojis { + for emoji in emojis { + var emojiSetter = [Setter]() + emojiSetter.append(id <- emoji.id) + emojiSetter.append(name <- emoji.name) + + emojiSetters.append(emojiSetter) + } + + metadata.emojis = nil + } + + // Metadata setter + var metadataSetter = [Setter]() + + metadataSetter.append(id <- post.id) + + let dataJSON = try! JSONEncoder().encode(metadata) + let dataString = String(data: dataJSON, encoding: String.Encoding.utf8)! + metadataSetter.append(data <- dataString) + + postMetadataSetters.append(metadataSetter) + } + } + + return MetadataSetters(postMetadataSetters: postMetadataSetters, + reactionSetters: reactionSetters, + fileSetters: fileSetters, + emojiSetters: emojiSetters) + } + + private func createPostsInThreadSetters(from posts: [Post], withServerUrl serverUrl: String) throws -> [[Setter]] { + let db = try getDatabaseForServer(serverUrl) + + var setters = [[Setter]]() + var postsInThread = [String: [Post]]() + + for post in posts { + if !post.root_id.isEmpty { + var threadPosts = postsInThread[post.root_id] ?? [Post]() + threadPosts.append(post) + + postsInThread.updateValue(threadPosts, forKey: post.root_id) + } + } + + let rootIdCol = Expression("root_id") + let earliestCol = Expression("earliest") + let latestCol = Expression("latest") + + 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 updateQuery = postsInThreadTable + .where(rootIdCol == rootId && earliestCol == rowEarliest && latestCol == rowLatest) + .update(earliestCol <- min(earliest, rowEarliest), + latestCol <- max(latest, rowLatest)) + try db.run(updateQuery) + } else { + var setter = [Setter]() + setter.append(rootIdCol <- rootId) + setter.append(earliestCol <- earliest) + setter.append(latestCol <- latest) + + setters.append(setter) + } + } + + return setters + } +} diff --git a/ios/Gekidou/Sources/Gekidou/Storage/Database+Teams.swift b/ios/Gekidou/Sources/Gekidou/Storage/Database+Teams.swift new file mode 100644 index 000000000..3d585c5fe --- /dev/null +++ b/ios/Gekidou/Sources/Gekidou/Storage/Database+Teams.swift @@ -0,0 +1,100 @@ +// +// Database+Teams.swift +// +// +// Created by Miguel Alatzar on 8/27/21. +// + +import Foundation +import SQLite + +public struct Team: Codable { + let id: String + let update_at: Int64 + let display_name: String + let name: String + let description: String + let type: String + let allowed_domains: String + let allow_open_invite: Bool + let policy_id: String +} + +public struct TeamMembership: Codable { + let team_id: String + let user_id: String + let roles: String + let delete_at: Int64 + let scheme_user: Bool + let scheme_admin: Bool + let explicit_roles: String +} + +extension Database { + public func hasMyTeam(withId teamId: String, withServerUrl serverUrl: String) throws -> Bool { + let db = try getDatabaseForServer(serverUrl) + + let idCol = Expression("id") + let query = myTeamTable.where(idCol == teamId) + + if let _ = try db.pluck(query) { + return true + } + + return false + } + + public func insertTeam(_ team: Team, _ serverUrl: String) throws { + let db = try getDatabaseForServer(serverUrl) + + let setter = createTeamSetter(from: team) + let insertQuery = teamTable.insert(or: .replace, setter) + try db.run(insertQuery) + } + + public func insertMyTeam(_ teamMembership: TeamMembership, _ serverUrl: String) throws { + let db = try getDatabaseForServer(serverUrl) + + let setter = createMyTeamSetter(from: teamMembership) + let insertQuery = myTeamTable.insert(or: .replace, setter) + try db.run(insertQuery) + } + + private func createTeamSetter(from team: Team) -> [Setter] { + let id = Expression("id") + let updateAt = Expression("update_at") + let displayName = Expression("display_name") + let name = Expression("name") + let description = Expression("description") + let type = Expression("type") + let allowedDomains = Expression("allowed_domains") + let isAllowOpenInvite = Expression("is_allow_open_invite") + let policyId = Expression("policy_id") + + var setter = [Setter]() + setter.append(id <- team.id) + setter.append(updateAt <- team.update_at) + setter.append(displayName <- team.display_name) + setter.append(name <- team.name) + setter.append(description <- team.description) + setter.append(type <- team.type) + setter.append(allowedDomains <- team.allowed_domains) + setter.append(isAllowOpenInvite <- team.allow_open_invite) + + // TODO: policyId is not yet in the Team table + // setter.append(policyId <- team.policy_id) + + return setter + } + + private func createMyTeamSetter(from teamMembership: TeamMembership) -> [Setter] { + let teamId = Expression("team_id") + let roles = Expression("roles") + + var setter = [Setter]() + setter.append(teamId <- teamMembership.team_id) + setter.append(roles <- teamMembership.roles) + + return setter + } +} diff --git a/ios/Gekidou/Sources/Gekidou/Storage/Database.swift b/ios/Gekidou/Sources/Gekidou/Storage/Database.swift new file mode 100644 index 000000000..5f3f2bee1 --- /dev/null +++ b/ios/Gekidou/Sources/Gekidou/Storage/Database.swift @@ -0,0 +1,135 @@ +// +// Database.swift +// Gekidou +// +// Created by Miguel Alatzar on 8/20/21. +// + +import Foundation +import SQLite3 +import SQLite + +// TODO: This should be exposed to Objective-C in order to handle +// any Database throwable methods. +enum DatabaseError: Error { + case OpenFailure(_ dbPath: String) + case MultipleServers + case NoResults(_ query: String) + case NoDatabase(_ serverUrl: String) + case InsertError(_ statement: String) +} + +extension DatabaseError: LocalizedError { + var errorDescription: String? { + switch self { + case .OpenFailure(dbPath: let dbPath): + return "Error opening database: \(dbPath)" + case .MultipleServers: + return "Cannot determine server URL as there are multiple servers" + case .NoResults(query: let query): + return "No results for query: \(query)" + case .NoDatabase(serverUrl: let serverUrl): + return "No database for server: \(serverUrl)" + case .InsertError(statement: let statement): + return "Insert error: \(statement)" + } + } +} + +public class Database: NSObject { + internal let DEFAULT_DB_NAME = "app.db" + internal var DEFAULT_DB_PATH: String + internal var defaultDB: OpaquePointer? = nil + + internal var serversTable = Table("Servers") + internal var systemTable = Table("System") + internal var teamTable = Table("Team") + internal var myTeamTable = Table("MyTeam") + internal var channelTable = Table("Channel") + internal var channelInfoTable = Table("ChannelInfo") + internal var channelMembershipTable = Table("ChannelMembership") + internal var myChannelTable = Table("MyChannel") + internal var myChannelSettingsTable = Table("MyChannelSettings") + internal var postTable = Table("Post") + internal var postsInChannelTable = Table("PostsInChannel") + internal var postsInThreadTable = Table("PostsInThread") + internal var postMetadataTable = Table("PostMetadata") + internal var reactionTable = Table("Reaction") + internal var fileTable = Table("File") + internal var emojiTable = Table("CustomEmoji") + + @objc public static let `default` = Database() + + override private init() { + let appGroupId = Bundle.main.object(forInfoDictionaryKey: "AppGroupIdentifier") as! String + let sharedDirectory = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupId)! + let databaseUrl = sharedDirectory.appendingPathComponent("databases/\(DEFAULT_DB_NAME)") + + DEFAULT_DB_PATH = databaseUrl.path + } + + @objc public func getOnlyServerUrlObjc() -> String { + do { + return try getOnlyServerUrl() + } catch { + print(error) + return "" + } + } + + public func getOnlyServerUrl() throws -> String { + let db = try Connection(DEFAULT_DB_PATH) + let url = Expression("url") + let query = serversTable.select(url) + + var serverUrl: String? + for result in try db.prepare(query) { + if (serverUrl != nil) { + throw DatabaseError.MultipleServers + } + + serverUrl = try result.get(url) + } + + if (serverUrl != nil) { + return serverUrl! + } + + throw DatabaseError.NoResults(query.asSQL()) + } + + internal func getDatabaseForServer(_ serverUrl: String) throws -> Connection { + let db = try Connection(DEFAULT_DB_PATH) + let url = Expression("url") + let dbPath = Expression("db_path") + let query = serversTable.select(dbPath).where(url == serverUrl) + + if let result = try db.pluck(query) { + let path = try result.get(dbPath) + return try Connection(path) + } + + throw DatabaseError.NoResults(query.asSQL()) + } + + internal func queryCurrentUserId(_ serverUrl: String) throws -> String { + let db = try getDatabaseForServer(serverUrl) + + let idCol = Expression("id") + let valueCol = Expression("value") + let query = systemTable.where(idCol == "currentUserId") + + if let result = try db.pluck(query) { + return try result.get(valueCol).replacingOccurrences(of: "\"", with: "") + } + + throw DatabaseError.NoResults(query.asSQL()) + } + + private func json(from object:Any?) -> String? { + guard let object = object, let data = try? JSONSerialization.data(withJSONObject: object, options: []) else { + return nil + } + return String(data: data, encoding: String.Encoding.utf8) + } +} diff --git a/ios/Gekidou/Tests/GekidouTests/GekidouTests.swift b/ios/Gekidou/Tests/GekidouTests/GekidouTests.swift new file mode 100644 index 000000000..f468faca5 --- /dev/null +++ b/ios/Gekidou/Tests/GekidouTests/GekidouTests.swift @@ -0,0 +1,10 @@ + import XCTest + @testable import Gekidou + + final class GekidouTests: XCTestCase { + func testExample() { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct + // results. + } + } diff --git a/ios/Mattermost.xcodeproj/project.pbxproj b/ios/Mattermost.xcodeproj/project.pbxproj index 603a58d31..31578a4a4 100644 --- a/ios/Mattermost.xcodeproj/project.pbxproj +++ b/ios/Mattermost.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 46; + objectVersion = 52; objects = { /* Begin PBXBuildFile section */ @@ -14,10 +14,10 @@ 13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; }; 279A77DD26A6F1BE00B515F1 /* compass-icons.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 279A77DC26A6F1BE00B515F1 /* compass-icons.ttf */; }; 2D5296A8926B4D7FBAF2D6E2 /* OpenSans-Light.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 6561AEAC21CC40B8A72ABB93 /* OpenSans-Light.ttf */; }; - 49415295267A91230039D64E /* libDatabaseHelper.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 49415294267A911C0039D64E /* libDatabaseHelper.a */; }; - 49415296267A91290039D64E /* libDatabaseHelper.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 49415294267A911C0039D64E /* libDatabaseHelper.a */; }; - 49415297267A91300039D64E /* libDatabaseHelper.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 49415294267A911C0039D64E /* libDatabaseHelper.a */; }; 4953BF602368AE8600593328 /* SwimeProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4953BF5F2368AE8600593328 /* SwimeProxy.swift */; }; + 49AE36FF26D4455800EF4E52 /* Gekidou in Frameworks */ = {isa = PBXBuildFile; productRef = 49AE36FE26D4455800EF4E52 /* Gekidou */; }; + 49AE370126D4455D00EF4E52 /* Gekidou in Frameworks */ = {isa = PBXBuildFile; productRef = 49AE370026D4455D00EF4E52 /* Gekidou */; }; + 49AE370526D5CD7800EF4E52 /* Gekidou in Frameworks */ = {isa = PBXBuildFile; productRef = 49AE370426D5CD7800EF4E52 /* Gekidou */; }; 49B4C050230C981C006E919E /* libUploadAttachments.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7FABE04522137F2A00D0F595 /* libUploadAttachments.a */; }; 531BEBC72513E93C00BC05B1 /* compass-icons.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 531BEBC52513E93C00BC05B1 /* compass-icons.ttf */; }; 536CC6C323E79287002C478C /* RNNotificationEventHandler+HandleReplyAction.m in Sources */ = {isa = PBXBuildFile; fileRef = 536CC6C123E79287002C478C /* RNNotificationEventHandler+HandleReplyAction.m */; }; @@ -52,13 +52,6 @@ /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ - 49415293267A911C0039D64E /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 4941528F267A911C0039D64E /* DatabaseHelper.xcodeproj */; - proxyType = 2; - remoteGlobalIDString = 49415253267955890039D64E; - remoteInfo = DatabaseHelper; - }; 7F240A21220D3A2300637665 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 83CBB9F71A601CBA00E9B192 /* Project object */; @@ -101,27 +94,6 @@ remoteGlobalIDString = 7FABE03522137F2900D0F595; remoteInfo = UploadAttachments; }; - 7FE6E441267C014600C99C18 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 4941528F267A911C0039D64E /* DatabaseHelper.xcodeproj */; - proxyType = 1; - remoteGlobalIDString = 49415252267955890039D64E; - remoteInfo = DatabaseHelper; - }; - 7FE6E444267C014E00C99C18 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 4941528F267A911C0039D64E /* DatabaseHelper.xcodeproj */; - proxyType = 1; - remoteGlobalIDString = 49415252267955890039D64E; - remoteInfo = DatabaseHelper; - }; - 7FE6E446267C015400C99C18 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 4941528F267A911C0039D64E /* DatabaseHelper.xcodeproj */; - proxyType = 1; - remoteGlobalIDString = 49415252267955890039D64E; - remoteInfo = DatabaseHelper; - }; /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -170,13 +142,13 @@ 34B20A903038487E8D7DEA1E /* Roboto-Light.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "Roboto-Light.ttf"; path = "../assets/fonts/Roboto-Light.ttf"; sourceTree = ""; }; 3647DF63D6764CF093375861 /* OpenSans-ExtraBold.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "OpenSans-ExtraBold.ttf"; path = "../assets/fonts/OpenSans-ExtraBold.ttf"; sourceTree = ""; }; 41F3AFE83AAF4B74878AB78A /* OpenSans-Italic.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "OpenSans-Italic.ttf"; path = "../assets/fonts/OpenSans-Italic.ttf"; sourceTree = ""; }; - 4941528F267A911C0039D64E /* DatabaseHelper.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = DatabaseHelper.xcodeproj; path = DatabaseHelper/DatabaseHelper.xcodeproj; sourceTree = ""; }; 4953BF5F2368AE8600593328 /* SwimeProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = SwimeProxy.swift; path = Mattermost/SwimeProxy.swift; sourceTree = ""; }; 495BC95F23565ABF00C40C83 /* libXCDYouTubeKit.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; path = libXCDYouTubeKit.a; sourceTree = BUILT_PRODUCTS_DIR; }; 495BC96123565ADD00C40C83 /* libYoutubePlayer-in-WKWebView.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; path = "libYoutubePlayer-in-WKWebView.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 499F7AF0235511FC00E7AF6E /* Mattermost-Regular.otf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "Mattermost-Regular.otf"; path = "../assets/fonts/Mattermost-Regular.otf"; sourceTree = ""; }; 499F7B3F235513F600E7AF6E /* libXCDYouTubeKit.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; path = libXCDYouTubeKit.a; sourceTree = BUILT_PRODUCTS_DIR; }; 499F7B412355141200E7AF6E /* libYoutubePlayer-in-WKWebView.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; path = "libYoutubePlayer-in-WKWebView.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 49AE36FB26D4452900EF4E52 /* Gekidou */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Gekidou; sourceTree = ""; }; 49B4BF51230C83D2006E919E /* libz.1.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libz.1.dylib; path = ../../../../../../../../../usr/lib/libz.1.dylib; sourceTree = ""; }; 531BEBC52513E93C00BC05B1 /* compass-icons.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "compass-icons.ttf"; path = "../assets/fonts/compass-icons.ttf"; sourceTree = ""; }; 536CC6C123E79287002C478C /* RNNotificationEventHandler+HandleReplyAction.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "RNNotificationEventHandler+HandleReplyAction.m"; path = "Mattermost/RNNotificationEventHandler+HandleReplyAction.m"; sourceTree = ""; }; @@ -250,7 +222,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 49415295267A91230039D64E /* libDatabaseHelper.a in Frameworks */, + 49AE370526D5CD7800EF4E52 /* Gekidou in Frameworks */, 49B4C050230C981C006E919E /* libUploadAttachments.a in Frameworks */, 6C9B1EFD6561083917AF06CF /* libPods-Mattermost.a in Frameworks */, ); @@ -260,7 +232,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 49415296267A91290039D64E /* libDatabaseHelper.a in Frameworks */, + 49AE36FF26D4455800EF4E52 /* Gekidou in Frameworks */, 7FABE0562213884700D0F595 /* libUploadAttachments.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -269,7 +241,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 49415297267A91300039D64E /* libDatabaseHelper.a in Frameworks */, + 49AE370126D4455D00EF4E52 /* Gekidou in Frameworks */, 7F581F78221EEA7C0099E66B /* libUploadAttachments.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -369,14 +341,6 @@ name = "Recovered References"; sourceTree = ""; }; - 49415290267A911C0039D64E /* Products */ = { - isa = PBXGroup; - children = ( - 49415294267A911C0039D64E /* libDatabaseHelper.a */, - ); - name = Products; - sourceTree = ""; - }; 4B992D7BAAEDF8759DB525B5 /* Frameworks */ = { isa = PBXGroup; children = ( @@ -453,7 +417,7 @@ 832341AE1AAA6A7D00B99B32 /* Libraries */ = { isa = PBXGroup; children = ( - 4941528F267A911C0039D64E /* DatabaseHelper.xcodeproj */, + 49AE36FB26D4452900EF4E52 /* Gekidou */, 7FABE04022137F2900D0F595 /* UploadAttachments.xcodeproj */, ); name = Libraries; @@ -510,12 +474,14 @@ buildRules = ( ); dependencies = ( - 7FE6E442267C014600C99C18 /* PBXTargetDependency */, 7FAB45BA222DD0E300EBFFC8 /* PBXTargetDependency */, 7F240A22220D3A2300637665 /* PBXTargetDependency */, 7F581D38221ED5C60099E66B /* PBXTargetDependency */, ); name = Mattermost; + packageProductDependencies = ( + 49AE370426D5CD7800EF4E52 /* Gekidou */, + ); productName = "Hello World"; productReference = 13B07F961A680F5B00A75B9A /* Mattermost.app */; productType = "com.apple.product-type.application"; @@ -531,10 +497,12 @@ buildRules = ( ); dependencies = ( - 7FE6E445267C014E00C99C18 /* PBXTargetDependency */, 7FAB4571222DD0DA00EBFFC8 /* PBXTargetDependency */, ); name = MattermostShare; + packageProductDependencies = ( + 49AE36FE26D4455800EF4E52 /* Gekidou */, + ); productName = MattermostShare; productReference = 7F240A19220D3A2300637665 /* MattermostShare.appex */; productType = "com.apple.product-type.app-extension"; @@ -550,10 +518,12 @@ buildRules = ( ); dependencies = ( - 7FE6E447267C015400C99C18 /* PBXTargetDependency */, 7FE5F962227739E600FEFFE1 /* PBXTargetDependency */, ); name = NotificationService; + packageProductDependencies = ( + 49AE370026D4455D00EF4E52 /* Gekidou */, + ); productName = NotificationService; productReference = 7F581D32221ED5C60099E66B /* NotificationService.appex */; productType = "com.apple.product-type.app-extension"; @@ -625,10 +595,6 @@ productRefGroup = 83CBBA001A601CBA00E9B192 /* Products */; projectDirPath = ""; projectReferences = ( - { - ProductGroup = 49415290267A911C0039D64E /* Products */; - ProjectRef = 4941528F267A911C0039D64E /* DatabaseHelper.xcodeproj */; - }, { ProductGroup = 7FABE04122137F2900D0F595 /* Products */; ProjectRef = 7FABE04022137F2900D0F595 /* UploadAttachments.xcodeproj */; @@ -644,13 +610,6 @@ /* End PBXProject section */ /* Begin PBXReferenceProxy section */ - 49415294267A911C0039D64E /* libDatabaseHelper.a */ = { - isa = PBXReferenceProxy; - fileType = archive.ar; - path = libDatabaseHelper.a; - remoteRef = 49415293267A911C0039D64E /* PBXContainerItemProxy */; - sourceTree = BUILT_PRODUCTS_DIR; - }; 7FABE04522137F2A00D0F595 /* libUploadAttachments.a */ = { isa = PBXReferenceProxy; fileType = archive.ar; @@ -872,21 +831,6 @@ name = UploadAttachments; targetProxy = 7FE5F961227739E600FEFFE1 /* PBXContainerItemProxy */; }; - 7FE6E442267C014600C99C18 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - name = DatabaseHelper; - targetProxy = 7FE6E441267C014600C99C18 /* PBXContainerItemProxy */; - }; - 7FE6E445267C014E00C99C18 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - name = DatabaseHelper; - targetProxy = 7FE6E444267C014E00C99C18 /* PBXContainerItemProxy */; - }; - 7FE6E447267C015400C99C18 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - name = DatabaseHelper; - targetProxy = 7FE6E446267C015400C99C18 /* PBXContainerItemProxy */; - }; /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ @@ -917,12 +861,14 @@ ENABLE_BITCODE = NO; HEADER_SEARCH_PATHS = ( "$(inherited)", - "$(SRCROOT)/DatabaseHelper/DatabaseHelper", "$(SRCROOT)/UploadAttachments/UploadAttachments", ); INFOPLIST_FILE = Mattermost/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 12.1; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); OTHER_CFLAGS = ( "$(inherited)", "-DFB_SONARKIT_ENABLED=1", @@ -960,12 +906,14 @@ ENABLE_BITCODE = NO; HEADER_SEARCH_PATHS = ( "$(inherited)", - "$(SRCROOT)/DatabaseHelper/DatabaseHelper", "$(SRCROOT)/UploadAttachments/UploadAttachments", ); INFOPLIST_FILE = Mattermost/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 12.1; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); OTHER_CFLAGS = ( "$(inherited)", "-DFB_SONARKIT_ENABLED=1", @@ -1012,8 +960,12 @@ GCC_C_LANGUAGE_STANDARD = gnu11; HEADER_SEARCH_PATHS = "$(SRCROOT)/UploadAttachments/UploadAttachments"; INFOPLIST_FILE = MattermostShare/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 12.1; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; OTHER_CFLAGS = ( @@ -1058,8 +1010,12 @@ GCC_C_LANGUAGE_STANDARD = gnu11; HEADER_SEARCH_PATHS = "$(SRCROOT)/UploadAttachments/UploadAttachments"; INFOPLIST_FILE = MattermostShare/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 12.1; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); MTL_FAST_MATH = YES; OTHER_CFLAGS = ( "$(inherited)", @@ -1068,8 +1024,9 @@ PRODUCT_BUNDLE_IDENTIFIER = com.mattermost.rnbeta.MattermostShare; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; + SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OBJC_BRIDGING_HEADER = "MattermostShare/MattermostShare-Bridging-Header.h"; - SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_OPTIMIZATION_LEVEL = "-O"; SWIFT_VERSION = 4.2; TARGETED_DEVICE_FAMILY = "1,2"; }; @@ -1100,8 +1057,12 @@ DEVELOPMENT_TEAM = UQ8HT4Q2XM; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = NotificationService/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 12.1; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; OTHER_CFLAGS = ( @@ -1144,8 +1105,12 @@ DEVELOPMENT_TEAM = UQ8HT4Q2XM; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = NotificationService/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 12.1; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); MTL_FAST_MATH = YES; OTHER_CFLAGS = ( "$(inherited)", @@ -1154,7 +1119,8 @@ PRODUCT_BUNDLE_IDENTIFIER = com.mattermost.rnbeta.NotificationService; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; - SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; SWIFT_VERSION = 4.2; TARGETED_DEVICE_FAMILY = "1,2"; }; @@ -1199,7 +1165,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.1; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -1239,7 +1205,7 @@ GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; HEADER_SEARCH_PATHS = ""; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.1; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; VALIDATE_PRODUCT = YES; @@ -1286,6 +1252,21 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + +/* Begin XCSwiftPackageProductDependency section */ + 49AE36FE26D4455800EF4E52 /* Gekidou */ = { + isa = XCSwiftPackageProductDependency; + productName = Gekidou; + }; + 49AE370026D4455D00EF4E52 /* Gekidou */ = { + isa = XCSwiftPackageProductDependency; + productName = Gekidou; + }; + 49AE370426D5CD7800EF4E52 /* Gekidou */ = { + isa = XCSwiftPackageProductDependency; + productName = Gekidou; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = 83CBB9F71A601CBA00E9B192 /* Project object */; } diff --git a/ios/Mattermost.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ios/Mattermost.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 000000000..2877a162d --- /dev/null +++ b/ios/Mattermost.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,16 @@ +{ + "object": { + "pins": [ + { + "package": "SQLite.swift", + "repositoryURL": "https://github.com/stephencelis/SQLite.swift.git", + "state": { + "branch": null, + "revision": "9af51e2edf491c0ea632e369a6566e09b65aa333", + "version": "0.13.0" + } + } + ] + }, + "version": 1 +} diff --git a/ios/Mattermost/AppDelegate.m b/ios/Mattermost/AppDelegate.m index f0e230390..829b6b3ed 100644 --- a/ios/Mattermost/AppDelegate.m +++ b/ios/Mattermost/AppDelegate.m @@ -9,13 +9,14 @@ #import #import #import -#import #import #import #import "Mattermost-Swift.h" #import +@import Gekidou; + @interface AppDelegate () @property (nonatomic, strong) UMModuleRegistryAdapter *moduleRegistryAdapter; @@ -91,15 +92,7 @@ NSString* const NOTIFICATION_UPDATE_BADGE_ACTION = @"update_badge"; UIApplicationState state = [UIApplication sharedApplication].applicationState; NSString* action = [userInfo objectForKey:@"type"]; NSString* channelId = [userInfo objectForKey:@"channel_id"]; - NSString* ackId = [userInfo objectForKey:@"ack_id"]; - - NSString* serverUrl = [userInfo objectForKey:@"server_url"]; - if (serverUrl == nil) { - NSString* onlyServerUrl = [[DatabaseHelper default] getOnlyServerUrlObjc]; - if ([onlyServerUrl length] > 0) { - serverUrl = onlyServerUrl; - } - } + RuntimeUtils *utils = [[RuntimeUtils alloc] init]; @@ -107,10 +100,9 @@ NSString* const NOTIFICATION_UPDATE_BADGE_ACTION = @"update_badge"; // If received a notification that a channel was read, remove all notifications from that channel (only with app in foreground/background) [self cleanNotificationsFromChannel:channelId]; } - - // TODO: Fetch channel data if action is of type message - [[UploadSession shared] notificationReceiptWithNotificationId:ackId serverUrl:serverUrl receivedAt:round([[NSDate date] timeIntervalSince1970] * 1000.0) type:action]; + [[Network default] postNotificationReceipt:userInfo]; + [utils delayWithSeconds:0.2 closure:^(void) { // This is to notify the NotificationCenter that something has changed. completionHandler(UIBackgroundFetchResultNewData); diff --git a/ios/Mattermost/RNNotificationEventHandler+HandleReplyAction.m b/ios/Mattermost/RNNotificationEventHandler+HandleReplyAction.m index a3e1b3051..3ab545be3 100644 --- a/ios/Mattermost/RNNotificationEventHandler+HandleReplyAction.m +++ b/ios/Mattermost/RNNotificationEventHandler+HandleReplyAction.m @@ -10,8 +10,8 @@ #import "RNNotificationEventHandler+HandleReplyAction.h" #import #import -#import #import +@import Gekidou; #define notificationCenterKey @"notificationCenter" @@ -50,7 +50,7 @@ NSString *const ReplyActionID = @"REPLY_ACTION"; NSString *serverUrl = [parsedResponse valueForKeyPath:@"notification.server_url"]; if (serverUrl == nil) { - NSString* onlyServerUrl = [[DatabaseHelper default] getOnlyServerUrlObjc]; + NSString* onlyServerUrl = [[Database default] getOnlyServerUrlObjc]; if ([onlyServerUrl length] > 0) { serverUrl = onlyServerUrl; } else { @@ -58,7 +58,7 @@ NSString *const ReplyActionID = @"REPLY_ACTION"; } } - NSString *sessionToken = [store getTokenForServerUrl:serverUrl]; + NSString *sessionToken = [[Keychain default] getTokenObjcFor:serverUrl]; if (sessionToken == nil) { [self handleReplyFailure:@"" completionHandler:notificationCompletionHandler]; return; diff --git a/ios/MattermostShare/ShareViewController.swift b/ios/MattermostShare/ShareViewController.swift index 5fb0fd3be..a15db28aa 100644 --- a/ios/MattermostShare/ShareViewController.swift +++ b/ios/MattermostShare/ShareViewController.swift @@ -2,7 +2,7 @@ import UIKit import Social import MobileCoreServices import UploadAttachments -import DatabaseHelper +import Gekidou import LocalAuthentication extension Bundle { @@ -40,8 +40,10 @@ class ShareViewController: SLComposeServiceViewController { // TODO: If we don't have a single server then we'll need the user to // select the server from a dropdown. Once the server is selected we // can fetch its token. - serverUrl = try? DatabaseHelper.default.getOnlyServerUrl() - sessionToken = serverUrl != nil ? store.getTokenForServerUrl(serverUrl) : nil + serverUrl = try? Database.default.getOnlyServerUrl() + if (serverUrl != nil), let token = try? Keychain.default.getToken(for: serverUrl!) { + sessionToken = token + } maxMessageSize = Int(store.getMaxPostSize()) canUploadFiles = store.getCanUploadFiles() diff --git a/ios/NotificationService/NotificationService.swift b/ios/NotificationService/NotificationService.swift index 01dc89ef0..562dc21f0 100644 --- a/ios/NotificationService/NotificationService.swift +++ b/ios/NotificationService/NotificationService.swift @@ -1,4 +1,4 @@ -import DatabaseHelper +import Gekidou import UserNotifications import UploadAttachments @@ -14,71 +14,49 @@ class NotificationService: UNNotificationServiceExtension { let fibonacciBackoffsInSeconds = [1.0, 2.0, 3.0, 5.0, 8.0] - func fetchReceipt(notificationId: String, serverUrl: String?, receivedAt: Int, type: String, postId: String, idLoaded: Bool ) -> Void { + func fetchReceipt(_ ackNotification: AckNotification) -> Void { if (self.retryIndex >= fibonacciBackoffsInSeconds.count) { contentHandler(self.bestAttemptContent!) return } - UploadSession.shared.notificationReceipt( - notificationId: notificationId, - serverUrl: serverUrl, - receivedAt: receivedAt, - type: type, - postId: postId, - idLoaded: idLoaded) { data, response, error in + Network.default.postNotificationReceipt(ackNotification) { data, response, error in if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode != 200 { contentHandler(self.bestAttemptContent!) return } guard let data = data, error == nil else { - if (idLoaded) { + if (ackNotification.isIdLoaded) { // Receipt retrieval failed. Kick off retries. let backoffInSeconds = fibonacciBackoffsInSeconds[self.retryIndex] DispatchQueue.main.asyncAfter(deadline: .now() + backoffInSeconds, execute: { - fetchReceipt( - notificationId: notificationId, - serverUrl: serverUrl, - receivedAt: Date().millisecondsSince1970, - type: type, - postId: postId, - idLoaded: idLoaded - ) + fetchReceipt(ackNotification) }) self.retryIndex += 1 } return } - self.processResponse(data: data, bestAttemptContent: self.bestAttemptContent!, contentHandler: contentHandler) + + self.processResponse(serverUrl: ackNotification.serverUrl, data: data, bestAttemptContent: self.bestAttemptContent!, contentHandler: contentHandler) } } bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent) - if let bestAttemptContent = bestAttemptContent { - let ackId = (bestAttemptContent.userInfo["ack_id"] ?? "") as! String - let type = (bestAttemptContent.userInfo["type"] ?? "") as! String - let postId = (bestAttemptContent.userInfo["post_id"] ?? "") as! String - let idLoaded = (bestAttemptContent.userInfo["id_loaded"] ?? false) as! Bool - var serverUrl = (bestAttemptContent.userInfo["server_url"]) as! String? - if (serverUrl == nil) { - serverUrl = try? DatabaseHelper.default.getOnlyServerUrl() - } - - fetchReceipt( - notificationId: ackId, - serverUrl: serverUrl, - receivedAt: Date().millisecondsSince1970, - type: type, - postId: postId, - idLoaded: idLoaded - ) + if let bestAttemptContent = bestAttemptContent, + let jsonData = try? JSONSerialization.data(withJSONObject: bestAttemptContent.userInfo), + let ackNotification = try? JSONDecoder().decode(AckNotification.self, from: jsonData) { + fetchReceipt(ackNotification) + } else { + contentHandler(request.content) } } - func processResponse(data: Data, bestAttemptContent: UNMutableNotificationContent, contentHandler: ((UNNotificationContent) -> Void)?) { + func processResponse(serverUrl: String, data: Data, bestAttemptContent: UNMutableNotificationContent, contentHandler: ((UNNotificationContent) -> Void)?) { + bestAttemptContent.userInfo["server_url"] = serverUrl + let json = try? JSONSerialization.jsonObject(with: data) as! [String: Any] if let json = json { if let message = json["message"] as? String { @@ -95,9 +73,8 @@ class NotificationService: UNNotificationServiceExtension { } } } - if let contentHandler = contentHandler { - contentHandler(bestAttemptContent) - } + + Network.default.fetchAndStoreDataForPushNotification(bestAttemptContent, withContentHandler: contentHandler) } override func serviceExtensionTimeWillExpire() { diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 792d31ede..e570c5b4e 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -721,7 +721,7 @@ SPEC CHECKSUMS: EXFileSystem: 0a04aba8da751b9ac954065911bcf166503f8267 ExpoModulesCore: 2734852616127a6c1fc23012197890a6f3763dc7 FBLazyVector: e686045572151edef46010a6f819ade377dfeb4b - FBReactNativeSpec: cef0cc6d50abc92e8cf52f140aa22b5371cfec0b + FBReactNativeSpec: 8d2166c0d020bf0a6a619c3d36b9a167a4c339b9 glog: 73c2498ac6884b13ede40eda8228cb1eee9d9d62 jail-monkey: 07b83767601a373db876e939b8dbf3f5eb15f073 libwebp: e90b9c01d99205d03b6bb8f2c8c415e5a4ef66f0 diff --git a/ios/UploadAttachments/UploadAttachments.xcodeproj/project.pbxproj b/ios/UploadAttachments/UploadAttachments.xcodeproj/project.pbxproj index 3ae54f662..feb40789e 100644 --- a/ios/UploadAttachments/UploadAttachments.xcodeproj/project.pbxproj +++ b/ios/UploadAttachments/UploadAttachments.xcodeproj/project.pbxproj @@ -133,6 +133,8 @@ dependencies = ( ); name = UploadAttachments; + packageProductDependencies = ( + ); productName = UploadAttachments; productReference = 7FABE03622137F2900D0F595 /* libUploadAttachments.a */; productType = "com.apple.product-type.library.static"; diff --git a/ios/UploadAttachments/UploadAttachments/StoreManager.h b/ios/UploadAttachments/UploadAttachments/StoreManager.h index 6c86b59a5..c740d3fac 100644 --- a/ios/UploadAttachments/UploadAttachments/StoreManager.h +++ b/ios/UploadAttachments/UploadAttachments/StoreManager.h @@ -20,7 +20,8 @@ -(UInt64)getMaxFileSize; -(UInt64)getMaxPostSize; -(NSArray *)getMyTeams; --(NSString *)getTokenForServerUrl:(NSString *)url; +-(NSString *)getServerUrl; +-(NSString *)getToken; -(BOOL)getCanUploadFiles; -(void)updateEntities:(NSString *)content; @end diff --git a/ios/UploadAttachments/UploadAttachments/StoreManager.m b/ios/UploadAttachments/UploadAttachments/StoreManager.m index bd8eb4d45..5292ff3ba 100644 --- a/ios/UploadAttachments/UploadAttachments/StoreManager.m +++ b/ios/UploadAttachments/UploadAttachments/StoreManager.m @@ -1,6 +1,5 @@ #import "StoreManager.h" #import "MMMConstants.h" -#import @implementation StoreManager +(instancetype)shared { @@ -144,23 +143,27 @@ return [self sortDictArrayByDisplayName:myTeams]; } --(NSString *)getTokenForServer:(NSString *)url { +-(NSString *)getServerUrl { + NSDictionary *general = [self.entities objectForKey:@"general"]; + NSDictionary *credentials = [general objectForKey:@"credentials"]; + + if (credentials) { + return [credentials objectForKey:@"url"]; + } + + return nil; +} + +-(NSString *)getToken { NSBundle *bundle = [NSBundle mainBundle]; NSString *appGroupId = [bundle objectForInfoDictionaryKey:@"AppGroupIdentifier"]; NSDictionary *options = @{ @"accessGroup": appGroupId }; - - NSString *serverUrl = url; - if (serverUrl == nil) { - NSString* onlyServerUrl = [[DatabaseHelper default] getOnlyServerUrlObjc]; - if ([onlyServerUrl length] > 0) { - serverUrl = onlyServerUrl; - } - } + NSString* serverUrl = [self getServerUrl]; if (serverUrl) { - NSDictionary *credentials = [self.keychain getInternetCredentialsForServer:serverUrl withOptions:options]; + NSDictionary *credentials = [self.keychain getInternetCredentialsForServer:[self getServerUrl] withOptions:options]; return [credentials objectForKey:@"password"]; } diff --git a/ios/UploadAttachments/UploadAttachments/UploadSession.swift b/ios/UploadAttachments/UploadAttachments/UploadSession.swift index aa28a4090..74f8e0546 100644 --- a/ios/UploadAttachments/UploadAttachments/UploadSession.swift +++ b/ios/UploadAttachments/UploadAttachments/UploadSession.swift @@ -19,7 +19,7 @@ import os.log let store = StoreManager.shared() as StoreManager let _ = store.getEntities(true) - if let serverUrl = uploadSessionData.serverUrl, let sessionToken = store.getTokenForServerUrl(serverUrl) { + if let serverUrl = uploadSessionData.serverUrl, let sessionToken = store.getToken() { let urlString = "\(serverUrl)/api/v4/posts" guard let url = URL(string: urlString) else { @@ -135,41 +135,4 @@ import os.log } }) } - - public func notificationReceipt(notificationId: Any?, serverUrl: String?, receivedAt: Int, type: Any?) { - notificationReceipt(notificationId:notificationId, serverUrl:serverUrl, receivedAt:receivedAt, type:type, postId:nil, idLoaded:false, completion:{_, _, _ in}) - } - - public func notificationReceipt(notificationId: Any?, serverUrl: String?, receivedAt: Int, type: Any?, postId: Any? = nil, idLoaded: Bool, completion: @escaping (Data?, URLResponse?, Error?) -> Void) { - if (notificationId != nil && serverUrl != nil) { - let store = StoreManager.shared() as StoreManager - - if let _ = store.getEntities(true), let sessionToken = store.getTokenForServerUrl(serverUrl) { - let urlString = "\(serverUrl!)/api/v4/notifications/ack" - - let jsonObject: [String: Any] = [ - "id": notificationId as Any, - "received_at": receivedAt, - "platform": "ios", - "type": type as Any, - "post_id": postId as Any, - "is_id_loaded": idLoaded as Bool - ] - - if !JSONSerialization.isValidJSONObject(jsonObject) {return} - - guard let url = URL(string: urlString) else {return} - var request = URLRequest(url: url) - request.httpMethod = "POST" - request.setValue("Bearer \(sessionToken)", forHTTPHeaderField: "Authorization") - request.setValue("application/json; charset=utf-8", forHTTPHeaderField: "Content-Type") - request.httpBody = try? JSONSerialization.data(withJSONObject: jsonObject, options: .prettyPrinted) - - let task = URLSession(configuration: .ephemeral).dataTask(with: request) { data, response, error in - completion(data, response, error) - } - task.resume() - } - } - } }