Revamp how share is managed for album

Shares are now stored in the album json, such that users other than the album owner are aware of the shares
This commit is contained in:
Ming Ming 2021-11-12 01:03:36 +08:00
parent b9655c66c2
commit 6e9a34342a
12 changed files with 598 additions and 259 deletions

View file

@ -1,20 +1,85 @@
import 'package:bloc/bloc.dart';
import 'package:flutter/foundation.dart';
import 'package:equatable/equatable.dart';
import 'package:logging/logging.dart';
import 'package:nc_photos/account.dart';
import 'package:nc_photos/debug_util.dart';
import 'package:nc_photos/entity/album.dart';
import 'package:nc_photos/entity/album/item.dart';
import 'package:nc_photos/entity/album/provider.dart';
import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/share.dart';
import 'package:nc_photos/entity/sharee.dart';
import 'package:nc_photos/iterable_extension.dart';
import 'package:nc_photos/object_extension.dart';
import 'package:nc_photos/string_extension.dart';
import 'package:nc_photos/use_case/list_share.dart';
import 'package:nc_photos/use_case/list_sharee.dart';
class ListAlbumShareOutlierItem {
const ListAlbumShareOutlierItem(this.file, this.shares);
class ListAlbumShareOutlierItem with EquatableMixin {
const ListAlbumShareOutlierItem(this.file, this.shareItems);
@override
toString() {
return "$runtimeType {"
"file: '${file.path}', "
"shareItems: ${shareItems.toReadableString()}, "
"}";
}
@override
get props => [
file,
shareItems,
];
final File file;
final List<Share> shares;
final List<ListAlbumShareOutlierShareItem> shareItems;
}
abstract class ListAlbumShareOutlierShareItem with EquatableMixin {
const ListAlbumShareOutlierShareItem();
}
class ListAlbumShareOutlierExtraShareItem
extends ListAlbumShareOutlierShareItem {
const ListAlbumShareOutlierExtraShareItem(this.share);
@override
toString() {
return "$runtimeType {"
"share: $share, "
"}";
}
@override
get props => [
share,
];
final Share share;
}
class ListAlbumShareOutlierMissingShareItem
extends ListAlbumShareOutlierShareItem {
const ListAlbumShareOutlierMissingShareItem(
this.shareWith, this.shareWithDisplayName);
@override
toString() {
return "$runtimeType {"
"shareWith: $shareWith, "
"shareWithDisplayName: $shareWithDisplayName, "
"}";
}
@override
get props => [
shareWith,
shareWithDisplayName,
];
final String shareWith;
final String? shareWithDisplayName;
}
abstract class ListAlbumShareOutlierBlocEvent {
@ -36,47 +101,47 @@ class ListAlbumShareOutlierBlocQuery extends ListAlbumShareOutlierBlocEvent {
final Album album;
}
abstract class ListAlbumShareOutlierBlocState {
const ListAlbumShareOutlierBlocState(
this.account, this.albumShares, this.items);
abstract class ListAlbumShareOutlierBlocState with EquatableMixin {
const ListAlbumShareOutlierBlocState(this.account, this.items);
@override
toString() {
return "$runtimeType {"
"account: $account, "
"albumShares: List {length: ${albumShares.length}}, "
"items: List {length: ${items.length}}, "
"items: ${items.toReadableString()}, "
"}";
}
@override
get props => [
account,
items,
];
final Account? account;
final List<Share> albumShares;
final List<ListAlbumShareOutlierItem> items;
}
class ListAlbumShareOutlierBlocInit extends ListAlbumShareOutlierBlocState {
ListAlbumShareOutlierBlocInit() : super(null, const [], const []);
ListAlbumShareOutlierBlocInit() : super(null, const []);
}
class ListAlbumShareOutlierBlocLoading extends ListAlbumShareOutlierBlocState {
const ListAlbumShareOutlierBlocLoading(Account? account,
List<Share> albumShares, List<ListAlbumShareOutlierItem> items)
: super(account, albumShares, items);
const ListAlbumShareOutlierBlocLoading(
Account? account, List<ListAlbumShareOutlierItem> items)
: super(account, items);
}
class ListAlbumShareOutlierBlocSuccess extends ListAlbumShareOutlierBlocState {
const ListAlbumShareOutlierBlocSuccess(Account? account,
List<Share> albumShares, List<ListAlbumShareOutlierItem> items)
: super(account, albumShares, items);
const ListAlbumShareOutlierBlocSuccess(
Account? account, List<ListAlbumShareOutlierItem> items)
: super(account, items);
}
class ListAlbumShareOutlierBlocFailure extends ListAlbumShareOutlierBlocState {
const ListAlbumShareOutlierBlocFailure(
Account? account,
List<Share> albumShares,
List<ListAlbumShareOutlierItem> items,
this.exception)
: super(account, albumShares, items);
Account? account, List<ListAlbumShareOutlierItem> items, this.exception)
: super(account, items);
@override
toString() {
@ -86,6 +151,12 @@ class ListAlbumShareOutlierBlocFailure extends ListAlbumShareOutlierBlocState {
"}";
}
@override
get props => [
...super.props,
exception,
];
final dynamic exception;
}
@ -95,7 +166,7 @@ class ListAlbumShareOutlierBlocFailure extends ListAlbumShareOutlierBlocState {
/// belongs, e.g., an unshared item in a shared album, or vice versa
class ListAlbumShareOutlierBloc extends Bloc<ListAlbumShareOutlierBlocEvent,
ListAlbumShareOutlierBlocState> {
ListAlbumShareOutlierBloc(this.shareRepo)
ListAlbumShareOutlierBloc(this.shareRepo, this.shareeRepo)
: super(ListAlbumShareOutlierBlocInit());
@override
@ -110,54 +181,150 @@ class ListAlbumShareOutlierBloc extends Bloc<ListAlbumShareOutlierBlocEvent,
ListAlbumShareOutlierBlocQuery ev) async* {
try {
assert(ev.album.provider is AlbumStaticProvider);
yield ListAlbumShareOutlierBlocLoading(
ev.account, state.albumShares, state.items);
yield ListAlbumShareOutlierBlocLoading(ev.account, state.items);
final albumShares =
(await ListShare(shareRepo)(ev.account, ev.album.albumFile!))
.where((element) => element.shareWith != null)
.sorted((a, b) => a.shareWith!.compareTo(b.shareWith!));
final albumSharees = albumShares
.where((element) => element.shareType == ShareType.user)
.map((e) => e.shareWith!)
.sorted();
final files = AlbumStaticProvider.of(ev.album)
.items
.whereType<AlbumFileItem>()
.map((e) => e.file)
.toList();
final products = <ListAlbumShareOutlierItem>[];
Object? error;
for (final f in files) {
try {
final shares = (await ListShare(shareRepo)(ev.account, f))
.where((element) => element.shareType == ShareType.user)
.toList();
final sharees = shares.map((e) => e.shareWith!).sorted();
if (!listEquals(sharees, albumSharees)) {
products.add(ListAlbumShareOutlierItem(f, shares));
}
} catch (e, stackTrace) {
_log.severe("[_query] Failed while listing share for file: ${f.path}",
e, stackTrace);
error = e;
final albumShares = await () async {
var temp = (ev.album.shares ?? [])
.where((s) => !s.userId.equalsIgnoreCase(ev.account.username))
.toList();
if (ev.album.albumFile!.ownerId != ev.account.username) {
// add owner if the album is not owned by this account
final ownerSharee = (await ListSharee(shareeRepo)(ev.account))
.firstWhere((s) => s.shareWith == ev.album.albumFile!.ownerId);
temp.add(AlbumShare(
userId: ownerSharee.shareWith,
displayName: ownerSharee.shareWithDisplayNameUnique,
));
}
}
if (error == null) {
yield ListAlbumShareOutlierBlocSuccess(
ev.account, albumShares, products);
return Map.fromEntries(
temp.map((as) => MapEntry(as.userId.toLowerCase(), as)));
}();
final albumSharees =
albumShares.values.map((s) => s.userId.toLowerCase()).toSet();
final products = <ListAlbumShareOutlierItem>[];
final errors = <Object>[];
products.addAll(await _processAlbumFile(
ev.account, ev.album, albumShares, albumSharees, errors));
products.addAll(await _processAlbumItems(
ev.account, ev.album, albumShares, albumSharees, errors));
if (errors.isEmpty) {
yield ListAlbumShareOutlierBlocSuccess(ev.account, products);
} else {
yield ListAlbumShareOutlierBlocFailure(
ev.account, albumShares, products, error);
ev.account, products, errors.first);
}
} catch (e, stackTrace) {
_log.severe("[_onEventQuery] Exception while request", e, stackTrace);
yield ListAlbumShareOutlierBlocFailure(
ev.account, state.albumShares, state.items, e);
yield ListAlbumShareOutlierBlocFailure(ev.account, state.items, e);
}
}
Future<List<ListAlbumShareOutlierItem>> _processAlbumFile(
Account account,
Album album,
Map<String, AlbumShare> albumShares,
Set<String> albumSharees,
List<Object> errors,
) async {
try {
final item = await _processSingleFile(
account, album.albumFile!, albumShares, albumSharees, errors);
return item == null ? [] : [item];
} catch (e, stackTrace) {
_log.severe(
"[_processAlbumFile] Failed while _processSingleFile: ${logFilename(album.albumFile?.path)}",
e,
stackTrace);
errors.add(e);
return [];
}
}
Future<List<ListAlbumShareOutlierItem>> _processAlbumItems(
Account account,
Album album,
Map<String, AlbumShare> albumShares,
Set<String> albumSharees,
List<Object> errors,
) async {
final products = <ListAlbumShareOutlierItem>[];
final files = AlbumStaticProvider.of(album)
.items
.whereType<AlbumFileItem>()
.map((e) => e.file)
.toList();
for (final f in files) {
try {
(await _processSingleFile(
account, f, albumShares, albumSharees, errors))
?.apply((item) {
products.add(item);
});
} catch (e, stackTrace) {
_log.severe(
"[_processAlbumItems] Failed while _processSingleFile: ${logFilename(f.path)}",
e,
stackTrace);
errors.add(e);
}
}
return products;
}
Future<ListAlbumShareOutlierItem?> _processSingleFile(
Account account,
File file,
Map<String, AlbumShare> albumShares,
Set<String> albumSharees,
List<Object> errors,
) async {
final shareItems = <ListAlbumShareOutlierShareItem>[];
final shares = (await ListShare(shareRepo)(account, file))
.where((element) => element.shareType == ShareType.user)
.toList();
final sharees = shares.map((s) => s.shareWith!.toLowerCase()).toSet();
final missings = albumSharees.difference(sharees);
_log.info(
"Missing shares: ${missings.toReadableString()} for file: ${logFilename(file.path)}");
for (final m in missings) {
try {
final as = albumShares[m.toLowerCase()]!;
shareItems.add(
ListAlbumShareOutlierMissingShareItem(as.userId, as.displayName));
} catch (e, stackTrace) {
_log.severe(
"[_processSingleFile] Failed while processing missing share for file: ${logFilename(file.path)}",
e,
stackTrace);
errors.add(e);
}
}
final extras = sharees.difference(albumSharees);
_log.info(
"Extra shares: ${extras.toReadableString()} for file: ${logFilename(file.path)}");
for (final e in extras) {
try {
shareItems.add(ListAlbumShareOutlierExtraShareItem(shares
.firstWhere((s) => s.shareWith?.equalsIgnoreCase(e) == true)));
} catch (e, stackTrace) {
_log.severe(
"[_processSingleFile] Failed while processing extra share for file: ${logFilename(file.path)}",
e,
stackTrace);
errors.add(e);
}
}
if (shareItems.isNotEmpty) {
return ListAlbumShareOutlierItem(file, shareItems);
} else {
return null;
}
}
final ShareRepo shareRepo;
final ShareeRepo shareeRepo;
static final _log =
Logger("bloc.list_album_share_outlier.ListAlbumShareOutlierBloc");

View file

@ -18,6 +18,7 @@ import 'package:nc_photos/int_util.dart' as int_util;
import 'package:nc_photos/iterable_extension.dart';
import 'package:nc_photos/or_null.dart';
import 'package:nc_photos/remote_storage_util.dart' as remote_storage_util;
import 'package:nc_photos/string_extension.dart';
import 'package:nc_photos/type.dart';
import 'package:nc_photos/use_case/get_file_binary.dart';
import 'package:nc_photos/use_case/ls.dart';
@ -36,6 +37,7 @@ class Album with EquatableMixin {
required this.provider,
required this.coverProvider,
required this.sortProvider,
this.shares,
this.albumFile,
}) : lastUpdated = (lastUpdated ?? DateTime.now()).toUtc();
@ -95,6 +97,9 @@ class Album with EquatableMixin {
result["coverProvider"].cast<String, dynamic>()),
sortProvider: AlbumSortProvider.fromJson(
result["sortProvider"].cast<String, dynamic>()),
shares: (result["shares"] as List?)
?.map((e) => AlbumShare.fromJson(e.cast<String, dynamic>()))
.toList(),
albumFile: result["albumFile"] == null
? null
: File.fromJson(result["albumFile"].cast<String, dynamic>()),
@ -109,6 +114,7 @@ class Album with EquatableMixin {
"provider: ${provider.toString(isDeep: isDeep)}, "
"coverProvider: $coverProvider, "
"sortProvider: $sortProvider, "
"shares: ${shares?.toReadableString()}, "
"albumFile: $albumFile, "
"}";
}
@ -124,6 +130,7 @@ class Album with EquatableMixin {
AlbumProvider? provider,
AlbumCoverProvider? coverProvider,
AlbumSortProvider? sortProvider,
OrNull<List<AlbumShare>>? shares,
OrNull<File>? albumFile,
}) {
return Album(
@ -133,6 +140,7 @@ class Album with EquatableMixin {
provider: provider ?? this.provider,
coverProvider: coverProvider ?? this.coverProvider,
sortProvider: sortProvider ?? this.sortProvider,
shares: shares == null ? this.shares : shares.obj,
albumFile: albumFile == null ? this.albumFile : albumFile.obj,
);
}
@ -145,6 +153,7 @@ class Album with EquatableMixin {
"provider": provider.toJson(),
"coverProvider": coverProvider.toJson(),
"sortProvider": sortProvider.toJson(),
if (shares != null) "shares": shares!.map((e) => e.toJson()).toList(),
// ignore albumFile
};
}
@ -157,6 +166,7 @@ class Album with EquatableMixin {
"provider": provider.toJson(),
"coverProvider": coverProvider.toJson(),
"sortProvider": sortProvider.toJson(),
if (shares != null) "shares": shares!.map((e) => e.toJson()).toList(),
if (albumFile != null) "albumFile": albumFile!.toJson(),
};
}
@ -168,6 +178,7 @@ class Album with EquatableMixin {
provider,
coverProvider,
sortProvider,
shares,
albumFile,
];
@ -177,6 +188,7 @@ class Album with EquatableMixin {
final AlbumProvider provider;
final AlbumCoverProvider coverProvider;
final AlbumSortProvider sortProvider;
final List<AlbumShare>? shares;
/// How is this album stored on server
///
@ -187,6 +199,50 @@ class Album with EquatableMixin {
static const version = 6;
}
class AlbumShare {
const AlbumShare({
required this.userId,
this.displayName,
});
factory AlbumShare.fromJson(JsonObj json) {
return AlbumShare(
userId: json["userId"],
displayName: json["displayName"],
);
}
JsonObj toJson() {
return {
"userId": userId,
if (displayName != null) "displayName": displayName,
};
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
other is AlbumShare &&
runtimeType == other.runtimeType &&
userId.equalsIgnoreCase(other.userId);
}
@override
get hashCode => userId.toLowerCase().hashCode;
@override
toString() {
return "$runtimeType {"
"userId: $userId, "
"displayName: $displayName, "
"}";
}
/// User ID or username, case insensitive
final String userId;
final String? displayName;
}
class AlbumRepo {
AlbumRepo(this.dataSrc);

View file

@ -0,0 +1,6 @@
extension ObjectExtension<T> on T {
T apply(void Function(T obj) fn) {
fn(this);
return this;
}
}

View file

@ -8,6 +8,7 @@ import 'package:nc_photos/entity/album/provider.dart';
import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/share.dart';
import 'package:nc_photos/pref.dart';
import 'package:nc_photos/string_extension.dart';
import 'package:nc_photos/use_case/create_share.dart';
import 'package:nc_photos/use_case/list_share.dart';
import 'package:nc_photos/use_case/preprocess_album.dart';
@ -55,44 +56,39 @@ class AddToAlbum {
Future<void> _shareFiles(
Account account, Album album, List<File> files) async {
try {
final albumShares =
(await ListShare(shareRepo)(account, album.albumFile!))
.where((element) => element.shareType == ShareType.user)
.map((e) => e.shareWith!)
.toSet();
if (albumShares.isEmpty) {
return;
}
for (final f in files) {
try {
final fileShares = (await ListShare(shareRepo)(account, f))
.where((element) => element.shareType == ShareType.user)
.map((e) => e.shareWith!)
.toSet();
final diffShares = albumShares.difference(fileShares);
for (final s in diffShares) {
try {
await CreateUserShare(shareRepo)(account, f, s);
} catch (e, stackTrace) {
_log.shout(
"[_shareFiles] Failed while CreateUserShare: ${logFilename(f.path)}",
e,
stackTrace);
}
if (album.shares?.isNotEmpty != true) {
return;
}
final albumShares = (album.shares!.map((e) => e.userId).toList()
..add(album.albumFile!.ownerId ?? account.username))
.where((element) => !element.equalsIgnoreCase(account.username))
.toSet();
if (albumShares.isEmpty) {
return;
}
for (final f in files) {
try {
final fileShares = (await ListShare(shareRepo)(account, f))
.where((element) => element.shareType == ShareType.user)
.map((e) => e.shareWith!)
.toSet();
final diffShares = albumShares.difference(fileShares);
for (final s in diffShares) {
try {
await CreateUserShare(shareRepo)(account, f, s);
} catch (e, stackTrace) {
_log.shout(
"[_shareFiles] Failed while CreateUserShare: ${logFilename(f.path)}",
e,
stackTrace);
}
} catch (e, stackTrace) {
_log.shout(
"[_shareFiles] Failed while listing shares: ${logFilename(f.path)}",
e,
stackTrace);
}
} catch (e, stackTrace) {
_log.shout(
"[_shareFiles] Failed while listing shares: ${logFilename(f.path)}",
e,
stackTrace);
}
} catch (e, stackTrace) {
_log.shout(
"[_shareFiles] Failed while listing album shares: ${logFilename(album.albumFile?.path)}",
e,
stackTrace);
}
}

View file

@ -1,56 +0,0 @@
import 'package:nc_photos/account.dart';
import 'package:nc_photos/entity/album.dart';
import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/share.dart';
import 'package:nc_photos/remote_storage_util.dart' as remote_storage_util;
import 'package:nc_photos/use_case/list_dir_share.dart';
import 'package:nc_photos/use_case/ls.dart';
class ListSharedAlbumItem {
const ListSharedAlbumItem(this.share, this.album);
final Share share;
final Album album;
}
class ListSharedAlbum {
const ListSharedAlbum(this.shareRepo, this.fileRepo, this.albumRepo);
/// List albums that are currently shared by you
///
/// If [whereShareWith] is not null, only shares sharing with [whereShareWith]
/// will be returned.
Future<List<ListSharedAlbumItem>> call(
Account account, {
String? whereShareWith,
}) async {
final shareItems = await ListDirShare(shareRepo)(
account, File(path: remote_storage_util.getRemoteAlbumsDir(account)));
final files = await Ls(fileRepo)(
account,
File(path: remote_storage_util.getRemoteAlbumsDir(account)),
);
final products = <ListSharedAlbumItem>[];
for (final si in shareItems) {
// find the file
final albumFile =
files.firstWhere((element) => element.compareServerIdentity(si.file));
final album = await albumRepo.get(
account,
albumFile,
);
for (final s in si.shares) {
if (whereShareWith != null && s.shareWith != whereShareWith) {
continue;
}
products.add(ListSharedAlbumItem(s, album));
}
}
return products;
}
final ShareRepo shareRepo;
final FileRepo fileRepo;
final AlbumRepo albumRepo;
}

View file

@ -9,7 +9,7 @@ import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/share.dart';
import 'package:nc_photos/iterable_extension.dart';
import 'package:nc_photos/pref.dart';
import 'package:nc_photos/use_case/list_share.dart';
import 'package:nc_photos/string_extension.dart';
import 'package:nc_photos/use_case/preprocess_album.dart';
import 'package:nc_photos/use_case/unshare_file_from_album.dart';
import 'package:nc_photos/use_case/update_album.dart';
@ -43,15 +43,7 @@ class RemoveFromAlbum {
final removeFiles =
items.whereType<AlbumFileItem>().map((e) => e.file).toList();
if (removeFiles.isNotEmpty) {
final albumShares =
(await ListShare(shareRepo)(account, newAlbum.albumFile!))
.where((element) => element.shareType == ShareType.user)
.map((e) => e.shareWith!)
.toList();
if (albumShares.isNotEmpty) {
await UnshareFileFromAlbum(shareRepo, fileRepo, albumRepo)(
account, newAlbum, removeFiles, albumShares);
}
await _unshareFiles(account, newAlbum, removeFiles);
}
}
@ -96,6 +88,21 @@ class RemoveFromAlbum {
return newAlbum;
}
Future<void> _unshareFiles(
Account account, Album album, List<File> files) async {
if (album.shares?.isNotEmpty != true) {
return;
}
final albumShares = (album.shares!.map((e) => e.userId).toList()
..add(album.albumFile!.ownerId ?? account.username))
.where((element) => !element.equalsIgnoreCase(account.username))
.toList();
if (albumShares.isNotEmpty) {
await UnshareFileFromAlbum(shareRepo, fileRepo, albumRepo)(
account, album, files, albumShares);
}
}
final AlbumRepo albumRepo;
final ShareRepo shareRepo;
final FileRepo fileRepo;

View file

@ -6,30 +6,65 @@ import 'package:nc_photos/entity/album/item.dart';
import 'package:nc_photos/entity/album/provider.dart';
import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/share.dart';
import 'package:nc_photos/entity/sharee.dart';
import 'package:nc_photos/or_null.dart';
import 'package:nc_photos/use_case/create_share.dart';
import 'package:nc_photos/use_case/update_album.dart';
class ShareAlbumWithUser {
ShareAlbumWithUser(this.shareRepo);
ShareAlbumWithUser(this.shareRepo, this.albumRepo);
Future<void> call(
Account account,
Album album,
Sharee sharee, {
void Function(File)? onShareFileFailed,
}) async {
assert(album.provider is AlbumStaticProvider);
// add the share to album file
final newAlbum = album.copyWith(
shares: OrNull((album.shares ?? [])
..add(AlbumShare(
userId: sharee.shareWith,
displayName: sharee.shareWithDisplayNameUnique,
))),
);
await UpdateAlbum(albumRepo)(account, newAlbum);
await _createFileShares(
account,
newAlbum,
sharee.shareWith,
onShareFileFailed: onShareFileFailed,
);
}
Future<void> _createFileShares(
Account account,
Album album,
String shareWith, {
void Function(File)? onShareFileFailed,
}) async {
assert(album.provider is AlbumStaticProvider);
final files = AlbumStaticProvider.of(album)
.items
.whereType<AlbumFileItem>()
.map((e) => e.file);
await CreateUserShare(shareRepo)(account, album.albumFile!, shareWith);
try {
await CreateUserShare(shareRepo)(account, album.albumFile!, shareWith);
} catch (e, stackTrace) {
_log.severe(
"[_createFileShares] Failed sharing album file '${logFilename(album.albumFile?.path)}' with '$shareWith'",
e,
stackTrace);
onShareFileFailed?.call(album.albumFile!);
}
for (final f in files) {
_log.info("[call] Sharing '${f.path}' with '$shareWith'");
_log.info("[_createFileShares] Sharing '${f.path}' with '$shareWith'");
try {
await CreateUserShare(shareRepo)(account, f, shareWith);
} catch (e, stackTrace) {
_log.severe(
"[call] Failed sharing file '${logFilename(f.path)}' with '$shareWith'",
"[_createFileShares] Failed sharing file '${logFilename(f.path)}' with '$shareWith'",
e,
stackTrace);
onShareFileFailed?.call(f);
@ -38,6 +73,7 @@ class ShareAlbumWithUser {
}
final ShareRepo shareRepo;
final AlbumRepo albumRepo;
static final _log =
Logger("use_case.share_album_with_user.ShareAlbumWithUser");

View file

@ -1,13 +1,16 @@
import 'package:logging/logging.dart';
import 'package:nc_photos/account.dart';
import 'package:nc_photos/debug_util.dart';
import 'package:nc_photos/entity/album.dart';
import 'package:nc_photos/entity/album/item.dart';
import 'package:nc_photos/entity/album/provider.dart';
import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/share.dart';
import 'package:nc_photos/entity/share/data_source.dart';
import 'package:nc_photos/use_case/list_shared_album.dart';
import 'package:nc_photos/or_null.dart';
import 'package:nc_photos/use_case/list_share.dart';
import 'package:nc_photos/use_case/remove_share.dart';
import 'package:nc_photos/use_case/unshare_file_from_album.dart';
import 'package:nc_photos/use_case/update_album.dart';
class UnshareAlbumWithUser {
UnshareAlbumWithUser(this.shareRepo, this.fileRepo, this.albumRepo);
@ -19,16 +22,43 @@ class UnshareAlbumWithUser {
void Function(Share)? onUnshareFileFailed,
}) async {
assert(album.provider is AlbumStaticProvider);
final shareRepo = ShareRepo(ShareRemoteDataSource());
final sharedItems =
await ListSharedAlbum(shareRepo, fileRepo, albumRepo)(account);
final thisShare = sharedItems
.firstWhere((element) =>
element.album.albumFile!.compareServerIdentity(album.albumFile!) &&
element.share.shareWith == shareWith)
.share;
await RemoveShare(shareRepo)(account, thisShare);
// remove the share from album file
final newShares =
album.shares?.where((s) => s.userId != shareWith).toList() ?? [];
final newAlbum = album.copyWith(
shares: OrNull(newShares.isEmpty ? null : newShares),
);
await UpdateAlbum(albumRepo)(account, newAlbum);
await _deleteFileShares(
account,
newAlbum,
shareWith,
onUnshareFileFailed: onUnshareFileFailed,
);
}
Future<void> _deleteFileShares(
Account account,
Album album,
String shareWith, {
void Function(Share)? onUnshareFileFailed,
}) async {
// remove share from the album file
final albumShares = await ListShare(shareRepo)(account, album.albumFile!);
for (final s in albumShares.where((s) => s.shareWith == shareWith)) {
try {
await RemoveShare(shareRepo)(account, s);
} catch (e, stackTrace) {
_log.severe(
"[_deleteFileShares] Failed unsharing album file '${logFilename(album.albumFile?.path)}' with '$shareWith'",
e,
stackTrace);
onUnshareFileFailed?.call(s);
}
}
// then remove shares from all files in this album
final files = AlbumStaticProvider.of(album)
.items
.whereType<AlbumFileItem>()
@ -39,7 +69,6 @@ class UnshareAlbumWithUser {
album,
files,
[shareWith],
listSharedAlbumResults: sharedItems,
onUnshareFileFailed: onUnshareFileFailed,
);
}
@ -47,4 +76,7 @@ class UnshareAlbumWithUser {
final ShareRepo shareRepo;
final FileRepo fileRepo;
final AlbumRepo albumRepo;
static final _log =
Logger("use_case.unshare_album_with_user.UnshareAlbumWithUser");
}

View file

@ -6,8 +6,8 @@ import 'package:nc_photos/entity/album/item.dart';
import 'package:nc_photos/entity/album/provider.dart';
import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/share.dart';
import 'package:nc_photos/use_case/list_album.dart';
import 'package:nc_photos/use_case/list_share.dart';
import 'package:nc_photos/use_case/list_shared_album.dart';
import 'package:nc_photos/use_case/remove_share.dart';
class UnshareFileFromAlbum {
@ -22,19 +22,19 @@ class UnshareFileFromAlbum {
Album album,
List<File> files,
List<String> unshareWith, {
List<ListSharedAlbumItem>? listSharedAlbumResults,
void Function(Share)? onUnshareFileFailed,
}) async {
_log.info(
"[call] Unshare ${files.length} files from album '${album.name}' with ${unshareWith.length} users");
// list albums with shares identical to one of [unshareWith]
final otherAlbums = (listSharedAlbumResults ??
await ListSharedAlbum(shareRepo, fileRepo, albumRepo)(account))
.where((element) =>
!element.album.albumFile!.compareServerIdentity(album.albumFile!) &&
element.album.provider is AlbumStaticProvider &&
unshareWith.contains(element.share.shareWith))
.toList();
// list albums with shares identical to any element in [unshareWith]
final otherAlbums = (await ListAlbum(fileRepo, albumRepo)(account)
.where((event) => event is Album)
.cast<Album>()
.where((album) =>
!album.albumFile!.compareServerIdentity(album.albumFile!) &&
album.provider is AlbumStaticProvider &&
album.shares?.any((s) => unshareWith.contains(s.userId)) == true)
.toList());
// look for shares that are exclusive to this album
final exclusiveShares = <Share>[];
@ -49,14 +49,18 @@ class UnshareFileFromAlbum {
}
}
for (final a in otherAlbums) {
final albumFiles = AlbumStaticProvider.of(a.album)
// check if the album is shared with the same users
if (!a.shares!
.any((as) => exclusiveShares.any((s) => s.shareWith == as.userId))) {
continue;
}
final albumFiles = AlbumStaticProvider.of(a)
.items
.whereType<AlbumFileItem>()
.map((e) => e.file)
.toList();
exclusiveShares.removeWhere((s) =>
a.share.shareWith == s.shareWith &&
albumFiles.any((element) => element.fileId == s.itemSource));
exclusiveShares.removeWhere(
(s) => albumFiles.any((element) => element.fileId == s.itemSource));
}
// unshare them

View file

@ -13,8 +13,9 @@ import 'package:nc_photos/entity/album.dart';
import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/share.dart';
import 'package:nc_photos/entity/share/data_source.dart';
import 'package:nc_photos/entity/sharee.dart';
import 'package:nc_photos/entity/sharee/data_source.dart';
import 'package:nc_photos/exception_util.dart' as exception_util;
import 'package:nc_photos/iterable_extension.dart';
import 'package:nc_photos/k.dart' as k;
import 'package:nc_photos/snack_bar_manager.dart';
import 'package:nc_photos/theme.dart';
@ -219,8 +220,8 @@ class _AlbumShareOutlierBrowserState extends State<AlbumShareOutlierBrowser> {
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
subtitle: Text(
L10n.global().missingShareDescription(item.shareWithDisplayName)),
subtitle: Text(L10n.global().missingShareDescription(
item.shareWithDisplayName ?? item.shareWith)),
trailing: trailing,
);
}
@ -321,9 +322,9 @@ class _AlbumShareOutlierBrowserState extends State<AlbumShareOutlierBrowser> {
_items = [];
} else if (state is ListAlbumShareOutlierBlocSuccess ||
state is ListAlbumShareOutlierBlocLoading) {
_transformItems(state.albumShares, state.items);
_transformItems(state.items);
} else if (state is ListAlbumShareOutlierBlocFailure) {
_transformItems(state.albumShares, state.items);
_transformItems(state.items);
SnackBarManager().showSnackBar(SnackBar(
content: Text(exception_util.toUserString(state.exception)),
duration: k.snackBarDurationNormal,
@ -362,48 +363,20 @@ class _AlbumShareOutlierBrowserState extends State<AlbumShareOutlierBrowser> {
}
}
void _transformItems(
List<Share> albumShares, List<ListAlbumShareOutlierItem> items) {
final to =
albumShares.sorted((a, b) => a.shareWith!.compareTo(b.shareWith!));
for (final i in items) {
_transformItem(to, i);
}
}
void _transformItem(List<Share> albumShares, ListAlbumShareOutlierItem item) {
final from =
item.shares.sorted((a, b) => a.shareWith!.compareTo(b.shareWith!));
final to = albumShares;
var fromI = 0, toI = 0;
while (fromI < from.length && toI < to.length) {
final fromShare = from[fromI];
final toShare = to[toI];
if (fromShare.shareWith == toShare.shareWith) {
++fromI;
++toI;
} else {
final diff = fromShare.shareWith!.compareTo(toShare.shareWith!);
if (diff < 0) {
// extra element in from
_items.add(_ExtraShareItem(item.file, fromShare));
++fromI;
} else {
// extra element in to
_items.add(_MissingShareeItem(
item.file, toShare.shareWith!, toShare.shareWithDisplayName));
++toI;
void _transformItems(List<ListAlbumShareOutlierItem> items) {
_items = () sync* {
for (final item in items) {
for (final si in item.shareItems) {
if (si is ListAlbumShareOutlierMissingShareItem) {
yield _MissingShareeItem(
item.file, si.shareWith, si.shareWithDisplayName);
} else if (si is ListAlbumShareOutlierExtraShareItem) {
yield _ExtraShareItem(item.file, si.share);
}
}
}
}
for (var i = fromI; i < from.length; ++i) {
_items.add(_ExtraShareItem(item.file, from[i]));
}
for (var i = toI; i < to.length; ++i) {
_items.add(_MissingShareeItem(
item.file, to[i].shareWith!, to[i].shareWithDisplayName));
}
}()
.toList();
}
Future<void> _fixMissingSharee(_MissingShareeItem item) async {
@ -489,7 +462,8 @@ class _AlbumShareOutlierBrowserState extends State<AlbumShareOutlierBrowser> {
_itemStatuses[fileKey]!.remove(shareeKey);
}
late final _bloc = ListAlbumShareOutlierBloc();
late final _bloc = ListAlbumShareOutlierBloc(
ShareRepo(ShareRemoteDataSource()), ShareeRepo(ShareeRemoteDataSource()));
var _items = <_ListItem>[];
final _itemStatuses = <String, Map<String, _ItemStatus>>{};
@ -515,7 +489,7 @@ class _MissingShareeItem extends _ListItem {
final File file;
final String shareWith;
final String shareWithDisplayName;
final String? shareWithDisplayName;
}
enum _ItemStatus {

View file

@ -5,7 +5,6 @@ import 'package:logging/logging.dart';
import 'package:nc_photos/account.dart';
import 'package:nc_photos/app_db.dart';
import 'package:nc_photos/app_localizations.dart';
import 'package:nc_photos/bloc/list_share.dart';
import 'package:nc_photos/bloc/list_sharee.dart';
import 'package:nc_photos/entity/album.dart';
import 'package:nc_photos/entity/file.dart';
@ -42,7 +41,6 @@ class _ShareAlbumDialogState extends State<ShareAlbumDialog> {
initState() {
super.initState();
_shareeBloc.add(ListShareeBlocQuery(widget.account));
_shareBloc.add(ListShareBlocQuery(widget.account, widget.album.albumFile!));
}
@override
@ -60,12 +58,7 @@ class _ShareAlbumDialogState extends State<ShareAlbumDialog> {
_onListShareeBlocStateChanged(context, shareeState),
child: BlocBuilder<ListShareeBloc, ListShareeBlocState>(
bloc: _shareeBloc,
builder: (_, shareeState) =>
BlocBuilder<ListShareBloc, ListShareBlocState>(
bloc: _shareBloc,
builder: (context, shareState) =>
_buildContent(context, shareeState, shareState),
),
builder: (_, shareeState) => _buildContent(context, shareeState),
),
),
),
@ -73,11 +66,9 @@ class _ShareAlbumDialogState extends State<ShareAlbumDialog> {
);
}
Widget _buildContent(BuildContext context, ListShareeBlocState shareeState,
ListShareBlocState shareState) {
Widget _buildContent(BuildContext context, ListShareeBlocState shareeState) {
final List<Widget> children;
if (shareeState is ListShareeBlocLoading ||
shareState is ListShareBlocLoading) {
if (shareeState is ListShareeBlocLoading) {
children = [
Padding(
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 24),
@ -95,7 +86,7 @@ class _ShareAlbumDialogState extends State<ShareAlbumDialog> {
children = shareeState.items
.where((element) => element.type == ShareeType.user)
.sorted((a, b) => a.label.compareTo(b.label))
.map((sharee) => _buildItem(context, shareState, sharee))
.map((sharee) => _buildItem(context, sharee))
.toList();
}
return GestureDetector(
@ -107,14 +98,15 @@ class _ShareAlbumDialogState extends State<ShareAlbumDialog> {
);
}
Widget _buildItem(
BuildContext context, ListShareBlocState shareState, Sharee sharee) {
Widget _buildItem(BuildContext context, Sharee sharee) {
final bool isShared;
if (_overrideSharee.containsKey(sharee.shareWith)) {
isShared = _overrideSharee[sharee.shareWith]!;
} else {
isShared = shareState.items
.any((element) => element.shareWith == sharee.shareWith);
isShared = widget.album.shares
?.map((e) => e.userId)
.contains(sharee.shareWith) ??
false;
}
final isProcessing =
@ -185,12 +177,13 @@ class _ShareAlbumDialogState extends State<ShareAlbumDialog> {
Future<void> _createShare(Sharee sharee) async {
final shareRepo = ShareRepo(ShareRemoteDataSource());
final albumRepo = AlbumRepo(AlbumCachedDataSource(AppDb()));
var hasFailure = false;
try {
await ShareAlbumWithUser(shareRepo)(
await ShareAlbumWithUser(shareRepo, albumRepo)(
widget.account,
widget.album,
sharee.shareWith,
sharee,
onShareFileFailed: (_) {
hasFailure = true;
},
@ -260,7 +253,6 @@ class _ShareAlbumDialogState extends State<ShareAlbumDialog> {
}
final _shareeBloc = ListShareeBloc();
final _shareBloc = ListShareBloc();
final _processingSharee = <String>[];
/// Store the modified value of each sharee

View file

@ -7,6 +7,7 @@ import 'package:nc_photos/entity/album/provider.dart';
import 'package:nc_photos/entity/album/sort_provider.dart';
import 'package:nc_photos/entity/album/upgrader.dart';
import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/type.dart';
import 'package:test/test.dart';
void main() {
@ -313,6 +314,8 @@ void main() {
));
});
test("shares", _fromJsonShares);
test("albumFile", () {
final json = <String, dynamic>{
"version": Album.version,
@ -602,6 +605,8 @@ void main() {
},
});
});
test("shares", _toRemoteJsonShares);
});
group("toAppDbJson", () {
@ -852,6 +857,8 @@ void main() {
});
});
test("shares", _toAppDbJsonShares);
test("albumFile", () {
final album = Album(
lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901),
@ -1669,3 +1676,121 @@ void main() {
});
});
}
void _fromJsonShares() {
final json = <String, dynamic>{
"version": Album.version,
"lastUpdated": "2020-01-02T03:04:05.678901Z",
"name": "",
"provider": <String, dynamic>{
"type": "static",
"content": <String, dynamic>{
"items": [],
},
},
"coverProvider": <String, dynamic>{
"type": "auto",
"content": <String, dynamic>{},
},
"sortProvider": <String, dynamic>{
"type": "null",
"content": <String, dynamic>{},
},
"shares": <JsonObj>[
{"userId": "admin"},
],
};
expect(
Album.fromJson(
json,
upgraderV1: null,
upgraderV2: null,
upgraderV3: null,
upgraderV4: null,
upgraderV5: null,
),
Album(
lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901),
name: "",
provider: AlbumStaticProvider(
items: [],
),
coverProvider: AlbumAutoCoverProvider(),
sortProvider: const AlbumNullSortProvider(),
shares: [AlbumShare(userId: "admin".toCi())],
));
}
void _toRemoteJsonShares() {
final album = Album(
lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901),
name: "",
provider: AlbumStaticProvider(
items: [],
),
coverProvider: AlbumAutoCoverProvider(),
sortProvider: const AlbumNullSortProvider(),
shares: [AlbumShare(userId: "admin".toCi())],
);
expect(album.toRemoteJson(), <String, dynamic>{
"version": Album.version,
"lastUpdated": "2020-01-02T03:04:05.678901Z",
"name": "",
"provider": <String, dynamic>{
"type": "static",
"content": <String, dynamic>{
"items": [],
},
},
"coverProvider": <String, dynamic>{
"type": "auto",
"content": <String, dynamic>{},
},
"sortProvider": <String, dynamic>{
"type": "null",
"content": <String, dynamic>{},
},
"shares": [
<String, dynamic>{
"userId": "admin",
},
],
});
}
void _toAppDbJsonShares() {
final album = Album(
lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901),
name: "",
provider: AlbumStaticProvider(
items: [],
),
coverProvider: AlbumAutoCoverProvider(),
sortProvider: const AlbumNullSortProvider(),
shares: [AlbumShare(userId: "admin".toCi())],
);
expect(album.toAppDbJson(), <String, dynamic>{
"version": Album.version,
"lastUpdated": "2020-01-02T03:04:05.678901Z",
"name": "",
"provider": <String, dynamic>{
"type": "static",
"content": <String, dynamic>{
"items": [],
},
},
"coverProvider": <String, dynamic>{
"type": "auto",
"content": <String, dynamic>{},
},
"sortProvider": <String, dynamic>{
"type": "null",
"content": <String, dynamic>{},
},
"shares": [
<String, dynamic>{
"userId": "admin",
},
],
});
}