Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[flutter_local_notifications] Implement options to hide or crop attachments in the thumbnail on iOS #1785

Merged
merged 1 commit into from Nov 29, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
56 changes: 52 additions & 4 deletions flutter_local_notifications/example/lib/main.dart
Expand Up @@ -826,9 +826,26 @@ class _HomePageState extends State<HomePage> {
},
),
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(
Expand Down Expand Up @@ -2162,12 +2179,43 @@ class _HomePageState extends State<HomePage> {
payload: 'item x');
}

Future<void> _showNotificationWithAttachment() async {
Future<void> _showNotificationWithAttachment({
required bool hideThumbnail,
}) async {
final String bigPicturePath = await _downloadAndSaveFile(
'https://via.placeholder.com/600x200', 'bigPicture.jpg');
final DarwinNotificationDetails darwinNotificationDetails =
DarwinNotificationDetails(attachments: <DarwinNotificationAttachment>[
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<void> _showNotificationWithClippedThumbnailAttachment() async {
final String bigPicturePath = await _downloadAndSaveFile(
'https://via.placeholder.com/600x200', 'bigPicture.jpg');
final DarwinNotificationDetails darwinNotificationDetails =
DarwinNotificationDetails(attachments: <DarwinNotificationAttachment>[
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);
Expand Down
Expand Up @@ -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";
Expand Down Expand Up @@ -912,14 +914,40 @@ - (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 =
MaikuB marked this conversation as resolved.
Show resolved Hide resolved
CFBridgingRelease(CGRectCreateDictionaryRepresentation(rect));
[options
setObject:rectDict
forKey:
UNNotificationAttachmentOptionsThumbnailClippingRectKey];
}

UNNotificationAttachment *notificationAttachment =
[UNNotificationAttachment
attachmentWithIdentifier:attachment[ATTACHMENT_IDENTIFIER]
URL:[NSURL
fileURLWithPath:
attachment
[ATTACHMENT_FILE_PATH]]
options:nil
options:options
error:&error];
if (error) {
result(getFlutterError(error));
Expand Down
Expand Up @@ -48,10 +48,21 @@ extension DarwinInitializationSettingsMapper on DarwinInitializationSettings {
};
}

extension DarwinNotificationAttachmentMapper on DarwinNotificationAttachment {
extension on DarwinNotificationAttachmentThumbnailClippingRect {
Map<String, Object> toMap() => <String, Object>{
'x': x,
'y': y,
'width': width,
'height': height,
};
}

extension DarwinNotificationAttachmentMapper on DarwinNotificationAttachment {
Map<String, Object?> toMap() => <String, Object?>{
'identifier': identifier ?? '',
'filePath': filePath,
'hideThumbnail': hideThumbnail,
'thumbnailClippingRect': thumbnailClippingRect?.toMap(),
};
}

Expand Down
Expand Up @@ -5,6 +5,8 @@ class DarwinNotificationAttachment {
const DarwinNotificationAttachment(
this.filePath, {
this.identifier,
this.hideThumbnail,
this.thumbnailClippingRect,
});

/// The local file path to the attachment.
Expand All @@ -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 {
noinskit marked this conversation as resolved.
Show resolved Hide resolved
/// 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;
}
Expand Up @@ -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"
Expand Down Expand Up @@ -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] = rect.dictionaryRepresentation
}
let notificationAttachment = try UNNotificationAttachment.init(
identifier: identifier,
url: URL.init(fileURLWithPath: filePath),
options: options
)
content.attachments.append(notificationAttachment)
}
}
Expand Down
Expand Up @@ -242,10 +242,12 @@ void main() {
'sound': 'sound.mp3',
'badgeNumber': 1,
'threadIdentifier': null,
'attachments': <Map<String, Object>>[
<String, Object>{
'attachments': <Map<String, Object?>>[
<String, Object?>{
'filePath': 'video.mp4',
'identifier': '2b3f705f-a680-4c9f-8075-a46a70e28373',
'hideThumbnail': null,
'thumbnailClippingRect': null,
}
],
'categoryIdentifier': 'category1',
Expand Down Expand Up @@ -309,10 +311,12 @@ void main() {
'sound': 'sound.mp3',
'badgeNumber': 1,
'threadIdentifier': null,
'attachments': <Map<String, Object>>[
<String, Object>{
'attachments': <Map<String, Object?>>[
<String, Object?>{
'filePath': 'video.mp4',
'identifier': '2b3f705f-a680-4c9f-8075-a46a70e28373',
'hideThumbnail': null,
'thumbnailClippingRect': null,
}
],
'categoryIdentifier': null,
Expand Down Expand Up @@ -379,10 +383,12 @@ void main() {
'sound': 'sound.mp3',
'badgeNumber': 1,
'threadIdentifier': null,
'attachments': <Map<String, Object>>[
<String, Object>{
'attachments': <Map<String, Object?>>[
<String, Object?>{
'filePath': 'video.mp4',
'identifier': '2b3f705f-a680-4c9f-8075-a46a70e28373',
'hideThumbnail': null,
'thumbnailClippingRect': null,
}
],
'categoryIdentifier': null,
Expand Down Expand Up @@ -445,10 +451,12 @@ void main() {
'sound': 'sound.mp3',
'badgeNumber': 1,
'threadIdentifier': null,
'attachments': <Map<String, Object>>[
<String, Object>{
'attachments': <Map<String, Object?>>[
<String, Object?>{
'filePath': 'video.mp4',
'identifier': '2b3f705f-a680-4c9f-8075-a46a70e28373',
'hideThumbnail': null,
'thumbnailClippingRect': null,
}
],
'categoryIdentifier': null,
Expand Down Expand Up @@ -512,10 +520,12 @@ void main() {
'sound': 'sound.mp3',
'badgeNumber': 1,
'threadIdentifier': null,
'attachments': <Map<String, Object>>[
<String, Object>{
'attachments': <Map<String, Object?>>[
<String, Object?>{
'filePath': 'video.mp4',
'identifier': '2b3f705f-a680-4c9f-8075-a46a70e28373',
'hideThumbnail': null,
'thumbnailClippingRect': null,
}
],
'categoryIdentifier': null,
Expand Down
Expand Up @@ -152,10 +152,12 @@ void main() {
'sound': 'sound.mp3',
'badgeNumber': 1,
'threadIdentifier': 'thread',
'attachments': <Map<String, Object>>[
<String, Object>{
'attachments': <Map<String, Object?>>[
<String, Object?>{
'filePath': 'video.mp4',
'identifier': '2b3f705f-a680-4c9f-8075-a46a70e28373',
'hideThumbnail': null,
'thumbnailClippingRect': null,
}
],
'categoryIdentifier': 'category1',
Expand Down Expand Up @@ -219,10 +221,12 @@ void main() {
'sound': 'sound.mp3',
'badgeNumber': 1,
'threadIdentifier': null,
'attachments': <Map<String, Object>>[
<String, Object>{
'attachments': <Map<String, Object?>>[
<String, Object?>{
'filePath': 'video.mp4',
'identifier': '2b3f705f-a680-4c9f-8075-a46a70e28373',
'hideThumbnail': null,
'thumbnailClippingRect': null,
}
],
'categoryIdentifier': null,
Expand Down Expand Up @@ -288,10 +292,12 @@ void main() {
'sound': 'sound.mp3',
'badgeNumber': 1,
'threadIdentifier': null,
'attachments': <Map<String, Object>>[
<String, Object>{
'attachments': <Map<String, Object?>>[
<String, Object?>{
'filePath': 'video.mp4',
'identifier': '2b3f705f-a680-4c9f-8075-a46a70e28373',
'hideThumbnail': null,
'thumbnailClippingRect': null,
}
],
'categoryIdentifier': null,
Expand Down Expand Up @@ -357,10 +363,12 @@ void main() {
'sound': 'sound.mp3',
'badgeNumber': 1,
'threadIdentifier': null,
'attachments': <Map<String, Object>>[
<String, Object>{
'attachments': <Map<String, Object?>>[
<String, Object?>{
'filePath': 'video.mp4',
'identifier': '2b3f705f-a680-4c9f-8075-a46a70e28373',
'hideThumbnail': null,
'thumbnailClippingRect': null,
}
],
'categoryIdentifier': null,
Expand Down Expand Up @@ -429,10 +437,12 @@ void main() {
'sound': 'sound.mp3',
'badgeNumber': 1,
'threadIdentifier': null,
'attachments': <Map<String, Object>>[
<String, Object>{
'attachments': <Map<String, Object?>>[
<String, Object?>{
'filePath': 'video.mp4',
'identifier': '2b3f705f-a680-4c9f-8075-a46a70e28373',
'hideThumbnail': null,
'thumbnailClippingRect': null,
}
],
'categoryIdentifier': null,
Expand Down