From 1bf870f9ec26bd3fc59eb63f44eda686b8a9589d Mon Sep 17 00:00:00 2001 From: Tomasz Noinski Date: Mon, 14 Nov 2022 09:31:12 +0100 Subject: [PATCH] Implement options to hide or crop attachments in the thumbnail on iOS --- .../example/lib/main.dart | 56 +++++++++++++++++-- .../Classes/FlutterLocalNotificationsPlugin.m | 30 +++++++++- .../platform_specifics/darwin/mappers.dart | 13 ++++- .../darwin/notification_attachment.dart | 41 ++++++++++++++ .../FlutterLocalNotificationsPlugin.swift | 16 +++++- .../ios_flutter_local_notifications_test.dart | 30 ++++++---- ...acos_flutter_local_notifications_test.dart | 30 ++++++---- 7 files changed, 189 insertions(+), 27 deletions(-) diff --git a/flutter_local_notifications/example/lib/main.dart b/flutter_local_notifications/example/lib/main.dart index 0764e3531..42372abd4 100644 --- a/flutter_local_notifications/example/lib/main.dart +++ b/flutter_local_notifications/example/lib/main.dart @@ -826,9 +826,26 @@ class _HomePageState extends State { }, ), PaddedElevatedButton( - buttonText: 'Show notification with attachment', + buttonText: + 'Show notification with attachment (with thumbnail)', onPressed: () async { - await _showNotificationWithAttachment(); + await _showNotificationWithAttachment( + hideThumbnail: false); + }, + ), + PaddedElevatedButton( + buttonText: + 'Show notification with attachment (no thumbnail)', + onPressed: () async { + await _showNotificationWithAttachment( + hideThumbnail: true); + }, + ), + PaddedElevatedButton( + buttonText: + 'Show notification with attachment (clipped thumbnail)', + onPressed: () async { + await _showNotificationWithClippedThumbnailAttachment(); }, ), PaddedElevatedButton( @@ -2162,12 +2179,43 @@ class _HomePageState extends State { payload: 'item x'); } - Future _showNotificationWithAttachment() async { + Future _showNotificationWithAttachment({ + required bool hideThumbnail, + }) async { + final String bigPicturePath = await _downloadAndSaveFile( + 'https://via.placeholder.com/600x200', 'bigPicture.jpg'); + final DarwinNotificationDetails darwinNotificationDetails = + DarwinNotificationDetails(attachments: [ + DarwinNotificationAttachment( + bigPicturePath, + hideThumbnail: hideThumbnail, + ) + ]); + final NotificationDetails notificationDetails = NotificationDetails( + iOS: darwinNotificationDetails, macOS: darwinNotificationDetails); + await flutterLocalNotificationsPlugin.show( + id++, + 'notification with attachment title', + 'notification with attachment body', + notificationDetails); + } + + Future _showNotificationWithClippedThumbnailAttachment() async { final String bigPicturePath = await _downloadAndSaveFile( 'https://via.placeholder.com/600x200', 'bigPicture.jpg'); final DarwinNotificationDetails darwinNotificationDetails = DarwinNotificationDetails(attachments: [ - DarwinNotificationAttachment(bigPicturePath) + DarwinNotificationAttachment( + bigPicturePath, + thumbnailClippingRect: + // lower right quadrant of the attachment + const DarwinNotificationAttachmentThumbnailClippingRect( + x: 0.5, + y: 0.5, + height: 0.5, + width: 0.5, + ), + ) ]); final NotificationDetails notificationDetails = NotificationDetails( iOS: darwinNotificationDetails, macOS: darwinNotificationDetails); diff --git a/flutter_local_notifications/ios/Classes/FlutterLocalNotificationsPlugin.m b/flutter_local_notifications/ios/Classes/FlutterLocalNotificationsPlugin.m index 8bc553a20..2e9140929 100644 --- a/flutter_local_notifications/ios/Classes/FlutterLocalNotificationsPlugin.m +++ b/flutter_local_notifications/ios/Classes/FlutterLocalNotificationsPlugin.m @@ -67,6 +67,8 @@ @implementation FlutterLocalNotificationsPlugin { NSString *const ATTACHMENTS = @"attachments"; NSString *const ATTACHMENT_IDENTIFIER = @"identifier"; NSString *const ATTACHMENT_FILE_PATH = @"filePath"; +NSString *const ATTACHMENT_HIDE_THUMBNAIL = @"hideThumbnail"; +NSString *const ATTACHMENT_THUMBNAIL_CLIPPING_RECT = @"thumbnailClippingRect"; NSString *const INTERRUPTION_LEVEL = @"interruptionLevel"; NSString *const THREAD_IDENTIFIER = @"threadIdentifier"; NSString *const PRESENT_ALERT = @"presentAlert"; @@ -912,6 +914,32 @@ - (void)cancelAll:(FlutterResult _Nonnull)result { [NSMutableArray arrayWithCapacity:attachments.count]; for (NSDictionary *attachment in attachments) { NSError *error; + + NSMutableDictionary *options = [[NSMutableDictionary alloc] init]; + if ([self containsKey:ATTACHMENT_HIDE_THUMBNAIL + forDictionary:attachment]) { + NSNumber *hideThumbnail = attachment[ATTACHMENT_HIDE_THUMBNAIL]; + [options + setObject:hideThumbnail + forKey:UNNotificationAttachmentOptionsThumbnailHiddenKey]; + } + if ([self containsKey:ATTACHMENT_THUMBNAIL_CLIPPING_RECT + forDictionary:attachment]) { + NSDictionary *thumbnailClippingRect = + attachment[ATTACHMENT_THUMBNAIL_CLIPPING_RECT]; + CGRect rect = + CGRectMake([thumbnailClippingRect[@"x"] doubleValue], + [thumbnailClippingRect[@"y"] doubleValue], + [thumbnailClippingRect[@"width"] doubleValue], + [thumbnailClippingRect[@"height"] doubleValue]); + NSDictionary *rectDict = + CFBridgingRelease(CGRectCreateDictionaryRepresentation(rect)); + [options + setObject:rectDict + forKey: + UNNotificationAttachmentOptionsThumbnailClippingRectKey]; + } + UNNotificationAttachment *notificationAttachment = [UNNotificationAttachment attachmentWithIdentifier:attachment[ATTACHMENT_IDENTIFIER] @@ -919,7 +947,7 @@ - (void)cancelAll:(FlutterResult _Nonnull)result { fileURLWithPath: attachment [ATTACHMENT_FILE_PATH]] - options:nil + options:options error:&error]; if (error) { result(getFlutterError(error)); diff --git a/flutter_local_notifications/lib/src/platform_specifics/darwin/mappers.dart b/flutter_local_notifications/lib/src/platform_specifics/darwin/mappers.dart index 32336d134..52fca1c0f 100644 --- a/flutter_local_notifications/lib/src/platform_specifics/darwin/mappers.dart +++ b/flutter_local_notifications/lib/src/platform_specifics/darwin/mappers.dart @@ -48,10 +48,21 @@ extension DarwinInitializationSettingsMapper on DarwinInitializationSettings { }; } -extension DarwinNotificationAttachmentMapper on DarwinNotificationAttachment { +extension on DarwinNotificationAttachmentThumbnailClippingRect { Map toMap() => { + 'x': x, + 'y': y, + 'width': width, + 'height': height, + }; +} + +extension DarwinNotificationAttachmentMapper on DarwinNotificationAttachment { + Map toMap() => { 'identifier': identifier ?? '', 'filePath': filePath, + 'hideThumbnail': hideThumbnail, + 'thumbnailClippingRect': thumbnailClippingRect?.toMap(), }; } diff --git a/flutter_local_notifications/lib/src/platform_specifics/darwin/notification_attachment.dart b/flutter_local_notifications/lib/src/platform_specifics/darwin/notification_attachment.dart index 02c9f95b6..ba7246ca6 100644 --- a/flutter_local_notifications/lib/src/platform_specifics/darwin/notification_attachment.dart +++ b/flutter_local_notifications/lib/src/platform_specifics/darwin/notification_attachment.dart @@ -5,6 +5,8 @@ class DarwinNotificationAttachment { const DarwinNotificationAttachment( this.filePath, { this.identifier, + this.hideThumbnail, + this.thumbnailClippingRect, }); /// The local file path to the attachment. @@ -18,4 +20,43 @@ class DarwinNotificationAttachment { /// When left empty, the platform's native APIs will generate a unique /// identifier final String? identifier; + + /// Should the attachment be considered for the notification thumbnail? + final bool? hideThumbnail; + + /// The clipping rectangle for the thumbnail image. + final DarwinNotificationAttachmentThumbnailClippingRect? + thumbnailClippingRect; +} + +/// Represents the clipping rectangle used for the thumbnail image. +class DarwinNotificationAttachmentThumbnailClippingRect { + /// Constructs an instance of + /// [DarwinNotificationAttachmentThumbnailClippingRect]. + const DarwinNotificationAttachmentThumbnailClippingRect({ + required this.x, + required this.y, + required this.width, + required this.height, + }); + + /// Horizontal offset of the rectangle as proportion of the original image. + /// + /// Value in the range from 0.0 to 1.0. + final double x; + + /// Vertical offset of the rectangle as proportion of the original image. + /// + /// Value in the range from 0.0 to 1.0. + final double y; + + /// Width of the rectangle as proportion of the original image. + /// + /// Value in the range from 0.0 to 1.0. + final double width; + + /// Height of the rectangle as proportion of the original image. + /// + /// Value in the range from 0.0 to 1.0. + final double height; } diff --git a/flutter_local_notifications/macos/Classes/FlutterLocalNotificationsPlugin.swift b/flutter_local_notifications/macos/Classes/FlutterLocalNotificationsPlugin.swift index a44d45fb5..22a13abb9 100644 --- a/flutter_local_notifications/macos/Classes/FlutterLocalNotificationsPlugin.swift +++ b/flutter_local_notifications/macos/Classes/FlutterLocalNotificationsPlugin.swift @@ -35,6 +35,8 @@ public class FlutterLocalNotificationsPlugin: NSObject, FlutterPlugin, UNUserNot static let attachments = "attachments" static let identifier = "identifier" static let filePath = "filePath" + static let hideThumbnail = "hideThumbnail" + static let attachmentThumbnailClippingRect = "thumbnailClippingRect" static let threadIdentifier = "threadIdentifier" static let interruptionLevel = "interruptionLevel" static let actionId = "actionId" @@ -517,7 +519,19 @@ public class FlutterLocalNotificationsPlugin: NSObject, FlutterPlugin, UNUserNot for attachment in attachments { let identifier = attachment[MethodCallArguments.identifier] as! String let filePath = attachment[MethodCallArguments.filePath] as! String - let notificationAttachment = try UNNotificationAttachment.init(identifier: identifier, url: URL.init(fileURLWithPath: filePath)) + var options: [String: Any] = [:] + if let hideThumbnail = attachment[MethodCallArguments.hideThumbnail] as? NSNumber { + options[UNNotificationAttachmentOptionsThumbnailHiddenKey] = hideThumbnail + } + if let thumbnailClippingRect = attachment[MethodCallArguments.attachmentThumbnailClippingRect] as? [String: Any] { + let rect = CGRect(x: thumbnailClippingRect["x"] as! Double, y: thumbnailClippingRect["y"] as! Double, width: thumbnailClippingRect["width"] as! Double, height: thumbnailClippingRect["height"] as! Double) + options[UNNotificationAttachmentOptionsThumbnailClippingRectKey] = CGRectCreateDictionaryRepresentation(rect) + } + let notificationAttachment = try UNNotificationAttachment.init( + identifier: identifier, + url: URL.init(fileURLWithPath: filePath), + options: options + ) content.attachments.append(notificationAttachment) } } diff --git a/flutter_local_notifications/test/ios_flutter_local_notifications_test.dart b/flutter_local_notifications/test/ios_flutter_local_notifications_test.dart index c3f7ba1fe..148bc51db 100644 --- a/flutter_local_notifications/test/ios_flutter_local_notifications_test.dart +++ b/flutter_local_notifications/test/ios_flutter_local_notifications_test.dart @@ -242,10 +242,12 @@ void main() { 'sound': 'sound.mp3', 'badgeNumber': 1, 'threadIdentifier': null, - 'attachments': >[ - { + 'attachments': >[ + { 'filePath': 'video.mp4', 'identifier': '2b3f705f-a680-4c9f-8075-a46a70e28373', + 'hideThumbnail': null, + 'thumbnailClippingRect': null, } ], 'categoryIdentifier': 'category1', @@ -309,10 +311,12 @@ void main() { 'sound': 'sound.mp3', 'badgeNumber': 1, 'threadIdentifier': null, - 'attachments': >[ - { + 'attachments': >[ + { 'filePath': 'video.mp4', 'identifier': '2b3f705f-a680-4c9f-8075-a46a70e28373', + 'hideThumbnail': null, + 'thumbnailClippingRect': null, } ], 'categoryIdentifier': null, @@ -379,10 +383,12 @@ void main() { 'sound': 'sound.mp3', 'badgeNumber': 1, 'threadIdentifier': null, - 'attachments': >[ - { + 'attachments': >[ + { 'filePath': 'video.mp4', 'identifier': '2b3f705f-a680-4c9f-8075-a46a70e28373', + 'hideThumbnail': null, + 'thumbnailClippingRect': null, } ], 'categoryIdentifier': null, @@ -445,10 +451,12 @@ void main() { 'sound': 'sound.mp3', 'badgeNumber': 1, 'threadIdentifier': null, - 'attachments': >[ - { + 'attachments': >[ + { 'filePath': 'video.mp4', 'identifier': '2b3f705f-a680-4c9f-8075-a46a70e28373', + 'hideThumbnail': null, + 'thumbnailClippingRect': null, } ], 'categoryIdentifier': null, @@ -512,10 +520,12 @@ void main() { 'sound': 'sound.mp3', 'badgeNumber': 1, 'threadIdentifier': null, - 'attachments': >[ - { + 'attachments': >[ + { 'filePath': 'video.mp4', 'identifier': '2b3f705f-a680-4c9f-8075-a46a70e28373', + 'hideThumbnail': null, + 'thumbnailClippingRect': null, } ], 'categoryIdentifier': null, diff --git a/flutter_local_notifications/test/macos_flutter_local_notifications_test.dart b/flutter_local_notifications/test/macos_flutter_local_notifications_test.dart index ad71f0056..a03f8f324 100644 --- a/flutter_local_notifications/test/macos_flutter_local_notifications_test.dart +++ b/flutter_local_notifications/test/macos_flutter_local_notifications_test.dart @@ -152,10 +152,12 @@ void main() { 'sound': 'sound.mp3', 'badgeNumber': 1, 'threadIdentifier': 'thread', - 'attachments': >[ - { + 'attachments': >[ + { 'filePath': 'video.mp4', 'identifier': '2b3f705f-a680-4c9f-8075-a46a70e28373', + 'hideThumbnail': null, + 'thumbnailClippingRect': null, } ], 'categoryIdentifier': 'category1', @@ -219,10 +221,12 @@ void main() { 'sound': 'sound.mp3', 'badgeNumber': 1, 'threadIdentifier': null, - 'attachments': >[ - { + 'attachments': >[ + { 'filePath': 'video.mp4', 'identifier': '2b3f705f-a680-4c9f-8075-a46a70e28373', + 'hideThumbnail': null, + 'thumbnailClippingRect': null, } ], 'categoryIdentifier': null, @@ -288,10 +292,12 @@ void main() { 'sound': 'sound.mp3', 'badgeNumber': 1, 'threadIdentifier': null, - 'attachments': >[ - { + 'attachments': >[ + { 'filePath': 'video.mp4', 'identifier': '2b3f705f-a680-4c9f-8075-a46a70e28373', + 'hideThumbnail': null, + 'thumbnailClippingRect': null, } ], 'categoryIdentifier': null, @@ -357,10 +363,12 @@ void main() { 'sound': 'sound.mp3', 'badgeNumber': 1, 'threadIdentifier': null, - 'attachments': >[ - { + 'attachments': >[ + { 'filePath': 'video.mp4', 'identifier': '2b3f705f-a680-4c9f-8075-a46a70e28373', + 'hideThumbnail': null, + 'thumbnailClippingRect': null, } ], 'categoryIdentifier': null, @@ -429,10 +437,12 @@ void main() { 'sound': 'sound.mp3', 'badgeNumber': 1, 'threadIdentifier': null, - 'attachments': >[ - { + 'attachments': >[ + { 'filePath': 'video.mp4', 'identifier': '2b3f705f-a680-4c9f-8075-a46a70e28373', + 'hideThumbnail': null, + 'thumbnailClippingRect': null, } ], 'categoryIdentifier': null,