0
Fork 0
mirror of https://github.com/immich-app/immich.git synced 2025-04-01 02:51:27 -05:00

refactor(mobile): remove int user id ()

* refactor: user entity

* chore: rebase fixes

* refactor: remove int user Id

* refactor: migrate store userId from int to string

* refactor: rename uid to id

* fix: migration

* pr feedback

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
This commit is contained in:
shenlong 2025-03-18 21:35:37 +05:30 committed by GitHub
parent e96ffd43e7
commit 9cf3b88f80
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
38 changed files with 182 additions and 157 deletions

View file

@ -4,8 +4,6 @@ import 'package:immich_mobile/domain/models/user.model.dart';
abstract interface class IUserRepository implements IDatabaseRepository {
Future<bool> insert(UserDto user);
Future<UserDto?> get(int id);
Future<UserDto?> getByUserId(String id);
Future<List<UserDto?>> getByUserIds(List<String> ids);
@ -16,7 +14,7 @@ abstract interface class IUserRepository implements IDatabaseRepository {
Future<UserDto> update(UserDto user);
Future<void> delete(List<int> ids);
Future<void> delete(List<String> ids);
Future<void> deleteAll();
}

View file

@ -1,7 +1,5 @@
import 'dart:ui';
import 'package:immich_mobile/utils/hash.dart';
enum AvatarColor {
// do not change this order or reuse indices for other purposes, adding is OK
primary,
@ -32,7 +30,7 @@ enum AvatarColor {
// TODO: Rename to User once Isar is removed
class UserDto {
final String uid;
final String id;
final String email;
final String name;
final bool isAdmin;
@ -50,11 +48,10 @@ class UserDto {
final int quotaUsageInBytes;
final int quotaSizeInBytes;
int get id => fastHash(uid);
bool get hasQuota => quotaSizeInBytes > 0;
const UserDto({
required this.uid,
required this.id,
required this.email,
required this.name,
required this.isAdmin,
@ -73,7 +70,6 @@ class UserDto {
String toString() {
return '''User: {
id: $id,
uid: $uid,
email: $email,
name: $name,
isAdmin: $isAdmin,
@ -90,7 +86,7 @@ quotaSizeInBytes: $quotaSizeInBytes,
}
UserDto copyWith({
String? uid,
String? id,
String? email,
String? name,
bool? isAdmin,
@ -105,7 +101,7 @@ quotaSizeInBytes: $quotaSizeInBytes,
int? quotaSizeInBytes,
}) =>
UserDto(
uid: uid ?? this.uid,
id: id ?? this.id,
email: email ?? this.email,
name: name ?? this.name,
isAdmin: isAdmin ?? this.isAdmin,
@ -124,7 +120,7 @@ quotaSizeInBytes: $quotaSizeInBytes,
bool operator ==(covariant UserDto other) {
if (identical(this, other)) return true;
return other.uid == uid &&
return other.id == id &&
other.updatedAt.isAtSameMomentAs(updatedAt) &&
other.avatarColor == avatarColor &&
other.email == email &&
@ -141,7 +137,7 @@ quotaSizeInBytes: $quotaSizeInBytes,
@override
int get hashCode =>
uid.hashCode ^
id.hashCode ^
name.hashCode ^
email.hashCode ^
updatedAt.hashCode ^

View file

@ -3,6 +3,7 @@ import 'dart:typed_data';
import 'package:collection/collection.dart';
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/utils/hash.dart';
extension ListExtension<E> on List<E> {
List<E> uniqueConsecutive({
@ -62,11 +63,11 @@ extension AssetListExtension on Iterable<Asset> {
void Function()? errorCallback,
}) {
if (owner == null) return [];
final userId = owner.id;
final bool onlyOwned = every((e) => e.ownerId == userId);
final isarUserId = fastHash(owner.id);
final bool onlyOwned = every((e) => e.ownerId == isarUserId);
if (!onlyOwned) {
if (errorCallback != null) errorCallback();
return where((a) => a.ownerId == userId);
return where((a) => a.ownerId == isarUserId);
}
return this;
}

View file

@ -40,7 +40,7 @@ class User {
});
static User fromDto(UserDto dto) => User(
id: dto.uid,
id: dto.id,
updatedAt: dto.updatedAt,
email: dto.email,
name: dto.name,
@ -56,7 +56,7 @@ class User {
);
UserDto toDto() => UserDto(
uid: id,
id: id,
email: email,
name: name,
isAdmin: isAdmin,

View file

@ -78,7 +78,9 @@ class IsarStoreRepository extends IsarDatabaseRepository
const (DateTime) => entity.intValue == null
? null
: DateTime.fromMillisecondsSinceEpoch(entity.intValue!),
const (UserDto) => await IsarUserRepository(_db).get(entity.intValue!),
const (UserDto) => entity.strValue == null
? null
: await IsarUserRepository(_db).getByUserId(entity.strValue!),
_ => null,
} as T?;
@ -89,8 +91,8 @@ class IsarStoreRepository extends IsarDatabaseRepository
const (bool) => ((value as bool) ? 1 : 0, null),
const (DateTime) => ((value as DateTime).millisecondsSinceEpoch, null),
const (UserDto) => (
(await IsarUserRepository(_db).update(value as UserDto)).id,
null,
(await IsarUserRepository(_db).update(value as UserDto)).id,
),
_ => throw UnsupportedError(
"Unsupported primitive type: ${key.type} for key: ${key.name}",

View file

@ -11,9 +11,9 @@ class IsarUserRepository extends IsarDatabaseRepository
const IsarUserRepository(super.db) : _db = db;
@override
Future<void> delete(List<int> ids) async {
Future<void> delete(List<String> ids) async {
await transaction(() async {
await _db.users.deleteAll(ids);
await _db.users.deleteAllById(ids);
});
}
@ -24,11 +24,6 @@ class IsarUserRepository extends IsarDatabaseRepository
});
}
@override
Future<UserDto?> get(int id) async {
return (await _db.users.get(id))?.toDto();
}
@override
Future<List<UserDto>> getAll({SortUserBy? sortBy}) async {
return (await _db.users

View file

@ -4,7 +4,7 @@ import 'package:openapi/api.dart';
abstract final class UserConverter {
/// Base user dto used where the complete user object is not required
static UserDto fromSimpleUserDto(UserResponseDto dto) => UserDto(
uid: dto.id,
id: dto.id,
email: dto.email,
name: dto.name,
isAdmin: false,
@ -18,7 +18,7 @@ abstract final class UserConverter {
UserPreferencesResponseDto? preferenceDto,
]) =>
UserDto(
uid: adminDto.id,
id: adminDto.id,
email: adminDto.email,
name: adminDto.name,
isAdmin: adminDto.isAdmin,
@ -34,7 +34,7 @@ abstract final class UserConverter {
);
static UserDto fromPartnerDto(PartnerResponseDto dto) => UserDto(
uid: dto.id,
id: dto.id,
email: dto.email,
name: dto.name,
isAdmin: false,

View file

@ -19,7 +19,7 @@ abstract interface class IAssetRepository implements IDatabaseRepository {
);
Future<List<Asset>> getAll({
required int ownerId,
required String ownerId,
AssetState? state,
AssetSort? sortBy,
int? limit,
@ -29,8 +29,8 @@ abstract interface class IAssetRepository implements IDatabaseRepository {
Future<List<Asset>> getByAlbum(
Album album, {
Iterable<int> notOwnedBy = const [],
int? ownerId,
Iterable<String> notOwnedBy = const [],
String? ownerId,
AssetState? state,
AssetSort? sortBy,
});
@ -45,7 +45,7 @@ abstract interface class IAssetRepository implements IDatabaseRepository {
Future<List<Asset>> getMatches({
required List<Asset> assets,
required int ownerId,
required String ownerId,
AssetState? state,
int limit = 100,
});
@ -64,10 +64,10 @@ abstract interface class IAssetRepository implements IDatabaseRepository {
Stream<Asset?> watchAsset(int id, {bool fireImmediately = false});
Future<List<Asset>> getTrashAssets(int userId);
Future<List<Asset>> getTrashAssets(String userId);
Future<List<Asset>> getRecentlyAddedAssets(int userId);
Future<List<Asset>> getMotionAssets(int userId);
Future<List<Asset>> getRecentlyAddedAssets(String userId);
Future<List<Asset>> getMotionAssets(String userId);
}
enum AssetSort { checksum, ownerIdChecksum }

View file

@ -2,7 +2,7 @@ import 'package:immich_mobile/entities/etag.entity.dart';
import 'package:immich_mobile/interfaces/database.interface.dart';
abstract interface class IETagRepository implements IDatabaseRepository {
Future<ETag?> get(int id);
Future<ETag?> get(String id);
Future<ETag?> getById(String id);

View file

@ -3,22 +3,25 @@ import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart';
abstract class ITimelineRepository {
Future<List<int>> getTimelineUserIds(int id);
Future<List<String>> getTimelineUserIds(String id);
Stream<List<int>> watchTimelineUsers(int id);
Stream<List<String>> watchTimelineUsers(String id);
Stream<RenderList> watchArchiveTimeline(int userId);
Stream<RenderList> watchFavoriteTimeline(int userId);
Stream<RenderList> watchTrashTimeline(int userId);
Stream<RenderList> watchArchiveTimeline(String userId);
Stream<RenderList> watchFavoriteTimeline(String userId);
Stream<RenderList> watchTrashTimeline(String userId);
Stream<RenderList> watchAlbumTimeline(
Album album,
GroupAssetsBy groupAssetsBy,
);
Stream<RenderList> watchAllVideosTimeline();
Stream<RenderList> watchHomeTimeline(int userId, GroupAssetsBy groupAssetsBy);
Stream<RenderList> watchHomeTimeline(
String userId,
GroupAssetsBy groupAssetsBy,
);
Stream<RenderList> watchMultiUsersTimeline(
List<int> userIds,
List<String> userIds,
GroupAssetsBy groupAssetsBy,
);
@ -27,5 +30,5 @@ abstract class ITimelineRepository {
GroupAssetsBy getGroupByOption,
);
Stream<RenderList> watchAssetSelectionTimeline(int userId);
Stream<RenderList> watchAssetSelectionTimeline(String userId);
}

View file

@ -26,7 +26,7 @@ class AlbumAdditionalSharedUserSelectionPage extends HookConsumerWidget {
final sharedUsersList = useState<Set<UserDto>>({});
addNewUsersHandler() {
context.maybePop(sharedUsersList.value.map((e) => e.uid).toList());
context.maybePop(sharedUsersList.value.map((e) => e.id).toList());
}
buildTileIcon(UserDto user) {
@ -151,7 +151,7 @@ class AlbumAdditionalSharedUserSelectionPage extends HookConsumerWidget {
onData: (users) {
for (var sharedUsers in album.sharedUsers) {
users.removeWhere(
(u) => u.uid == sharedUsers.id || u.uid == album.ownerId,
(u) => u.id == sharedUsers.id || u.id == album.ownerId,
);
}

View file

@ -85,7 +85,7 @@ class AlbumOptionsPage extends HookConsumerWidget {
void handleUserClick(UserDto user) {
var actions = [];
if (user.uid == userId) {
if (user.id == userId) {
actions = [
ListTile(
leading: const Icon(Icons.exit_to_app_rounded),
@ -170,10 +170,10 @@ class AlbumOptionsPage extends HookConsumerWidget {
color: context.colorScheme.onSurfaceSecondary,
),
),
trailing: userId == user.uid || isOwner
trailing: userId == user.id || isOwner
? const Icon(Icons.more_horiz_rounded)
: const SizedBox(),
onTap: userId == user.uid || isOwner
onTap: userId == user.id || isOwner
? () => handleUserClick(user)
: null,
);

View file

@ -33,7 +33,7 @@ class AlbumsPage extends HookConsumerWidget {
final searchController = useTextEditingController();
final debounceTimer = useRef<Timer?>(null);
final filterMode = useState(QuickFilterMode.all);
final userId = ref.watch(currentUserProvider)?.uid;
final userId = ref.watch(currentUserProvider)?.id;
final searchFocusNode = useFocusNode();
toggleViewMode() {

View file

@ -72,7 +72,7 @@ class ActivitiesPage extends HookConsumerWidget {
final activity = data[index];
final canDelete = activity.user.id == user?.id ||
album.ownerId == user?.uid;
album.ownerId == user?.id;
return Padding(
padding: const EdgeInsets.all(5),

View file

@ -153,7 +153,7 @@ class AuthNotifier extends StateNotifier<AuthState> {
state = state.copyWith(
deviceId: deviceId,
userId: user.uid,
userId: user.id,
userEmail: user.email,
isAuthenticated: true,
name: user.name,

View file

@ -5,7 +5,7 @@ import 'package:immich_mobile/providers/locale_provider.dart';
import 'package:immich_mobile/services/timeline.service.dart';
import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart';
final singleUserTimelineProvider = StreamProvider.family<RenderList, int?>(
final singleUserTimelineProvider = StreamProvider.family<RenderList, String?>(
(ref, userId) {
if (userId == null) {
return const Stream.empty();
@ -18,7 +18,8 @@ final singleUserTimelineProvider = StreamProvider.family<RenderList, int?>(
dependencies: [localeProvider],
);
final multiUsersTimelineProvider = StreamProvider.family<RenderList, List<int>>(
final multiUsersTimelineProvider =
StreamProvider.family<RenderList, List<String>>(
(ref, userIds) {
ref.watch(localeProvider);
final timelineService = ref.watch(timelineServiceProvider);

View file

@ -34,7 +34,7 @@ final currentUserProvider =
return CurrentUserProvider(ref.watch(userServiceProvider));
});
class TimelineUserIdsProvider extends StateNotifier<List<int>> {
class TimelineUserIdsProvider extends StateNotifier<List<String>> {
TimelineUserIdsProvider(this._timelineService) : super([]) {
_timelineService.getTimelineUserIds().then((users) => state = users);
streamSub = _timelineService
@ -42,7 +42,7 @@ class TimelineUserIdsProvider extends StateNotifier<List<int>> {
.listen((users) => state = users);
}
late final StreamSubscription<List<int>> streamSub;
late final StreamSubscription<List<String>> streamSub;
final TimelineService _timelineService;
@override
@ -53,6 +53,6 @@ class TimelineUserIdsProvider extends StateNotifier<List<int>> {
}
final timelineUsersIdsProvider =
StateNotifierProvider<TimelineUserIdsProvider, List<int>>((ref) {
StateNotifierProvider<TimelineUserIdsProvider, List<String>>((ref) {
return TimelineUserIdsProvider(ref.watch(timelineServiceProvider));
});

View file

@ -10,6 +10,7 @@ import 'package:immich_mobile/interfaces/album.interface.dart';
import 'package:immich_mobile/models/albums/album_search.model.dart';
import 'package:immich_mobile/providers/db.provider.dart';
import 'package:immich_mobile/repositories/database.repository.dart';
import 'package:immich_mobile/utils/hash.dart';
import 'package:isar/isar.dart';
final albumRepositoryProvider =
@ -43,14 +44,11 @@ class AlbumRepository extends DatabaseRepository implements IAlbumRepository {
if (shared != null) {
query = query.sharedEqualTo(shared);
}
final isarUserId = fastHash(Store.get(StoreKey.currentUser).id);
if (owner == true) {
query = query.owner(
(q) => q.isarIdEqualTo(Store.get(StoreKey.currentUser).id),
);
query = query.owner((q) => q.isarIdEqualTo(isarUserId));
} else if (owner == false) {
query = query.owner(
(q) => q.not().isarIdEqualTo(Store.get(StoreKey.currentUser).id),
);
query = query.owner((q) => q.not().isarIdEqualTo(isarUserId));
}
if (remote == true) {
query = query.localIdIsNull();
@ -140,16 +138,13 @@ class AlbumRepository extends DatabaseRepository implements IAlbumRepository {
.filter()
.nameContains(searchTerm, caseSensitive: false)
.remoteIdIsNotNull();
final isarUserId = fastHash(Store.get(StoreKey.currentUser).id);
switch (filterMode) {
case QuickFilterMode.sharedWithMe:
query = query.owner(
(q) => q.not().isarIdEqualTo(Store.get(StoreKey.currentUser).id),
);
query = query.owner((q) => q.not().isarIdEqualTo(isarUserId));
case QuickFilterMode.myAlbums:
query = query.owner(
(q) => q.isarIdEqualTo(Store.get(StoreKey.currentUser).id),
);
query = query.owner((q) => q.isarIdEqualTo(isarUserId));
case QuickFilterMode.all:
break;
}

View file

@ -11,6 +11,7 @@ import 'package:immich_mobile/infrastructure/entities/exif.entity.dart';
import 'package:immich_mobile/interfaces/asset.interface.dart';
import 'package:immich_mobile/providers/db.provider.dart';
import 'package:immich_mobile/repositories/database.repository.dart';
import 'package:immich_mobile/utils/hash.dart';
import 'package:isar/isar.dart';
final assetRepositoryProvider =
@ -22,20 +23,21 @@ class AssetRepository extends DatabaseRepository implements IAssetRepository {
@override
Future<List<Asset>> getByAlbum(
Album album, {
Iterable<int> notOwnedBy = const [],
int? ownerId,
Iterable<String> notOwnedBy = const [],
String? ownerId,
AssetState? state,
AssetSort? sortBy,
}) {
var query = album.assets.filter();
final isarUserIds = notOwnedBy.map(fastHash).toList();
if (notOwnedBy.length == 1) {
query = query.not().ownerIdEqualTo(notOwnedBy.first);
query = query.not().ownerIdEqualTo(isarUserIds.first);
} else if (notOwnedBy.isNotEmpty) {
query =
query.not().anyOf(notOwnedBy, (q, int id) => q.ownerIdEqualTo(id));
query.not().anyOf(isarUserIds, (q, int id) => q.ownerIdEqualTo(id));
}
if (ownerId != null) {
query = query.ownerIdEqualTo(ownerId);
query = query.ownerIdEqualTo(fastHash(ownerId));
}
if (state != null) {
@ -87,27 +89,28 @@ class AssetRepository extends DatabaseRepository implements IAssetRepository {
@override
Future<List<Asset>> getAll({
required int ownerId,
required String ownerId,
AssetState? state,
AssetSort? sortBy,
int? limit,
}) {
final baseQuery = db.assets.where();
final isarUserIds = fastHash(ownerId);
final QueryBuilder<Asset, Asset, QAfterFilterCondition> filteredQuery =
switch (state) {
null => baseQuery.ownerIdEqualToAnyChecksum(ownerId).noOp(),
null => baseQuery.ownerIdEqualToAnyChecksum(isarUserIds).noOp(),
AssetState.local => baseQuery
.remoteIdIsNull()
.filter()
.localIdIsNotNull()
.ownerIdEqualTo(ownerId),
.ownerIdEqualTo(isarUserIds),
AssetState.remote => baseQuery
.localIdIsNull()
.filter()
.remoteIdIsNotNull()
.ownerIdEqualTo(ownerId),
.ownerIdEqualTo(isarUserIds),
AssetState.merged => baseQuery
.ownerIdEqualToAnyChecksum(ownerId)
.ownerIdEqualToAnyChecksum(isarUserIds)
.filter()
.remoteIdIsNotNull()
.localIdIsNotNull(),
@ -132,7 +135,7 @@ class AssetRepository extends DatabaseRepository implements IAssetRepository {
@override
Future<List<Asset>> getMatches({
required List<Asset> assets,
required int ownerId,
required String ownerId,
AssetState? state,
int limit = 100,
}) {
@ -147,7 +150,7 @@ class AssetRepository extends DatabaseRepository implements IAssetRepository {
AssetState.merged =>
baseQuery.localIdIsNotNull().filter().remoteIdIsNotNull(),
};
return _getMatchesImpl(query, ownerId, assets, limit);
return _getMatchesImpl(query, fastHash(ownerId), assets, limit);
}
@override
@ -185,10 +188,10 @@ class AssetRepository extends DatabaseRepository implements IAssetRepository {
@override
Future<List<Asset?>> getAllByOwnerIdChecksum(
List<int> ids,
List<int> ownerIds,
List<String> checksums,
) =>
db.assets.getAllByOwnerIdChecksum(ids, checksums);
db.assets.getAllByOwnerIdChecksum(ownerIds, checksums);
@override
Future<List<Asset>> getAllLocal() =>
@ -224,30 +227,30 @@ class AssetRepository extends DatabaseRepository implements IAssetRepository {
}
@override
Future<List<Asset>> getTrashAssets(int userId) {
Future<List<Asset>> getTrashAssets(String userId) {
return db.assets
.where()
.remoteIdIsNotNull()
.filter()
.ownerIdEqualTo(userId)
.ownerIdEqualTo(fastHash(userId))
.isTrashedEqualTo(true)
.findAll();
}
@override
Future<List<Asset>> getRecentlyAddedAssets(int userId) {
Future<List<Asset>> getRecentlyAddedAssets(String userId) {
return db.assets
.where()
.ownerIdEqualToAnyChecksum(userId)
.ownerIdEqualToAnyChecksum(fastHash(userId))
.sortByFileCreatedAtDesc()
.findAll();
}
@override
Future<List<Asset>> getMotionAssets(int userId) {
Future<List<Asset>> getMotionAssets(String userId) {
return db.assets
.where()
.ownerIdEqualToAnyChecksum(userId)
.ownerIdEqualToAnyChecksum(fastHash(userId))
.filter()
.livePhotoVideoIdIsNotNull()
.findAll();

View file

@ -4,6 +4,7 @@ import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/interfaces/asset_media.interface.dart';
import 'package:immich_mobile/utils/hash.dart';
import 'package:photo_manager/photo_manager.dart' hide AssetType;
final assetMediaRepositoryProvider = Provider((ref) => AssetMediaRepository());
@ -24,7 +25,7 @@ class AssetMediaRepository implements IAssetMediaRepository {
final Asset asset = Asset(
checksum: "",
localId: local.id,
ownerId: Store.get(StoreKey.currentUser).id,
ownerId: fastHash(Store.get(StoreKey.currentUser).id),
fileCreatedAt: local.createDateTime,
fileModifiedAt: local.modifiedDateTime,
updatedAt: local.modifiedDateTime,

View file

@ -15,7 +15,7 @@ class ETagRepository extends DatabaseRepository implements IETagRepository {
Future<List<String>> getAllIds() => db.eTags.where().idProperty().findAll();
@override
Future<ETag?> get(int id) => db.eTags.get(id);
Future<ETag?> get(String id) => db.eTags.getById(id);
@override
Future<void> upsertAll(List<ETag> etags) => txn(() => db.eTags.putAll(etags));

View file

@ -6,6 +6,7 @@ import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
import 'package:immich_mobile/interfaces/timeline.interface.dart';
import 'package:immich_mobile/providers/db.provider.dart';
import 'package:immich_mobile/repositories/database.repository.dart';
import 'package:immich_mobile/utils/hash.dart';
import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart';
import 'package:isar/isar.dart';
@ -17,32 +18,32 @@ class TimelineRepository extends DatabaseRepository
TimelineRepository(super.db);
@override
Future<List<int>> getTimelineUserIds(int id) {
Future<List<String>> getTimelineUserIds(String id) {
return db.users
.filter()
.inTimelineEqualTo(true)
.or()
.isarIdEqualTo(id)
.isarIdProperty()
.idEqualTo(id)
.idProperty()
.findAll();
}
@override
Stream<List<int>> watchTimelineUsers(int id) {
Stream<List<String>> watchTimelineUsers(String id) {
return db.users
.filter()
.inTimelineEqualTo(true)
.or()
.isarIdEqualTo(id)
.isarIdProperty()
.idEqualTo(id)
.idProperty()
.watch();
}
@override
Stream<RenderList> watchArchiveTimeline(int userId) {
Stream<RenderList> watchArchiveTimeline(String userId) {
final query = db.assets
.where()
.ownerIdEqualToAnyChecksum(userId)
.ownerIdEqualToAnyChecksum(fastHash(userId))
.filter()
.isArchivedEqualTo(true)
.isTrashedEqualTo(false)
@ -52,10 +53,10 @@ class TimelineRepository extends DatabaseRepository
}
@override
Stream<RenderList> watchFavoriteTimeline(int userId) {
Stream<RenderList> watchFavoriteTimeline(String userId) {
final query = db.assets
.where()
.ownerIdEqualToAnyChecksum(userId)
.ownerIdEqualToAnyChecksum(fastHash(userId))
.filter()
.isFavoriteEqualTo(true)
.isTrashedEqualTo(false)
@ -79,10 +80,10 @@ class TimelineRepository extends DatabaseRepository
}
@override
Stream<RenderList> watchTrashTimeline(int userId) {
Stream<RenderList> watchTrashTimeline(String userId) {
final query = db.assets
.filter()
.ownerIdEqualTo(userId)
.ownerIdEqualTo(fastHash(userId))
.isTrashedEqualTo(true)
.sortByFileCreatedAtDesc();
@ -103,12 +104,12 @@ class TimelineRepository extends DatabaseRepository
@override
Stream<RenderList> watchHomeTimeline(
int userId,
String userId,
GroupAssetsBy groupAssetByOption,
) {
final query = db.assets
.where()
.ownerIdEqualToAnyChecksum(userId)
.ownerIdEqualToAnyChecksum(fastHash(userId))
.filter()
.isArchivedEqualTo(false)
.isTrashedEqualTo(false)
@ -120,12 +121,13 @@ class TimelineRepository extends DatabaseRepository
@override
Stream<RenderList> watchMultiUsersTimeline(
List<int> userIds,
List<String> userIds,
GroupAssetsBy groupAssetByOption,
) {
final isarUserIds = userIds.map(fastHash).toList();
final query = db.assets
.where()
.anyOf(userIds, (qb, userId) => qb.ownerIdEqualToAnyChecksum(userId))
.anyOf(isarUserIds, (qb, id) => qb.ownerIdEqualToAnyChecksum(id))
.filter()
.isArchivedEqualTo(false)
.isTrashedEqualTo(false)
@ -143,12 +145,12 @@ class TimelineRepository extends DatabaseRepository
}
@override
Stream<RenderList> watchAssetSelectionTimeline(int userId) {
Stream<RenderList> watchAssetSelectionTimeline(String userId) {
final query = db.assets
.where()
.remoteIdIsNotNull()
.filter()
.ownerIdEqualTo(userId)
.ownerIdEqualTo(fastHash(userId))
.isTrashedEqualTo(false)
.stackPrimaryAssetIdIsNull()
.sortByFileCreatedAtDesc();

View file

@ -28,6 +28,7 @@ import 'package:immich_mobile/repositories/asset.repository.dart';
import 'package:immich_mobile/repositories/backup.repository.dart';
import 'package:immich_mobile/services/entity.service.dart';
import 'package:immich_mobile/services/sync.service.dart';
import 'package:immich_mobile/utils/hash.dart';
import 'package:logging/logging.dart';
final albumServiceProvider = Provider(
@ -208,7 +209,7 @@ class AlbumService {
final Album album = await _albumApiRepository.create(
albumName,
assetIds: assets.map((asset) => asset.remoteId!),
sharedUserIds: sharedUsers.map((user) => user.uid),
sharedUserIds: sharedUsers.map((user) => user.id),
);
await _entityService.fillAlbumWithDatabaseEntities(album);
return _albumRepository.create(album);
@ -296,7 +297,7 @@ class AlbumService {
Future<bool> deleteAlbum(Album album) async {
try {
final userId = _userService.getMyUser().id;
if (album.owner.value?.isarId == userId) {
if (album.owner.value?.isarId == fastHash(userId)) {
await _albumApiRepository.delete(album.remoteId!);
}
if (album.shared) {
@ -362,7 +363,7 @@ class AlbumService {
try {
await _albumApiRepository.removeUser(
album.remoteId!,
userId: user.uid,
userId: user.id,
);
album.sharedUsers.remove(entity.User.fromDto(user));

View file

@ -101,7 +101,7 @@ class AssetService {
_getRemoteAssetChanges(List<UserDto> users, DateTime since) async {
final dto = AssetDeltaSyncDto(
updatedAfter: since,
userIds: users.map((e) => e.uid).toList(),
userIds: users.map((e) => e.id).toList(),
);
final changes = await _apiService.syncApi.getDeltaSync(dto);
return changes == null || changes.needsFullSync
@ -142,7 +142,7 @@ class AssetService {
limit: chunkSize,
updatedUntil: until,
lastId: lastId,
userId: user.uid,
userId: user.id,
);
log.fine("Requesting $chunkSize assets from $lastId");
final List<AssetResponseDto>? assets =

View file

@ -46,10 +46,10 @@ class PartnerService {
Future<bool> removePartner(UserDto partner) async {
try {
await _partnerApiRepository.delete(partner.uid);
await _partnerApiRepository.delete(partner.id);
await _userRepository.update(partner.copyWith(isPartnerSharedBy: false));
} catch (e) {
_log.warning("Failed to remove partner ${partner.uid}", e);
_log.warning("Failed to remove partner ${partner.id}", e);
return false;
}
return true;
@ -57,11 +57,11 @@ class PartnerService {
Future<bool> addPartner(UserDto partner) async {
try {
await _partnerApiRepository.create(partner.uid);
await _partnerApiRepository.create(partner.id);
await _userRepository.update(partner.copyWith(isPartnerSharedBy: true));
return true;
} catch (e) {
_log.warning("Failed to add partner ${partner.uid}", e);
_log.warning("Failed to add partner ${partner.id}", e);
}
return false;
}
@ -72,14 +72,14 @@ class PartnerService {
}) async {
try {
final dto = await _partnerApiRepository.update(
partner.uid,
partner.id,
inTimeline: inTimeline,
);
await _userRepository
.update(partner.copyWith(inTimeline: dto.inTimeline));
return true;
} catch (e) {
_log.warning("Failed to update partner ${partner.uid}", e);
_log.warning("Failed to update partner ${partner.id}", e);
}
return false;
}

View file

@ -32,6 +32,7 @@ import 'package:immich_mobile/services/hash.service.dart';
import 'package:immich_mobile/utils/async_mutex.dart';
import 'package:immich_mobile/utils/datetime_comparison.dart';
import 'package:immich_mobile/utils/diff.dart';
import 'package:immich_mobile/utils/hash.dart';
import 'package:logging/logging.dart';
final syncServiceProvider = Provider(
@ -152,14 +153,14 @@ class SyncService {
/// Syncs users from the server to the local database
/// Returns `true`if there were any changes
Future<bool> _syncUsersFromServer(List<UserDto> users) async {
users.sortBy((u) => u.uid);
users.sortBy((u) => u.id);
final dbUsers = await _userRepository.getAll(sortBy: SortUserBy.id);
final List<int> toDelete = [];
final List<String> toDelete = [];
final List<UserDto> toUpsert = [];
final changes = diffSortedListsSync(
users,
dbUsers,
compare: (UserDto a, UserDto b) => a.uid.compareTo(b.uid),
compare: (UserDto a, UserDto b) => a.id.compareTo(b.id),
both: (UserDto a, UserDto b) {
if (!a.updatedAt.isAtSameMomentAs(b.updatedAt) ||
a.isPartnerSharedBy != b.isPartnerSharedBy ||
@ -319,12 +320,12 @@ class SyncService {
}
Future<void> _updateUserAssetsETag(List<UserDto> users, DateTime time) {
final etags = users.map((u) => ETag(id: u.uid, time: time)).toList();
final etags = users.map((u) => ETag(id: u.id, time: time)).toList();
return _eTagRepository.upsertAll(etags);
}
Future<void> _clearUserAssetsETag(List<UserDto> users) {
final ids = users.map((u) => u.uid).toList();
final ids = users.map((u) => u.id).toList();
return _eTagRepository.deleteByIds(ids);
}
@ -408,7 +409,7 @@ class SyncService {
sharedUsers,
compare: (UserDto a, UserDto b) => a.id.compareTo(b.id),
both: (a, b) => false,
onlyFirst: (UserDto a) => userIdsToAdd.add(a.uid),
onlyFirst: (UserDto a) => userIdsToAdd.add(a.id),
onlySecond: (UserDto a) => usersToUnlink.add(a),
);
@ -459,7 +460,8 @@ class SyncService {
existing.addAll(foreign);
// delete assets in DB unless they belong to this user or part of some other shared album
deleteCandidates.addAll(toUnlink.where((a) => a.ownerId != userId));
final isarUserId = fastHash(userId);
deleteCandidates.addAll(toUnlink.where((a) => a.ownerId != isarUserId));
}
return true;
@ -878,16 +880,16 @@ class SyncService {
return null;
}
users.sortBy((u) => u.uid);
sharedBy.sortBy((u) => u.uid);
sharedWith.sortBy((u) => u.uid);
users.sortBy((u) => u.id);
sharedBy.sortBy((u) => u.id);
sharedWith.sortBy((u) => u.id);
final updatedSharedBy = <UserDto>[];
diffSortedListsSync(
users,
sharedBy,
compare: (UserDto a, UserDto b) => a.uid.compareTo(b.uid),
compare: (UserDto a, UserDto b) => a.id.compareTo(b.id),
both: (UserDto a, UserDto b) {
updatedSharedBy.add(a.copyWith(isPartnerSharedBy: true));
return true;
@ -901,7 +903,7 @@ class SyncService {
diffSortedListsSync(
updatedSharedBy,
sharedWith,
compare: (UserDto a, UserDto b) => a.uid.compareTo(b.uid),
compare: (UserDto a, UserDto b) => a.id.compareTo(b.id),
both: (UserDto a, UserDto b) {
updatedSharedWith.add(
a.copyWith(inTimeline: b.inTimeline, isPartnerSharedWith: true),

View file

@ -28,21 +28,21 @@ class TimelineService {
this._userService,
);
Future<List<int>> getTimelineUserIds() async {
Future<List<String>> getTimelineUserIds() async {
final me = _userService.getMyUser();
return _timelineRepository.getTimelineUserIds(me.id);
}
Stream<List<int>> watchTimelineUserIds() async* {
Stream<List<String>> watchTimelineUserIds() async* {
final me = _userService.getMyUser();
yield* _timelineRepository.watchTimelineUsers(me.id);
}
Stream<RenderList> watchHomeTimeline(int userId) {
Stream<RenderList> watchHomeTimeline(String userId) {
return _timelineRepository.watchHomeTimeline(userId, _getGroupByOption());
}
Stream<RenderList> watchMultiUsersTimeline(List<int> userIds) {
Stream<RenderList> watchMultiUsersTimeline(List<String> userIds) {
return _timelineRepository.watchMultiUsersTimeline(
userIds,
_getGroupByOption(),
@ -83,10 +83,10 @@ class TimelineService {
GroupAssetsBy? groupBy,
) {
GroupAssetsBy groupOption = GroupAssetsBy.none;
if (groupBy != null) {
groupOption = groupBy;
} else {
if (groupBy == null) {
groupOption = _getGroupByOption();
} else {
groupOption = groupBy;
}
return _timelineRepository.getTimelineFromAssets(

View file

@ -6,13 +6,33 @@ import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/entities/etag.entity.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/infrastructure/entities/exif.entity.dart';
import 'package:immich_mobile/infrastructure/entities/store.entity.dart';
import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
import 'package:isar/isar.dart';
const int targetVersion = 8;
const int targetVersion = 9;
Future<void> migrateDatabaseIfNeeded(Isar db) async {
final int version = Store.get(StoreKey.version, 1);
if (version < 9) {
await Store.put(StoreKey.version, version);
final value = await db.storeValues.get(StoreKey.currentUser.id);
if (value != null) {
final id = value.intValue;
if (id == null) {
return;
}
await db.writeTxn(() async {
final user = await db.users.get(id);
await db.storeValues
.put(StoreValue(StoreKey.currentUser.id, strValue: user?.id));
});
}
// Do not clear other entities
return;
}
if (version < targetVersion) {
_migrateTo(db, targetVersion);
}

View file

@ -58,7 +58,7 @@ class AlbumThumbnailCard extends ConsumerWidget {
// Add the owner name to the subtitle
String? owner;
if (showOwner) {
if (album.ownerId == ref.read(currentUserProvider)?.uid) {
if (album.ownerId == ref.read(currentUserProvider)?.id) {
owner = 'album_thumbnail_owned'.tr();
} else if (album.ownerName != null) {
owner = 'album_thumbnail_shared_by'.tr(args: [album.ownerName!]);

View file

@ -19,6 +19,7 @@ import 'package:immich_mobile/providers/server_info.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/services/stack.service.dart';
import 'package:immich_mobile/utils/hash.dart';
import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart';
import 'package:immich_mobile/widgets/asset_grid/delete_dialog.dart';
import 'package:immich_mobile/widgets/asset_viewer/video_controls.dart';
@ -49,7 +50,8 @@ class BottomGalleryBar extends ConsumerWidget {
if (asset == null) {
return const SizedBox();
}
final isOwner = asset.ownerId == ref.watch(currentUserProvider)?.id;
final isOwner =
asset.ownerId == fastHash(ref.watch(currentUserProvider)?.id ?? '');
final showControls = ref.watch(showControlsProvider);
final stackId = asset.stackId;

View file

@ -9,6 +9,7 @@ import 'package:immich_mobile/extensions/theme_extensions.dart';
import 'package:immich_mobile/providers/asset.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/services/asset.service.dart';
import 'package:immich_mobile/utils/hash.dart';
import 'package:immich_mobile/widgets/common/immich_toast.dart';
import 'package:logging/logging.dart';
@ -81,7 +82,7 @@ class DescriptionInput extends HookConsumerWidget {
}
return TextField(
enabled: owner?.id == asset.ownerId,
enabled: fastHash(owner?.id ?? '') == asset.ownerId,
focusNode: focusNode,
onTap: () => isFocus.value = true,
onChanged: (value) {

View file

@ -16,6 +16,7 @@ import 'package:immich_mobile/providers/tab.provider.dart';
import 'package:immich_mobile/providers/trash.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/utils/hash.dart';
import 'package:immich_mobile/widgets/album/add_to_album_bottom_sheet.dart';
import 'package:immich_mobile/widgets/asset_grid/upload_dialog.dart';
import 'package:immich_mobile/widgets/asset_viewer/top_control_app_bar.dart';
@ -33,12 +34,13 @@ class GalleryAppBar extends ConsumerWidget {
return const SizedBox();
}
final album = ref.watch(currentAlbumProvider);
final isOwner = asset.ownerId == ref.watch(currentUserProvider)?.id;
final isOwner =
asset.ownerId == fastHash(ref.watch(currentUserProvider)?.id ?? '');
final showControls = ref.watch(showControlsProvider);
final isPartner = ref
.watch(partnerSharedWithProvider)
.map((e) => e.id)
.map((e) => fastHash(e.id))
.contains(asset.ownerId);
toggleFavorite(Asset asset) =>

View file

@ -8,7 +8,7 @@ import 'package:immich_mobile/services/api.service.dart';
Widget userAvatar(BuildContext context, UserDto u, {double? radius}) {
final url =
"${Store.get(StoreKey.serverEndpoint)}/users/${u.uid}/profile-image";
"${Store.get(StoreKey.serverEndpoint)}/users/${u.id}/profile-image";
final nameFirstLetter = u.name.isNotEmpty ? u.name[0] : "";
return CircleAvatar(
radius: radius,

View file

@ -27,7 +27,7 @@ class UserCircleAvatar extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
bool isDarkTheme = context.themeData.brightness == Brightness.dark;
final profileImageUrl =
'${Store.get(StoreKey.serverEndpoint)}/users/${user.uid}/profile-image?d=${Random().nextInt(1024)}';
'${Store.get(StoreKey.serverEndpoint)}/users/${user.id}/profile-image?d=${Random().nextInt(1024)}';
final textIcon = DefaultTextStyle(
style: TextStyle(

View file

@ -4,7 +4,7 @@ abstract final class UserStub {
const UserStub._();
static final admin = UserDto(
uid: "admin",
id: "admin",
email: "admin@test.com",
name: "admin",
isAdmin: true,
@ -14,7 +14,7 @@ abstract final class UserStub {
);
static final user1 = UserDto(
uid: "user1",
id: "user1",
email: "user1@test.com",
name: "user1",
isAdmin: false,
@ -24,7 +24,7 @@ abstract final class UserStub {
);
static final user2 = UserDto(
uid: "user2",
id: "user2",
email: "user2@test.com",
name: "user2",
isAdmin: false,

View file

@ -66,7 +66,7 @@ void main() {
final MockUserService userService = MockUserService();
final owner = UserDto(
uid: "1",
id: "1",
updatedAt: DateTime.now(),
email: "a@b.c",
name: "first last",
@ -110,7 +110,7 @@ void main() {
);
when(() => userService.getMyUser()).thenReturn(owner);
when(() => eTagRepository.get(owner.id))
.thenAnswer((_) async => ETag(id: owner.uid, time: DateTime.now()));
.thenAnswer((_) async => ETag(id: owner.id, time: DateTime.now()));
when(() => eTagRepository.deleteByIds(["1"])).thenAnswer((_) async {});
when(() => eTagRepository.upsertAll(any())).thenAnswer((_) async {});
when(() => partnerRepository.getSharedWith()).thenAnswer((_) async => []);

View file

@ -149,7 +149,7 @@ void main() {
() => albumApiRepository.create(
"name",
assetIds: [AssetStub.image1.remoteId!],
sharedUserIds: [UserStub.user1.uid],
sharedUserIds: [UserStub.user1.id],
),
).called(1);
verify(
@ -217,7 +217,7 @@ void main() {
final result = await sut.addUsers(
AlbumStub.emptyAlbum,
[UserStub.user2.uid],
[UserStub.user2.id],
);
expect(result, true);

View file

@ -41,7 +41,7 @@ void main() {
[User.fromDto(UserStub.admin), User.fromDto(UserStub.admin)],
);
when(() => userRepository.get(any()))
when(() => userRepository.getByUserId(any()))
.thenAnswer((_) async => UserStub.admin);
when(() => userRepository.getByUserId(any()))
.thenAnswer((_) async => UserStub.admin);