Sharing notifications (#1237)
This commit is contained in:
commit
2cb0315804
|
@ -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 |
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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];
|
||||
|
|
5
lib/generated/intl/messages_en.dart
generated
5
lib/generated/intl/messages_en.dart
generated
|
@ -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..."),
|
||||
|
|
30
lib/generated/l10n.dart
generated
30
lib/generated/l10n.dart
generated
|
@ -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(
|
||||
|
|
|
@ -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",
|
||||
|
@ -771,7 +773,7 @@
|
|||
},
|
||||
"deleteAll": "Delete All",
|
||||
"renameAlbum": "Rename album",
|
||||
"setCover" : "Set cover",
|
||||
"setCover": "Set cover",
|
||||
"@setCover": {
|
||||
"description": "Text to set cover photo for an album"
|
||||
},
|
||||
|
@ -973,7 +975,7 @@
|
|||
"save": "Save",
|
||||
"centerPoint": "Center point",
|
||||
"pickCenterPoint": "Pick center point",
|
||||
"useSelectedPhoto": "Use selected photo",
|
||||
"useSelectedPhoto": "Use selected photo",
|
||||
"edit": "Edit",
|
||||
"deleteLocation": "Delete location",
|
||||
"rotateLeft": "Rotate left",
|
||||
|
@ -1000,13 +1002,13 @@
|
|||
"@storageBreakupYou": {
|
||||
"description": "Label to indicate how much storage you are using when you are part of a family plan"
|
||||
},
|
||||
"storageUsageInfo" : "{usedAmount} {usedStorageUnit} of {totalAmount} {totalStorageUnit} used",
|
||||
"@storageUsageInfo" :{
|
||||
"storageUsageInfo": "{usedAmount} {usedStorageUnit} of {totalAmount} {totalStorageUnit} used",
|
||||
"@storageUsageInfo": {
|
||||
"description": "Example: 1.2 GB of 2 GB used or 100 GB or 2TB used"
|
||||
},
|
||||
"freeStorageSpace" : "{freeAmount} {storageUnit} free",
|
||||
"freeStorageSpace": "{freeAmount} {storageUnit} free",
|
||||
"appVersion": "Version: {versionValue}",
|
||||
"verifyIDLabel" : "Verify",
|
||||
"verifyIDLabel": "Verify",
|
||||
"fileInfoAddDescHint": "Add a description...",
|
||||
"editLocationTagTitle": "Edit location",
|
||||
"setLabel": "Set",
|
||||
|
@ -1074,10 +1076,9 @@
|
|||
"@map": {
|
||||
"description": "Label for the map view"
|
||||
},
|
||||
"maps" : "Maps",
|
||||
"maps": "Maps",
|
||||
"enableMaps": "Enable Maps",
|
||||
"enableMapsDesc" : "This will show your photos on a world map.\n\nThis map is hosted by Open Street Map, and the exact locations of your photos are never shared.\n\nYou can disable this feature anytime from Settings.",
|
||||
"enableMapsDesc": "This will show your photos on a world map.\n\nThis map is hosted by Open Street Map, and the exact locations of your photos are never shared.\n\nYou can disable this feature anytime from Settings.",
|
||||
"unpinAlbum": "Unpin album",
|
||||
"pinAlbum": "Pin album"
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -26,6 +26,7 @@ class File extends EnteFile {
|
|||
int? creationTime;
|
||||
int? modificationTime;
|
||||
int? updationTime;
|
||||
int? addedTime;
|
||||
Location? location;
|
||||
late FileType fileType;
|
||||
int? fileSubType;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
@ -170,7 +171,7 @@ class RemoteSyncService {
|
|||
_logger.info("Pulling remote diff");
|
||||
final isFirstSync = !_collectionsService.hasSyncedCollections();
|
||||
if (isFirstSync && !_isExistingSyncSilent) {
|
||||
Bus.instance.fire(SyncStatusUpdate(SyncStatus.applyingRemoteDiff));
|
||||
Bus.instance.fire(SyncStatusUpdate(SyncStatus.applyingRemoteDiff));
|
||||
}
|
||||
await _collectionsService.sync();
|
||||
// check and reset user's collection syncTime in past for older clients
|
||||
|
@ -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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
94
lib/ui/settings/notification_settings_screen.dart
Normal file
94
lib/ui/settings/notification_settings_screen.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"];
|
||||
|
|
16
pubspec.lock
16
pubspec.lock
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue