diff --git a/android/app/src/main/java/com/mattermost/rnbeta/CustomPushNotification.java b/android/app/src/main/java/com/mattermost/rnbeta/CustomPushNotification.java index 1d185c48b..d864b2a7a 100644 --- a/android/app/src/main/java/com/mattermost/rnbeta/CustomPushNotification.java +++ b/android/app/src/main/java/com/mattermost/rnbeta/CustomPushNotification.java @@ -94,7 +94,13 @@ public class CustomPushNotification extends PushNotification { Bundle data = mNotificationProps.asBundle(); final String channelId = data.getString("channel_id"); final String type = data.getString("type"); + final String ackId = data.getString("ack_id"); int notificationId = MESSAGE_NOTIFICATION_ID; + + if (ackId != null) { + notificationReceiptDelivery(ackId, type); + } + if (channelId != null) { notificationId = channelId.hashCode(); Object objCount = channelIdToNotificationCount.get(channelId); @@ -379,4 +385,8 @@ public class CustomPushNotification extends PushNotification { String sender = String.format("%s: ", getSenderName(senderName, channelName, message)); return message.replaceFirst(sender, ""); } + + private void notificationReceiptDelivery(String ackId, String type) { + ReceiptDelivery.send(context, ackId, type); + } } diff --git a/android/app/src/main/java/com/mattermost/rnbeta/ReceiptDelivery.java b/android/app/src/main/java/com/mattermost/rnbeta/ReceiptDelivery.java new file mode 100644 index 000000000..64ec0d96f --- /dev/null +++ b/android/app/src/main/java/com/mattermost/rnbeta/ReceiptDelivery.java @@ -0,0 +1,89 @@ +package com.mattermost.rnbeta; + +import android.content.Context; +import android.support.annotation.Nullable; +import android.util.Log; +import java.lang.System; + +import okhttp3.Call; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; + +import org.json.JSONObject; +import org.json.JSONException; + +import com.mattermost.react_native_interface.ResolvePromise; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.WritableMap; +import com.oblador.keychain.KeychainModule; + +public class ReceiptDelivery { + public static void send (Context context, final String ackId, final String type) { + final ReactApplicationContext reactApplicationContext = new ReactApplicationContext(context); + final KeychainModule keychainModule = new KeychainModule(reactApplicationContext); + + keychainModule.getGenericPasswordForOptions(null, new ResolvePromise() { + @Override + public void resolve(@Nullable Object value) { + if (value instanceof Boolean && !(Boolean)value) { + return; + } + + WritableMap map = (WritableMap) value; + if (map != null) { + String[] credentials = map.getString("password").split(",[ ]*"); + String token = null; + String serverUrl = null; + if (credentials.length == 2) { + token = credentials[0]; + serverUrl = credentials[1]; + + } + + Log.i("ReactNative", String.format("Send receipt delivery ACK=%s TYPE=%s to URL=%s with TOKEN=%s", ackId, type, serverUrl, token)); + execute(serverUrl, token, ackId, type); + } + } + }); + } + + protected static void execute(String serverUrl, String token, String ackId, String type) { + if (token == null || serverUrl == null) { + return; + } + + JSONObject json; + long receivedAt = System.currentTimeMillis(); + + try { + json = new JSONObject(); + json.put("id", ackId); + json.put("received_at", receivedAt); + json.put("platform", "android"); + json.put("type", type); + } catch (JSONException e) { + Log.e("ReactNative", "Receipt delivery failed to build json payload"); + return; + } + + final OkHttpClient client = new OkHttpClient(); + final MediaType JSON = MediaType.parse("application/json; charset=utf-8"); + RequestBody body = RequestBody.create(JSON, json.toString()); + Request request = new Request.Builder() + .header("Authorization", String.format("Bearer %s", token)) + .header("Content-Type", "application/json") + .url(String.format("%s/api/v4/notifications/ack", serverUrl.replaceAll("/$", ""))) + .post(body) + .build(); + + try { + client.newCall(request).execute(); + } catch (Exception e) { + Log.e("ReactNative", "Receipt delivery failed to send"); + } + + } +} diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 104b814be..d01d70ecc 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -237,6 +237,20 @@ platform :ios do display_name: app_name ) + # Set the notification service extension bundle identifier + notification_bundle_id = ENV['NOTIFICATION_SERVICE_IDENTIFIER'] || 'com.mattermost.rnbeta.NotificationService' + update_app_identifier( + xcodeproj: './ios/Mattermost.xcodeproj', + plist_path: 'NotificationService/Info.plist', + app_identifier: notification_bundle_id + ) + + find_replace_string( + path_to_file: './ios/Mattermost.xcodeproj/project.pbxproj', + old_string: 'com.mattermost.rnbeta.NotificationService', + new_string: notification_bundle_id + ) + # Set the share extension bundle identifier extension_bundle_id = ENV['EXTENSION_APP_IDENTIFIER'] || 'com.mattermost.rnbeta.MattermostShare' update_app_identifier( @@ -299,6 +313,11 @@ platform :ios do app_group_identifiers: [app_group_id] ) + update_app_group_identifiers( + entitlements_file: './ios/NotificationService/NotificationService.entitlements', + app_group_identifiers: [app_group_id] + ) + # Sync the provisioning profiles using match if ENV['SYNC_PROVISIONING_PROFILES'] == 'true' match(type: ENV['MATCH_TYPE'] || 'adhoc') @@ -381,11 +400,11 @@ platform :android do lane :configure_telemetry_android do if ENV['TELEMETRY_ENABLED'] == 'true' && ENV['TELEMETRY_URL'] != '' && ENV['TELEMETRY_API_KEY'] != '' # Update telemetry config - json = load_config_json + json = load_config_json('../dist/assets/config.json') json['TelemetryEnabled'] = true json['TelemetryUrl'] = ENV['TELEMETRY_URL'] json['TelemetryApiKey'] = ENV['TELEMETRY_API_KEY'] - save_config_json(json) + save_config_json('../dist/assets/config.json', json) beta_dir = './android/app/src/main/java/com/mattermost/rnbeta/' diff --git a/ios/Mattermost.xcodeproj/project.pbxproj b/ios/Mattermost.xcodeproj/project.pbxproj index a8534bc7e..6dcca1abc 100644 --- a/ios/Mattermost.xcodeproj/project.pbxproj +++ b/ios/Mattermost.xcodeproj/project.pbxproj @@ -74,6 +74,9 @@ 7F43D6061F6BF9EB001FC614 /* libPods-Mattermost.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7F43D6051F6BF9EB001FC614 /* libPods-Mattermost.a */; }; 7F43D63F1F6BFA19001FC614 /* libBVLinearGradient.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 37D8FEC21E80B5230091F3BD /* libBVLinearGradient.a */; }; 7F43D6401F6BFA82001FC614 /* libRCTPushNotification.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7F63D2811E6C957C001FAE12 /* libRCTPushNotification.a */; }; + 7F581D35221ED5C60099E66B /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F581D34221ED5C60099E66B /* NotificationService.swift */; }; + 7F581D39221ED5C60099E66B /* NotificationService.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 7F581D32221ED5C60099E66B /* NotificationService.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 7F581F78221EEA7C0099E66B /* libUploadAttachments.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7FABE04522137F2A00D0F595 /* libUploadAttachments.a */; }; 7F4C2598227E3B11009144EF /* libRNCNetInfo.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7F4C2595227E3AE9009144EF /* libRNCNetInfo.a */; }; 7F5CA9A0208FE3B9004F91CE /* libRNDocumentPicker.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7F5CA991208FE38F004F91CE /* libRNDocumentPicker.a */; }; 7F642DF02093533300F3165E /* libRNDeviceInfo.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7F642DED2093530B00F3165E /* libRNDeviceInfo.a */; }; @@ -475,6 +478,13 @@ remoteGlobalIDString = ED296FEE214C9CF800B7C4FE; remoteInfo = "jsiexecutor-tvOS"; }; + 7F581D37221ED5C60099E66B /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 83CBB9F71A601CBA00E9B192 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 7F581D31221ED5C60099E66B; + remoteInfo = NotificationService; + }; 7F5CA990208FE38F004F91CE /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 7F5CA956208FE38F004F91CE /* RNDocumentPicker.xcodeproj */; @@ -636,6 +646,13 @@ remoteGlobalIDString = 5DBEB1501B18CEA900B34395; remoteInfo = RNVectorIcons; }; + 7FE5F961227739E600FEFFE1 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 7FABE04022137F2900D0F595 /* UploadAttachments.xcodeproj */; + proxyType = 1; + remoteGlobalIDString = 7FABE03522137F2900D0F595; + remoteInfo = UploadAttachments; + }; 7FE5FB3521A1EB710075D1AA /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 2B25899FDAC149EB96ED3305 /* RNVectorIcons.xcodeproj */; @@ -691,6 +708,7 @@ dstSubfolderSpec = 13; files = ( 7F240A23220D3A2300637665 /* MattermostShare.appex in Embed App Extensions */, + 7F581D39221ED5C60099E66B /* NotificationService.appex in Embed App Extensions */, ); name = "Embed App Extensions"; runOnlyForDeploymentPostprocessing = 0; @@ -786,6 +804,10 @@ 7F43D6051F6BF9EB001FC614 /* libPods-Mattermost.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = "libPods-Mattermost.a"; path = "../../../../../../../Library/Developer/Xcode/DerivedData/Mattermost-czlinsdviifujheezzjvmisotjrm/Build/Products/Debug-iphonesimulator/libPods-Mattermost.a"; sourceTree = ""; }; 7F4C258F227E3AE9009144EF /* RNCNetInfo.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RNCNetInfo.xcodeproj; path = "../node_modules/@react-native-community/netinfo/ios/RNCNetInfo.xcodeproj"; sourceTree = ""; }; 7F54ABFAE6CE4A6DB11D1ED7 /* Roboto-BlackItalic.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "Roboto-BlackItalic.ttf"; path = "../assets/fonts/Roboto-BlackItalic.ttf"; sourceTree = ""; }; + 7F581D32221ED5C60099E66B /* NotificationService.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = NotificationService.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + 7F581D34221ED5C60099E66B /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = ""; }; + 7F581D36221ED5C60099E66B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 7F581F77221EEA5A0099E66B /* NotificationService.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = NotificationService.entitlements; sourceTree = ""; }; 7F5CA956208FE38F004F91CE /* RNDocumentPicker.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RNDocumentPicker.xcodeproj; path = "../node_modules/react-native-document-picker/ios/RNDocumentPicker.xcodeproj"; sourceTree = ""; }; 7F63D27B1E6C957C001FAE12 /* RCTPushNotification.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTPushNotification.xcodeproj; path = "../node_modules/react-native/Libraries/PushNotificationIOS/RCTPushNotification.xcodeproj"; sourceTree = ""; }; 7F63D2C21E6DD98A001FAE12 /* Mattermost.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; name = Mattermost.entitlements; path = Mattermost/Mattermost.entitlements; sourceTree = ""; }; @@ -919,6 +941,14 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 7F581D2F221ED5C60099E66B /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 7F581F78221EEA7C0099E66B /* libUploadAttachments.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ @@ -1049,7 +1079,6 @@ 13B07FAE1A68108700A75B9A /* Mattermost */ = { isa = PBXGroup; children = ( - 7F151D43221B082A00FAD8F3 /* Mattermost-Bridging-Header.h */, 13B07FAF1A68108700A75B9A /* AppDelegate.h */, 13B07FB01A68108700A75B9A /* AppDelegate.m */, 7FEB10961F6101710039A015 /* BlurAppScreen.h */, @@ -1061,6 +1090,7 @@ 7FF2AF8A2086483E00FFBDF4 /* KeyShareConsumer.storyboard */, 13B07FB71A68108700A75B9A /* main.m */, 7F63D2C21E6DD98A001FAE12 /* Mattermost.entitlements */, + 7F151D43221B082A00FAD8F3 /* Mattermost-Bridging-Header.h */, 7F240ACB220D460300637665 /* MattermostBucketModule.h */, 7F240ACC220D460300637665 /* MattermostBucketModule.m */, 7FEB10991F61019C0039A015 /* MattermostManaged.h */, @@ -1282,6 +1312,16 @@ name = Products; sourceTree = ""; }; + 7F581D33221ED5C60099E66B /* NotificationService */ = { + isa = PBXGroup; + children = ( + 7F581F77221EEA5A0099E66B /* NotificationService.entitlements */, + 7F581D34221ED5C60099E66B /* NotificationService.swift */, + 7F581D36221ED5C60099E66B /* Info.plist */, + ); + path = NotificationService; + sourceTree = ""; + }; 7F5CA957208FE38F004F91CE /* Products */ = { isa = PBXGroup; children = ( @@ -1501,6 +1541,7 @@ children = ( 13B07FAE1A68108700A75B9A /* Mattermost */, 7F240A1A220D3A2300637665 /* MattermostShare */, + 7F581D33221ED5C60099E66B /* NotificationService */, 7F292A701E8AB73400A450A3 /* SplashScreenResource */, 832341AE1AAA6A7D00B99B32 /* Libraries */, 00E356EF1AD99517003FC87E /* MattermostTests */, @@ -1520,6 +1561,7 @@ 13B07F961A680F5B00A75B9A /* Mattermost.app */, 00E356EE1AD99517003FC87E /* MattermostTests.xctest */, 7F240A19220D3A2300637665 /* MattermostShare.appex */, + 7F581D32221ED5C60099E66B /* NotificationService.appex */, ); name = Products; sourceTree = ""; @@ -1564,6 +1606,7 @@ dependencies = ( 7FAB45BA222DD0E300EBFFC8 /* PBXTargetDependency */, 7F240A22220D3A2300637665 /* PBXTargetDependency */, + 7F581D38221ED5C60099E66B /* PBXTargetDependency */, ); name = Mattermost; productName = "Hello World"; @@ -1588,6 +1631,24 @@ productReference = 7F240A19220D3A2300637665 /* MattermostShare.appex */; productType = "com.apple.product-type.app-extension"; }; + 7F581D31221ED5C60099E66B /* NotificationService */ = { + isa = PBXNativeTarget; + buildConfigurationList = 7F581D82221ED5C60099E66B /* Build configuration list for PBXNativeTarget "NotificationService" */; + buildPhases = ( + 7F581D2E221ED5C60099E66B /* Sources */, + 7F581D2F221ED5C60099E66B /* Frameworks */, + 7F581D30221ED5C60099E66B /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 7FE5F962227739E600FEFFE1 /* PBXTargetDependency */, + ); + name = NotificationService; + productName = NotificationService; + productReference = 7F581D32221ED5C60099E66B /* NotificationService.appex */; + productType = "com.apple.product-type.app-extension"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -1634,6 +1695,16 @@ }; }; }; + 7F581D31221ED5C60099E66B = { + CreatedOnToolsVersion = 10.1; + DevelopmentTeam = UQ8HT4Q2XM; + ProvisioningStyle = Automatic; + SystemCapabilities = { + com.apple.ApplicationGroups.iOS = { + enabled = 1; + }; + }; + }; }; }; buildConfigurationList = 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "Mattermost" */; @@ -1815,6 +1886,7 @@ 13B07F861A680F5B00A75B9A /* Mattermost */, 00E356ED1AD99517003FC87E /* MattermostTests */, 7F240A18220D3A2300637665 /* MattermostShare */, + 7F581D31221ED5C60099E66B /* NotificationService */, ); }; /* End PBXProject section */ @@ -2416,6 +2488,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 7F581D30221ED5C60099E66B /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ @@ -2522,6 +2601,14 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 7F581D2E221ED5C60099E66B /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 7F581D35221ED5C60099E66B /* NotificationService.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ @@ -2535,6 +2622,11 @@ target = 7F240A18220D3A2300637665 /* MattermostShare */; targetProxy = 7F240A21220D3A2300637665 /* PBXContainerItemProxy */; }; + 7F581D38221ED5C60099E66B /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 7F581D31221ED5C60099E66B /* NotificationService */; + targetProxy = 7F581D37221ED5C60099E66B /* PBXContainerItemProxy */; + }; 7FAB4571222DD0DA00EBFFC8 /* PBXTargetDependency */ = { isa = PBXTargetDependency; name = UploadAttachments; @@ -2545,6 +2637,11 @@ name = UploadAttachments; targetProxy = 7FAB45B9222DD0E300EBFFC8 /* PBXContainerItemProxy */; }; + 7FE5F962227739E600FEFFE1 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = UploadAttachments; + targetProxy = 7FE5F961227739E600FEFFE1 /* PBXContainerItemProxy */; + }; /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ @@ -2820,6 +2917,83 @@ }; name = Release; }; + 7F581D3A221ED5C60099E66B /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; + CODE_SIGN_IDENTITY = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + CODE_SIGN_STYLE = Automatic; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = UQ8HT4Q2XM; + GCC_C_LANGUAGE_STANDARD = gnu11; + INFOPLIST_FILE = NotificationService/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 10.3; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.mattermost.rnbeta.NotificationService; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 4.2; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 7F581D3B221ED5C60099E66B /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; + CODE_SIGN_IDENTITY = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + CODE_SIGN_STYLE = Automatic; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = UQ8HT4Q2XM; + GCC_C_LANGUAGE_STANDARD = gnu11; + INFOPLIST_FILE = NotificationService/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 10.3; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.mattermost.rnbeta.NotificationService; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_VERSION = 4.2; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; 83CBBA201A601CBA00E9B192 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -2934,6 +3108,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + 7F581D82221ED5C60099E66B /* Build configuration list for PBXNativeTarget "NotificationService" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 7F581D3A221ED5C60099E66B /* Debug */, + 7F581D3B221ED5C60099E66B /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "Mattermost" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/ios/Mattermost/AppDelegate.m b/ios/Mattermost/AppDelegate.m index 6d8decfec..1e3ff9097 100644 --- a/ios/Mattermost/AppDelegate.m +++ b/ios/Mattermost/AppDelegate.m @@ -120,11 +120,13 @@ NSString* const NotificationClearAction = @"clear"; UIApplicationState state = [UIApplication sharedApplication].applicationState; NSString* action = [userInfo objectForKey:@"type"]; NSString* channelId = [userInfo objectForKey:@"channel_id"]; + NSString* ackId = [userInfo objectForKey:@"ack_id"]; if (action && [action isEqualToString: NotificationClearAction]) { // If received a notification that a channel was read, remove all notifications from that channel (only with app in foreground/background) [self cleanNotificationsFromChannel:channelId andUpdateBadge:NO]; RuntimeUtils *utils = [[RuntimeUtils alloc] init]; + [[UploadSession shared] notificationReceiptWithNotificationId:ackId receivedAt:round([[NSDate date] timeIntervalSince1970] * 1000.0) type:action]; [utils delayWithSeconds:0.2 closure:^(void) { // This is to notify the NotificationCenter that something has changed. completionHandler(UIBackgroundFetchResultNewData); diff --git a/ios/NotificationService/Info.plist b/ios/NotificationService/Info.plist new file mode 100644 index 000000000..7f2ba4b1a --- /dev/null +++ b/ios/NotificationService/Info.plist @@ -0,0 +1,31 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + NotificationService + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + XPC! + CFBundleShortVersionString + 1.19.0 + CFBundleVersion + 188 + NSExtension + + NSExtensionPointIdentifier + com.apple.usernotifications.service + NSExtensionPrincipalClass + $(PRODUCT_MODULE_NAME).NotificationService + + + diff --git a/ios/NotificationService/NotificationService.entitlements b/ios/NotificationService/NotificationService.entitlements new file mode 100644 index 000000000..55fdcdea6 --- /dev/null +++ b/ios/NotificationService/NotificationService.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.application-groups + + group.com.mattermost.rnbeta + + + diff --git a/ios/NotificationService/NotificationService.swift b/ios/NotificationService/NotificationService.swift new file mode 100644 index 000000000..371a7f36f --- /dev/null +++ b/ios/NotificationService/NotificationService.swift @@ -0,0 +1,42 @@ +import UserNotifications +import UploadAttachments + +class NotificationService: UNNotificationServiceExtension { + + var contentHandler: ((UNNotificationContent) -> Void)? + var bestAttemptContent: UNMutableNotificationContent? + + override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { + self.contentHandler = contentHandler + bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent) + + if let bestAttemptContent = bestAttemptContent { + UploadSession.shared.notificationReceipt( + notificationId: bestAttemptContent.userInfo["ack_id"], + receivedAt: Date().millisencondsSince1970, + type: bestAttemptContent.userInfo["type"] + ) + + contentHandler(bestAttemptContent) + } + } + + override func serviceExtensionTimeWillExpire() { + // Called just before the extension will be terminated by the system. + // Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used. + if let contentHandler = contentHandler, let bestAttemptContent = bestAttemptContent { + contentHandler(bestAttemptContent) + } + } + +} + +extension Date { + var millisencondsSince1970: Int { + return Int((self.timeIntervalSince1970 * 1000.0).rounded()) + } + + init(milliseconds: Int) { + self = Date(timeIntervalSince1970: TimeInterval(milliseconds) / 1000) + } +} diff --git a/ios/UploadAttachments/UploadAttachments/UploadSession.swift b/ios/UploadAttachments/UploadAttachments/UploadSession.swift index 47bbde74d..de1b51726 100644 --- a/ios/UploadAttachments/UploadAttachments/UploadSession.swift +++ b/ios/UploadAttachments/UploadAttachments/UploadSession.swift @@ -38,9 +38,9 @@ import os.log if #available(iOS 12.0, *) { os_log(OSLogType.default, "Mattermost will post identifier=%{public}@", identifier) } - + URLSession(configuration: .ephemeral).dataTask(with: request).resume() - + UploadSessionManager.shared.removeUploadSessionData(identifier: identifier) UploadSessionManager.shared.clearTempDirectory() } @@ -56,7 +56,7 @@ import os.log if #available(iOS 11.0, *) { sessionConfig.waitsForConnectivity = true } - + session = URLSession(configuration: sessionConfig, delegate: self, delegateQueue: OperationQueue.main) } @@ -66,32 +66,32 @@ import os.log if #available(iOS 11.0, *) { sessionConfig.waitsForConnectivity = true } - + self.session = URLSession(configuration: sessionConfig, delegate: self, delegateQueue: nil) if #available(iOS 12.0, *) { os_log(OSLogType.default, "Mattermost Session created identifier=%{public}@", identifier) } return self.session! } - + public func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { // here we should get the file Id and update it in the session guard let identifier = session.configuration.identifier else {return} - do { - let jsonObject = try JSONSerialization.jsonObject(with: data, options: []) as! NSDictionary - let fileInfos = jsonObject.object(forKey: "file_infos") as! NSArray - if fileInfos.count > 0 { - let fileInfoData = fileInfos[0] as! NSDictionary - let fileId = fileInfoData.object(forKey: "id") as! String - UploadSessionManager.shared.appendCompletedUploadToSession(identifier: identifier, fileId: fileId) - } - } catch { - if #available(iOS 12.0, *) { - os_log(OSLogType.default, "Mattermost Failed to receive data identifier=%{public}@ error=%{public}", identifier, error.localizedDescription) - } - print("MMLOG: Failed to get the file upload response %@", error.localizedDescription) + do { + let jsonObject = try JSONSerialization.jsonObject(with: data, options: []) as! NSDictionary + let fileInfos = jsonObject.object(forKey: "file_infos") as! NSArray + if fileInfos.count > 0 { + let fileInfoData = fileInfos[0] as! NSDictionary + let fileId = fileInfoData.object(forKey: "id") as! String + UploadSessionManager.shared.appendCompletedUploadToSession(identifier: identifier, fileId: fileId) } - + } catch { + if #available(iOS 12.0, *) { + os_log(OSLogType.default, "Mattermost Failed to receive data identifier=%{public}@ error=%{public}", identifier, error.localizedDescription) + } + print("MMLOG: Failed to get the file upload response %@", error.localizedDescription) + } + } public func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { @@ -123,4 +123,31 @@ import os.log } }) } + + public func notificationReceipt(notificationId: Any?, receivedAt: Int, type: Any?) { + let store = StoreManager.shared() as StoreManager + let _ = store.getEntities(true) + let serverURL = store.getServerUrl() + let sessionToken = store.getToken() + let urlString = "\(serverURL!)/api/v4/notifications/ack" + + if (notificationId != nil) { + let jsonObject: [String: Any] = [ + "id": notificationId as Any, + "received_at": receivedAt, + "platform": "ios", + "type": type as Any + ] + + 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) + URLSession(configuration: .ephemeral).dataTask(with: request).resume() + } + } }