From afe186a5a8ab6822734be0d381626a2d3ec4e486 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 | 27 ++++++++- .../ios_flutter_local_notifications_test.dart | 30 ++++++---- ...acos_flutter_local_notifications_test.dart | 30 ++++++---- 6 files changed, 158 insertions(+), 28 deletions(-) diff --git a/flutter_local_notifications/example/lib/main.dart b/flutter_local_notifications/example/lib/main.dart index 3fdf89333..a269bbd02 100644 --- a/flutter_local_notifications/example/lib/main.dart +++ b/flutter_local_notifications/example/lib/main.dart @@ -829,9 +829,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( @@ -2165,12 +2182,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..750bac741 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 @@ -2,9 +2,10 @@ /// systems such as iOS and macOS class DarwinNotificationAttachment { /// Constructs an instance of [DarwinNotificationAttachment]. - const DarwinNotificationAttachment( - this.filePath, { + const DarwinNotificationAttachment(this.filePath, { this.identifier, + this.hideThumbnail, + this.thumbnailClippingRect, }); /// The local file path to the attachment. @@ -18,4 +19,26 @@ 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 { + const DarwinNotificationAttachmentThumbnailClippingRect({ + required this.x, + required this.y, + required this.width, + required this.height, + }); + + final double x; + final double y; + + final double width; + final double height; } 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 ca0715742..88fbc1621 100644 --- a/flutter_local_notifications/test/ios_flutter_local_notifications_test.dart +++ b/flutter_local_notifications/test/ios_flutter_local_notifications_test.dart @@ -243,10 +243,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', @@ -310,10 +312,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, @@ -380,10 +384,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, @@ -446,10 +452,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, @@ -513,10 +521,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 7be2395a2..622fc846a 100644 --- a/flutter_local_notifications/test/macos_flutter_local_notifications_test.dart +++ b/flutter_local_notifications/test/macos_flutter_local_notifications_test.dart @@ -153,10 +153,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', @@ -220,10 +222,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, @@ -289,10 +293,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, @@ -358,10 +364,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, @@ -430,10 +438,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,