Sharing notifications (#1237)

This commit is contained in:
Vishnu Mohandas 2023-06-30 19:24:24 +05:30 committed by GitHub
commit 2cb0315804
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 421 additions and 76 deletions

View file

@ -111,4 +111,5 @@
tools:ignore="ScopedStorage" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="com.android.vending.BILLING" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
</manifest>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 901 B

After

Width:  |  Height:  |  Size: 886 B

View file

@ -10,6 +10,10 @@ import Flutter
var flutter_native_splash = 1
UIApplication.shared.isStatusBarHidden = false
if #available(iOS 10.0, *) {
UNUserNotificationCenter.current().delegate = self as UNUserNotificationCenterDelegate
}
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}

View file

@ -50,6 +50,7 @@ class FilesDB {
static const columnCreationTime = 'creation_time';
static const columnModificationTime = 'modification_time';
static const columnUpdationTime = 'updation_time';
static const columnAddedTime = 'added_time';
static const columnEncryptedKey = 'encrypted_key';
static const columnKeyDecryptionNonce = 'key_decryption_nonce';
static const columnFileDecryptionHeader = 'file_decryption_header';
@ -82,6 +83,7 @@ class FilesDB {
...addFileSizeColumn(),
...updateIndexes(),
...createEntityDataTable(),
...addAddedTime(),
];
final dbConfig = MigrationConfig(
@ -367,6 +369,17 @@ class FilesDB {
];
}
static List<String> addAddedTime() {
return [
'''
ALTER TABLE $filesTable ADD COLUMN $columnAddedTime INTEGER NOT NULL DEFAULT -1;
''',
'''
CREATE INDEX IF NOT EXISTS added_time_index ON $filesTable($columnAddedTime);
'''
];
}
Future<void> clearTable() async {
final db = await instance.database;
await db.delete(filesTable);
@ -627,6 +640,23 @@ class FilesDB {
return files;
}
Future<List<File>> getNewFilesInCollection(
int collectionID,
int addedTime,
) async {
final db = await instance.database;
const String whereClause =
'$columnCollectionID = ? AND $columnAddedTime > ?';
final List<Object> whereArgs = [collectionID, addedTime];
final results = await db.query(
filesTable,
where: whereClause,
whereArgs: whereArgs,
);
final files = convertToFiles(results);
return files;
}
Future<FileLoadResult> getFilesInCollections(
List<int> collectionIDs,
int startTime,
@ -1507,6 +1537,8 @@ class FilesDB {
row[columnCreationTime] = file.creationTime;
row[columnModificationTime] = file.modificationTime;
row[columnUpdationTime] = file.updationTime;
row[columnAddedTime] =
file.addedTime ?? DateTime.now().microsecondsSinceEpoch;
row[columnEncryptedKey] = file.encryptedKey;
row[columnKeyDecryptionNonce] = file.keyDecryptionNonce;
row[columnFileDecryptionHeader] = file.fileDecryptionHeader;
@ -1552,6 +1584,8 @@ class FilesDB {
row[columnCreationTime] = file.creationTime;
row[columnModificationTime] = file.modificationTime;
row[columnUpdationTime] = file.updationTime;
row[columnAddedTime] =
file.addedTime ?? DateTime.now().microsecondsSinceEpoch;
row[columnFileDecryptionHeader] = file.fileDecryptionHeader;
row[columnThumbnailDecryptionHeader] = file.thumbnailDecryptionHeader;
row[columnMetadataDecryptionHeader] = file.metadataDecryptionHeader;
@ -1597,6 +1631,7 @@ class FilesDB {
file.creationTime = row[columnCreationTime];
file.modificationTime = row[columnModificationTime];
file.updationTime = row[columnUpdationTime] ?? -1;
file.addedTime = row[columnAddedTime];
file.encryptedKey = row[columnEncryptedKey];
file.keyDecryptionNonce = row[columnKeyDecryptionNonce];
file.fileDecryptionHeader = row[columnFileDecryptionHeader];

View file

@ -839,6 +839,7 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("No results found"),
"nothingToSeeHere":
MessageLookupByLibrary.simpleMessage("Nothing to see here! 👀"),
"notifications": MessageLookupByLibrary.simpleMessage("Notifications"),
"ok": MessageLookupByLibrary.simpleMessage("Ok"),
"onDevice": MessageLookupByLibrary.simpleMessage("On device"),
"onEnte": MessageLookupByLibrary.simpleMessage(
@ -1093,6 +1094,10 @@ class MessageLookup extends MessageLookupByLibrary {
"sharedAlbumSectionDescription": MessageLookupByLibrary.simpleMessage(
"Create shared and collaborative albums with other ente users, including users on free plans."),
"sharedByMe": MessageLookupByLibrary.simpleMessage("Shared by me"),
"sharedPhotoNotifications":
MessageLookupByLibrary.simpleMessage("New shared photos"),
"sharedPhotoNotificationsExplanation": MessageLookupByLibrary.simpleMessage(
"Receive notifications when someone adds a photo to a shared album that you\'re a part of"),
"sharedWith": m47,
"sharedWithMe": MessageLookupByLibrary.simpleMessage("Shared with me"),
"sharing": MessageLookupByLibrary.simpleMessage("Sharing..."),

View file

@ -3434,6 +3434,36 @@ class S {
);
}
/// `Notifications`
String get notifications {
return Intl.message(
'Notifications',
name: 'notifications',
desc: '',
args: [],
);
}
/// `New shared photos`
String get sharedPhotoNotifications {
return Intl.message(
'New shared photos',
name: 'sharedPhotoNotifications',
desc: '',
args: [],
);
}
/// `Receive notifications when someone adds a photo to a shared album that you're a part of`
String get sharedPhotoNotificationsExplanation {
return Intl.message(
'Receive notifications when someone adds a photo to a shared album that you\'re a part of',
name: 'sharedPhotoNotificationsExplanation',
desc: '',
args: [],
);
}
/// `Advanced`
String get advanced {
return Intl.message(

View file

@ -507,6 +507,9 @@
},
"familyPlans": "Family plans",
"referrals": "Referrals",
"notifications": "Notifications",
"sharedPhotoNotifications": "New shared photos",
"sharedPhotoNotificationsExplanation": "Receive notifications when someone adds a photo to a shared album that you're a part of",
"advanced": "Advanced",
"general": "General",
"security": "Security",
@ -659,7 +662,6 @@
"description": "Button text for raising a support tickets in case of unhandled errors during backup",
"type": "text"
},
"backupFailed": "Backup failed",
"couldNotBackUpTryLater": "We could not backup your data.\nWe will retry later.",
"enteCanEncryptAndPreserveFilesOnlyIfYouGrant": "ente can encrypt and preserve files only if you grant access to them",
@ -1080,4 +1082,3 @@
"unpinAlbum": "Unpin album",
"pinAlbum": "Pin album"
}

View file

@ -29,7 +29,6 @@ import 'package:photos/services/local_file_update_service.dart';
import 'package:photos/services/local_sync_service.dart';
import "package:photos/services/location_service.dart";
import 'package:photos/services/memories_service.dart';
import 'package:photos/services/notification_service.dart';
import "package:photos/services/object_detection/object_detection_service.dart";
import 'package:photos/services/push_service.dart';
import 'package:photos/services/remote_sync_service.dart';
@ -149,6 +148,7 @@ Future<void> _init(bool isBackground, {String via = ''}) async {
final SharedPreferences preferences = await SharedPreferences.getInstance();
await _logFGHeartBeatInfo();
_scheduleHeartBeat(preferences, isBackground);
AppLifecycleService.instance.init(preferences);
if (isBackground) {
AppLifecycleService.instance.onAppInBackground('init via: $via');
} else {
@ -157,7 +157,6 @@ Future<void> _init(bool isBackground, {String via = ''}) async {
// Start workers asynchronously. No need to wait for them to start
Computer.shared().turnOn(workersCount: 4, verbose: kDebugMode);
CryptoUtil.init();
await NotificationService.instance.init();
await NetworkClient.instance.init();
await Configuration.instance.init();
await UserService.instance.init();

View file

@ -26,6 +26,7 @@ class File extends EnteFile {
int? creationTime;
int? modificationTime;
int? updationTime;
int? addedTime;
Location? location;
late FileType fileType;
int? fileSubType;

View file

@ -1,18 +1,26 @@
import 'package:logging/logging.dart';
import 'package:media_extension/media_extension_action_types.dart';
import "package:shared_preferences/shared_preferences.dart";
class AppLifecycleService {
static const String keyLastAppOpenTime = "last_app_open_time";
final _logger = Logger("AppLifecycleService");
bool isForeground = false;
MediaExtentionAction mediaExtensionAction =
MediaExtentionAction(action: IntentAction.main);
late SharedPreferences _preferences;
static final AppLifecycleService instance =
AppLifecycleService._privateConstructor();
AppLifecycleService._privateConstructor();
void init(SharedPreferences preferences) {
_preferences = preferences;
}
void setMediaExtensionAction(MediaExtentionAction mediaExtensionAction) {
_logger.info("App invoked via ${mediaExtensionAction.action}");
this.mediaExtensionAction = mediaExtensionAction;
@ -25,6 +33,14 @@ class AppLifecycleService {
void onAppInBackground(String reason) {
_logger.info("App in background $reason");
_preferences.setInt(
keyLastAppOpenTime,
DateTime.now().microsecondsSinceEpoch,
);
isForeground = false;
}
int getLastAppOpenTime() {
return _preferences.getInt(keyLastAppOpenTime) ?? 0;
}
}

View file

@ -1,53 +1,123 @@
import 'dart:io';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import "package:photos/services/remote_sync_service.dart";
import "package:shared_preferences/shared_preferences.dart";
class NotificationService {
static final NotificationService instance =
NotificationService._privateConstructor();
static const String keyGrantedNotificationPermission =
"notification_permission_granted";
static const String keyShouldShowNotificationsForSharedPhotos =
"notifications_enabled_shared_photos";
NotificationService._privateConstructor();
final FlutterLocalNotificationsPlugin _flutterLocalNotificationsPlugin =
late SharedPreferences _preferences;
final FlutterLocalNotificationsPlugin _notificationsPlugin =
FlutterLocalNotificationsPlugin();
Future<void> init() async {
if (!Platform.isAndroid) {
return;
}
const AndroidInitializationSettings initializationSettingsAndroid =
AndroidInitializationSettings('notification_icon');
Future<void> init(
void Function(
NotificationResponse notificationResponse,
)
onNotificationTapped,
) async {
_preferences = await SharedPreferences.getInstance();
const androidSettings = AndroidInitializationSettings('notification_icon');
const iosSettings = DarwinInitializationSettings(
requestAlertPermission: false,
requestSoundPermission: false,
requestBadgePermission: false,
requestCriticalPermission: false,
);
const InitializationSettings initializationSettings =
InitializationSettings(
android: initializationSettingsAndroid,
android: androidSettings,
iOS: iosSettings,
);
await _flutterLocalNotificationsPlugin.initialize(
await _notificationsPlugin.initialize(
initializationSettings,
onSelectNotification: selectNotification,
onDidReceiveNotificationResponse: onNotificationTapped,
);
final launchDetails =
await _notificationsPlugin.getNotificationAppLaunchDetails();
if (launchDetails != null &&
launchDetails.didNotificationLaunchApp &&
launchDetails.notificationResponse != null) {
onNotificationTapped(launchDetails.notificationResponse!);
}
if (!hasGrantedPermissions() &&
RemoteSyncService.instance.isFirstRemoteSyncDone()) {
await requestPermissions();
}
}
Future<void> requestPermissions() async {
bool? result;
if (Platform.isIOS) {
result = await _notificationsPlugin
.resolvePlatformSpecificImplementation<
IOSFlutterLocalNotificationsPlugin>()
?.requestPermissions(
sound: true,
alert: true,
);
} else {
result = await _notificationsPlugin
.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin>()
?.requestPermission();
}
if (result != null) {
_preferences.setBool(keyGrantedNotificationPermission, result);
}
}
bool hasGrantedPermissions() {
final result = _preferences.getBool(keyGrantedNotificationPermission);
return result ?? false;
}
bool shouldShowNotificationsForSharedPhotos() {
final result =
_preferences.getBool(keyShouldShowNotificationsForSharedPhotos);
return result ?? true;
}
Future<void> setShouldShowNotificationsForSharedPhotos(bool value) {
return _preferences.setBool(
keyShouldShowNotificationsForSharedPhotos,
value,
);
}
Future selectNotification(String? payload) async {}
Future<void> showNotification(String title, String message) async {
if (!Platform.isAndroid) {
return;
}
const AndroidNotificationDetails androidPlatformChannelSpecifics =
AndroidNotificationDetails(
'io.ente.photos',
'ente',
Future<void> showNotification(
String title,
String message, {
String channelID = "io.ente.photos",
String channelName = "ente",
String payload = "ente://home",
}) async {
final androidSpecs = AndroidNotificationDetails(
channelID,
channelName,
channelDescription: 'ente alerts',
importance: Importance.max,
priority: Priority.high,
showWhen: false,
);
const NotificationDetails platformChannelSpecifics =
NotificationDetails(android: androidPlatformChannelSpecifics);
await _flutterLocalNotificationsPlugin.show(
0,
final iosSpecs = DarwinNotificationDetails(threadIdentifier: channelID);
final platformChannelSpecs =
NotificationDetails(android: androidSpecs, iOS: iosSpecs);
await _notificationsPlugin.show(
channelName.hashCode,
title,
message,
platformChannelSpecifics,
platformChannelSpecs,
payload: payload,
);
}
}

View file

@ -26,6 +26,7 @@ import 'package:photos/services/collections_service.dart';
import "package:photos/services/feature_flag_service.dart";
import 'package:photos/services/ignored_files_service.dart';
import 'package:photos/services/local_file_update_service.dart';
import "package:photos/services/notification_service.dart";
import 'package:photos/services/sync_service.dart';
import 'package:photos/services/trash_sync_service.dart';
import 'package:photos/utils/diff_fetcher.dart';
@ -183,13 +184,14 @@ class RemoteSyncService {
await _markResetSyncTimeAsDone();
}
await _syncUpdatedCollections();
unawaited(_localFileUpdateService.markUpdatedFilesForReUpload());
}
Future<void> _syncUpdatedCollections() async {
final idsToRemoteUpdationTimeMap =
await _collectionsService.getCollectionIDsToBeSynced();
await _syncUpdatedCollections(idsToRemoteUpdationTimeMap);
unawaited(_localFileUpdateService.markUpdatedFilesForReUpload());
unawaited(_notifyNewFiles(idsToRemoteUpdationTimeMap.keys.toList()));
}
Future<void> _syncUpdatedCollections(final idsToRemoteUpdationTimeMap) async {
for (final cid in idsToRemoteUpdationTimeMap.keys) {
await _syncCollectionDiff(
cid,
@ -621,7 +623,7 @@ class RemoteSyncService {
);
}
/* _storeDiff maps each remoteDiff file to existing
/* _storeDiff maps each remoteFile to existing
entries in files table. When match is found, it compares both file to
perform relevant actions like
[1] Clear local cache when required (Both Shared and Owned files)
@ -637,7 +639,7 @@ class RemoteSyncService {
[Existing]
]
*/
Future _storeDiff(List<File> diff, int collectionID) async {
Future<void> _storeDiff(List<File> diff, int collectionID) async {
int sharedFileNew = 0,
sharedFileUpdated = 0,
localUploadedFromDevice = 0,
@ -648,60 +650,60 @@ class RemoteSyncService {
// this is required when same file is uploaded twice in the same
// collection. Without this check, if both remote files are part of same
// diff response, then we end up inserting one entry instead of two
// as we update the generatedID for remoteDiff to local file's genID
// as we update the generatedID for remoteFile to local file's genID
final Set<int> alreadyClaimedLocalFilesGenID = {};
final List<File> toBeInserted = [];
for (File remoteDiff in diff) {
for (File remoteFile in diff) {
// existingFile will be either set to existing collectionID+localID or
// to the unclaimed aka not already linked to any uploaded file.
File? existingFile;
if (remoteDiff.generatedID != null) {
if (remoteFile.generatedID != null) {
// Case [1] Check and clear local cache when uploadedFile already exist
// Note: Existing file can be null here if it's replaced by the time we
// reach here
existingFile = await _db.getFile(remoteDiff.generatedID!);
existingFile = await _db.getFile(remoteFile.generatedID!);
if (existingFile != null &&
_shouldClearCache(remoteDiff, existingFile)) {
_shouldClearCache(remoteFile, existingFile)) {
needsGalleryReload = true;
await clearCache(remoteDiff);
await clearCache(remoteFile);
}
}
/* If file is not owned by the user, no further processing is required
as Case [2,3,4] are only relevant to files owned by user
*/
if (userID != remoteDiff.ownerID) {
if (userID != remoteFile.ownerID) {
if (existingFile == null) {
sharedFileNew++;
remoteDiff.localID = null;
remoteFile.localID = null;
} else {
sharedFileUpdated++;
// if user has downloaded the file on the device, avoid removing the
// localID reference.
// [Todo-fix: Excluded shared file's localIDs during syncALL]
remoteDiff.localID = existingFile.localID;
remoteFile.localID = existingFile.localID;
}
toBeInserted.add(remoteDiff);
toBeInserted.add(remoteFile);
// end processing for file here, move to next file now
continue;
}
// If remoteDiff is not already synced (i.e. existingFile is null), check
// If remoteFile is not already synced (i.e. existingFile is null), check
// if the remoteFile was uploaded from this device.
// Note: DeviceFolder is ignored for iOS during matching
if (existingFile == null && remoteDiff.localID != null) {
if (existingFile == null && remoteFile.localID != null) {
final localFileEntries = await _db.getUnlinkedLocalMatchesForRemoteFile(
userID,
remoteDiff.localID!,
remoteDiff.fileType,
title: remoteDiff.title ?? '',
deviceFolder: remoteDiff.deviceFolder ?? '',
remoteFile.localID!,
remoteFile.fileType,
title: remoteFile.title ?? '',
deviceFolder: remoteFile.deviceFolder ?? '',
);
if (localFileEntries.isEmpty) {
// set remote file's localID as null because corresponding local file
// does not exist [Case 2, do not retain localID of the remote file]
remoteDiff.localID = null;
remoteFile.localID = null;
} else {
// case 4: Check and schedule the file for update
final int maxModificationTime = localFileEntries
@ -717,11 +719,11 @@ class RemoteSyncService {
for the adjustments or just if the asset has been modified ever.
https://stackoverflow.com/a/50093266/546896
*/
if (maxModificationTime > remoteDiff.modificationTime! &&
if (maxModificationTime > remoteFile.modificationTime! &&
Platform.isAndroid) {
localButUpdatedOnDevice++;
await FileUpdationDB.instance.insertMultiple(
[remoteDiff.localID!],
[remoteFile.localID!],
FileUpdationDB.modificationTimeUpdated,
);
}
@ -738,17 +740,17 @@ class RemoteSyncService {
existingFile = localFileEntries.first;
localUploadedFromDevice++;
alreadyClaimedLocalFilesGenID.add(existingFile.generatedID!);
remoteDiff.generatedID = existingFile.generatedID;
remoteFile.generatedID = existingFile.generatedID;
}
}
}
if (existingFile != null &&
_shouldReloadHomeGallery(remoteDiff, existingFile)) {
_shouldReloadHomeGallery(remoteFile, existingFile)) {
needsGalleryReload = true;
} else {
remoteNewFile++;
}
toBeInserted.add(remoteDiff);
toBeInserted.add(remoteFile);
}
await _db.insertMultiple(toBeInserted);
_logger.info(
@ -850,4 +852,38 @@ class RemoteSyncService {
}
});
}
bool _shouldShowNotification(int collectionID) {
// TODO: Add option to opt out of notifications for a specific collection
// Screen: https://www.figma.com/file/SYtMyLBs5SAOkTbfMMzhqt/ente-Visual-Design?type=design&node-id=7689-52943&t=IyWOfh0Gsb0p7yVC-4
return NotificationService.instance
.shouldShowNotificationsForSharedPhotos() &&
isFirstRemoteSyncDone() &&
!AppLifecycleService.instance.isForeground;
}
Future<void> _notifyNewFiles(List<int> collectionIDs) async {
final userID = Configuration.instance.getUserID();
final appOpenTime = AppLifecycleService.instance.getLastAppOpenTime();
for (final collectionID in collectionIDs) {
final collection = _collectionsService.getCollectionByID(collectionID);
final files =
await _db.getNewFilesInCollection(collectionID, appOpenTime);
final sharedFileCount =
files.where((file) => file.ownerID != userID).length;
final collectedFileCount = files
.where((file) => file.pubMagicMetadata!.uploaderName != null)
.length;
final totalCount = sharedFileCount + collectedFileCount;
if (totalCount > 0 && _shouldShowNotification(collectionID)) {
NotificationService.instance.showNotification(
collection!.displayName,
totalCount.toString() + " new 📸",
channelID: "collection:" + collectionID.toString(),
channelName: collection.displayName,
payload: "ente://collection/?collectionID=" + collectionID.toString(),
);
}
}
}
}

View file

@ -12,6 +12,7 @@ import "package:photos/ui/growth/referral_screen.dart";
import 'package:photos/ui/settings/advanced_settings_screen.dart';
import 'package:photos/ui/settings/common_settings.dart';
import "package:photos/ui/settings/language_picker.dart";
import "package:photos/ui/settings/notification_settings_screen.dart";
import 'package:photos/utils/navigation_util.dart';
class GeneralSectionWidget extends StatelessWidget {
@ -82,6 +83,18 @@ class GeneralSectionWidget extends StatelessWidget {
},
),
sectionOptionSpacing,
MenuItemWidget(
captionedTextWidget: CaptionedTextWidget(
title: S.of(context).notifications,
),
pressedColor: getEnteColorScheme(context).fillFaint,
trailingIcon: Icons.chevron_right_outlined,
trailingIconIsMuted: true,
onTap: () async {
_onNotificationsTapped(context);
},
),
sectionOptionSpacing,
MenuItemWidget(
captionedTextWidget: CaptionedTextWidget(
title: S.of(context).advanced,
@ -104,6 +117,13 @@ class GeneralSectionWidget extends StatelessWidget {
BillingService.instance.launchFamilyPortal(context, userDetails);
}
void _onNotificationsTapped(BuildContext context) {
routeToPage(
context,
const NotificationSettingsScreen(),
);
}
void _onAdvancedTapped(BuildContext context) {
routeToPage(
context,

View file

@ -0,0 +1,94 @@
import 'package:flutter/material.dart';
import "package:photos/generated/l10n.dart";
import "package:photos/services/notification_service.dart";
import 'package:photos/theme/ente_theme.dart';
import 'package:photos/ui/components/buttons/icon_button_widget.dart';
import 'package:photos/ui/components/captioned_text_widget.dart';
import 'package:photos/ui/components/menu_item_widget/menu_item_widget.dart';
import 'package:photos/ui/components/menu_section_description_widget.dart';
import 'package:photos/ui/components/title_bar_title_widget.dart';
import 'package:photos/ui/components/title_bar_widget.dart';
import 'package:photos/ui/components/toggle_switch_widget.dart';
class NotificationSettingsScreen extends StatelessWidget {
const NotificationSettingsScreen({super.key});
@override
Widget build(BuildContext context) {
final colorScheme = getEnteColorScheme(context);
return Scaffold(
body: CustomScrollView(
primary: false,
slivers: <Widget>[
TitleBarWidget(
flexibleSpaceTitle: TitleBarTitleWidget(
title: S.of(context).notifications,
),
actionIcons: [
IconButtonWidget(
icon: Icons.close_outlined,
iconButtonType: IconButtonType.secondary,
onTap: () {
Navigator.pop(context);
Navigator.pop(context);
},
),
],
),
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 20),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Column(
children: [
MenuItemWidget(
captionedTextWidget: CaptionedTextWidget(
title: S.of(context).sharedPhotoNotifications,
),
menuItemColor: colorScheme.fillFaint,
trailingWidget: ToggleSwitchWidget(
value: () =>
NotificationService.instance
.hasGrantedPermissions() &&
NotificationService.instance
.shouldShowNotificationsForSharedPhotos(),
onChanged: () async {
await NotificationService.instance
.requestPermissions();
await NotificationService.instance
.setShouldShowNotificationsForSharedPhotos(
!NotificationService.instance
.shouldShowNotificationsForSharedPhotos(),
);
},
),
singleBorderRadius: 8,
alignCaptionedTextToLeft: true,
isGestureDetectorDisabled: true,
),
MenuSectionDescriptionWidget(
content: S
.of(context)
.sharedPhotoNotificationsExplanation,
)
],
)
],
),
),
);
},
childCount: 1,
),
),
],
),
);
}
}

View file

@ -4,6 +4,7 @@ import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
import "package:flutter_local_notifications/flutter_local_notifications.dart";
import 'package:logging/logging.dart';
import 'package:media_extension/media_extension_action_types.dart';
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
@ -22,11 +23,13 @@ import 'package:photos/events/tab_changed_event.dart';
import 'package:photos/events/trigger_logout_event.dart';
import 'package:photos/events/user_logged_out_event.dart';
import "package:photos/generated/l10n.dart";
import "package:photos/models/collection_items.dart";
import 'package:photos/models/selected_files.dart';
import 'package:photos/services/app_lifecycle_service.dart';
import 'package:photos/services/collections_service.dart';
import "package:photos/services/entity_service.dart";
import 'package:photos/services/local_sync_service.dart';
import "package:photos/services/notification_service.dart";
import 'package:photos/services/update_service.dart';
import 'package:photos/services/user_service.dart';
import 'package:photos/states/user_details_state.dart';
@ -48,7 +51,9 @@ import 'package:photos/ui/settings/app_update_dialog.dart';
import 'package:photos/ui/settings_page.dart';
import "package:photos/ui/tabs/shared_collections_tab.dart";
import "package:photos/ui/tabs/user_collections_tab.dart";
import "package:photos/ui/viewer/gallery/collection_page.dart";
import 'package:photos/utils/dialog_util.dart';
import "package:photos/utils/navigation_util.dart";
import 'package:receive_sharing_intent/receive_sharing_intent.dart';
import 'package:uni_links/uni_links.dart';
@ -205,6 +210,8 @@ class _HomeWidgetState extends State<HomeWidget> {
),
);
NotificationService.instance.init(_onDidReceiveNotificationResponse);
super.initState();
}
@ -492,4 +499,29 @@ class _HomeWidgetState extends State<HomeWidget> {
// Do not show change dialog again
UpdateService.instance.hideChangeLog().ignore();
}
void _onDidReceiveNotificationResponse(
NotificationResponse notificationResponse,
) async {
final String? payload = notificationResponse.payload;
if (payload != null) {
debugPrint('notification payload: $payload');
final collectionID = Uri.parse(payload).queryParameters["collectionID"];
if (collectionID != null) {
final collection = CollectionsService.instance
.getCollectionByID(int.parse(collectionID))!;
final thumbnail =
await CollectionsService.instance.getCover(collection);
routeToPage(
context,
CollectionPage(
CollectionWithThumbnail(
collection,
thumbnail,
),
),
);
}
}
}
}

View file

@ -53,6 +53,7 @@ class DiffFetcher {
.getUploadedFile(file.uploadedFileID!, file.collectionID!);
if (existingFile != null) {
file.generatedID = existingFile.generatedID;
file.addedTime = existingFile.addedTime;
}
}
file.ownerID = item["ownerID"];

View file

@ -689,26 +689,26 @@ packages:
dependency: "direct main"
description:
name: flutter_local_notifications
sha256: "57d0012730780fe137260dd180e072c18a73fbeeb924cdc029c18aaa0f338d64"
sha256: f222919a34545931e47b06000836b5101baeffb0e6eb5a4691d2d42851740dd9
url: "https://pub.dev"
source: hosted
version: "9.9.1"
version: "12.0.4"
flutter_local_notifications_linux:
dependency: transitive
description:
name: flutter_local_notifications_linux
sha256: b472bfc173791b59ede323661eae20f7fff0b6908fea33dd720a6ef5d576bae8
sha256: "6af440e3962eeab8459602c309d7d4ab9e62f05d5cfe58195a28f846a0b5d523"
url: "https://pub.dev"
source: hosted
version: "0.5.1"
version: "1.0.0"
flutter_local_notifications_platform_interface:
dependency: transitive
description:
name: flutter_local_notifications_platform_interface
sha256: "21bceee103a66a53b30ea9daf677f990e5b9e89b62f222e60dd241cd08d63d3a"
sha256: "5ec1feac5f7f7d9266759488bc5f76416152baba9aa1b26fe572246caa00d1ab"
url: "https://pub.dev"
source: hosted
version: "5.0.0"
version: "6.0.0"
flutter_localizations:
dependency: "direct main"
description: flutter
@ -1938,10 +1938,10 @@ packages:
dependency: transitive
description:
name: timezone
sha256: "57b35f6e8ef731f18529695bffc62f92c6189fac2e52c12d478dec1931afb66e"
sha256: "1cfd8ddc2d1cfd836bc93e67b9be88c3adaeca6f40a00ca999104c30693cdca0"
url: "https://pub.dev"
source: hosted
version: "0.8.0"
version: "0.9.2"
timing:
dependency: transitive
description:

View file

@ -63,7 +63,7 @@ dependencies:
flutter_image_compress: ^1.1.0
flutter_inappwebview: ^5.5.0+2
flutter_launcher_icons: ^0.9.3
flutter_local_notifications: ^9.5.3+1
flutter_local_notifications: ^12.0.4
flutter_localizations:
sdk: flutter
flutter_map: ^4.0.0