refactor(mobile): remove int user id (#16814)

* 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 { abstract interface class IUserRepository implements IDatabaseRepository {
Future<bool> insert(UserDto user); Future<bool> insert(UserDto user);
Future<UserDto?> get(int id);
Future<UserDto?> getByUserId(String id); Future<UserDto?> getByUserId(String id);
Future<List<UserDto?>> getByUserIds(List<String> ids); Future<List<UserDto?>> getByUserIds(List<String> ids);
@ -16,7 +14,7 @@ abstract interface class IUserRepository implements IDatabaseRepository {
Future<UserDto> update(UserDto user); Future<UserDto> update(UserDto user);
Future<void> delete(List<int> ids); Future<void> delete(List<String> ids);
Future<void> deleteAll(); Future<void> deleteAll();
} }

View File

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

View File

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

View File

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

View File

@ -78,7 +78,9 @@ class IsarStoreRepository extends IsarDatabaseRepository
const (DateTime) => entity.intValue == null const (DateTime) => entity.intValue == null
? null ? null
: DateTime.fromMillisecondsSinceEpoch(entity.intValue!), : 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, _ => null,
} as T?; } as T?;
@ -89,8 +91,8 @@ class IsarStoreRepository extends IsarDatabaseRepository
const (bool) => ((value as bool) ? 1 : 0, null), const (bool) => ((value as bool) ? 1 : 0, null),
const (DateTime) => ((value as DateTime).millisecondsSinceEpoch, null), const (DateTime) => ((value as DateTime).millisecondsSinceEpoch, null),
const (UserDto) => ( const (UserDto) => (
(await IsarUserRepository(_db).update(value as UserDto)).id,
null, null,
(await IsarUserRepository(_db).update(value as UserDto)).id,
), ),
_ => throw UnsupportedError( _ => throw UnsupportedError(
"Unsupported primitive type: ${key.type} for key: ${key.name}", "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; const IsarUserRepository(super.db) : _db = db;
@override @override
Future<void> delete(List<int> ids) async { Future<void> delete(List<String> ids) async {
await transaction(() 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 @override
Future<List<UserDto>> getAll({SortUserBy? sortBy}) async { Future<List<UserDto>> getAll({SortUserBy? sortBy}) async {
return (await _db.users return (await _db.users

View File

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

View File

@ -19,7 +19,7 @@ abstract interface class IAssetRepository implements IDatabaseRepository {
); );
Future<List<Asset>> getAll({ Future<List<Asset>> getAll({
required int ownerId, required String ownerId,
AssetState? state, AssetState? state,
AssetSort? sortBy, AssetSort? sortBy,
int? limit, int? limit,
@ -29,8 +29,8 @@ abstract interface class IAssetRepository implements IDatabaseRepository {
Future<List<Asset>> getByAlbum( Future<List<Asset>> getByAlbum(
Album album, { Album album, {
Iterable<int> notOwnedBy = const [], Iterable<String> notOwnedBy = const [],
int? ownerId, String? ownerId,
AssetState? state, AssetState? state,
AssetSort? sortBy, AssetSort? sortBy,
}); });
@ -45,7 +45,7 @@ abstract interface class IAssetRepository implements IDatabaseRepository {
Future<List<Asset>> getMatches({ Future<List<Asset>> getMatches({
required List<Asset> assets, required List<Asset> assets,
required int ownerId, required String ownerId,
AssetState? state, AssetState? state,
int limit = 100, int limit = 100,
}); });
@ -64,10 +64,10 @@ abstract interface class IAssetRepository implements IDatabaseRepository {
Stream<Asset?> watchAsset(int id, {bool fireImmediately = false}); 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>> getRecentlyAddedAssets(String userId);
Future<List<Asset>> getMotionAssets(int userId); Future<List<Asset>> getMotionAssets(String userId);
} }
enum AssetSort { checksum, ownerIdChecksum } 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'; import 'package:immich_mobile/interfaces/database.interface.dart';
abstract interface class IETagRepository implements IDatabaseRepository { abstract interface class IETagRepository implements IDatabaseRepository {
Future<ETag?> get(int id); Future<ETag?> get(String id);
Future<ETag?> getById(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'; import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart';
abstract class ITimelineRepository { 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> watchArchiveTimeline(String userId);
Stream<RenderList> watchFavoriteTimeline(int userId); Stream<RenderList> watchFavoriteTimeline(String userId);
Stream<RenderList> watchTrashTimeline(int userId); Stream<RenderList> watchTrashTimeline(String userId);
Stream<RenderList> watchAlbumTimeline( Stream<RenderList> watchAlbumTimeline(
Album album, Album album,
GroupAssetsBy groupAssetsBy, GroupAssetsBy groupAssetsBy,
); );
Stream<RenderList> watchAllVideosTimeline(); Stream<RenderList> watchAllVideosTimeline();
Stream<RenderList> watchHomeTimeline(int userId, GroupAssetsBy groupAssetsBy); Stream<RenderList> watchHomeTimeline(
String userId,
GroupAssetsBy groupAssetsBy,
);
Stream<RenderList> watchMultiUsersTimeline( Stream<RenderList> watchMultiUsersTimeline(
List<int> userIds, List<String> userIds,
GroupAssetsBy groupAssetsBy, GroupAssetsBy groupAssetsBy,
); );
@ -27,5 +30,5 @@ abstract class ITimelineRepository {
GroupAssetsBy getGroupByOption, 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>>({}); final sharedUsersList = useState<Set<UserDto>>({});
addNewUsersHandler() { addNewUsersHandler() {
context.maybePop(sharedUsersList.value.map((e) => e.uid).toList()); context.maybePop(sharedUsersList.value.map((e) => e.id).toList());
} }
buildTileIcon(UserDto user) { buildTileIcon(UserDto user) {
@ -151,7 +151,7 @@ class AlbumAdditionalSharedUserSelectionPage extends HookConsumerWidget {
onData: (users) { onData: (users) {
for (var sharedUsers in album.sharedUsers) { for (var sharedUsers in album.sharedUsers) {
users.removeWhere( 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) { void handleUserClick(UserDto user) {
var actions = []; var actions = [];
if (user.uid == userId) { if (user.id == userId) {
actions = [ actions = [
ListTile( ListTile(
leading: const Icon(Icons.exit_to_app_rounded), leading: const Icon(Icons.exit_to_app_rounded),
@ -170,10 +170,10 @@ class AlbumOptionsPage extends HookConsumerWidget {
color: context.colorScheme.onSurfaceSecondary, color: context.colorScheme.onSurfaceSecondary,
), ),
), ),
trailing: userId == user.uid || isOwner trailing: userId == user.id || isOwner
? const Icon(Icons.more_horiz_rounded) ? const Icon(Icons.more_horiz_rounded)
: const SizedBox(), : const SizedBox(),
onTap: userId == user.uid || isOwner onTap: userId == user.id || isOwner
? () => handleUserClick(user) ? () => handleUserClick(user)
: null, : null,
); );

View File

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

View File

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

View File

@ -153,7 +153,7 @@ class AuthNotifier extends StateNotifier<AuthState> {
state = state.copyWith( state = state.copyWith(
deviceId: deviceId, deviceId: deviceId,
userId: user.uid, userId: user.id,
userEmail: user.email, userEmail: user.email,
isAuthenticated: true, isAuthenticated: true,
name: user.name, 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/services/timeline.service.dart';
import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.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) { (ref, userId) {
if (userId == null) { if (userId == null) {
return const Stream.empty(); return const Stream.empty();
@ -18,7 +18,8 @@ final singleUserTimelineProvider = StreamProvider.family<RenderList, int?>(
dependencies: [localeProvider], dependencies: [localeProvider],
); );
final multiUsersTimelineProvider = StreamProvider.family<RenderList, List<int>>( final multiUsersTimelineProvider =
StreamProvider.family<RenderList, List<String>>(
(ref, userIds) { (ref, userIds) {
ref.watch(localeProvider); ref.watch(localeProvider);
final timelineService = ref.watch(timelineServiceProvider); final timelineService = ref.watch(timelineServiceProvider);

View File

@ -34,7 +34,7 @@ final currentUserProvider =
return CurrentUserProvider(ref.watch(userServiceProvider)); return CurrentUserProvider(ref.watch(userServiceProvider));
}); });
class TimelineUserIdsProvider extends StateNotifier<List<int>> { class TimelineUserIdsProvider extends StateNotifier<List<String>> {
TimelineUserIdsProvider(this._timelineService) : super([]) { TimelineUserIdsProvider(this._timelineService) : super([]) {
_timelineService.getTimelineUserIds().then((users) => state = users); _timelineService.getTimelineUserIds().then((users) => state = users);
streamSub = _timelineService streamSub = _timelineService
@ -42,7 +42,7 @@ class TimelineUserIdsProvider extends StateNotifier<List<int>> {
.listen((users) => state = users); .listen((users) => state = users);
} }
late final StreamSubscription<List<int>> streamSub; late final StreamSubscription<List<String>> streamSub;
final TimelineService _timelineService; final TimelineService _timelineService;
@override @override
@ -53,6 +53,6 @@ class TimelineUserIdsProvider extends StateNotifier<List<int>> {
} }
final timelineUsersIdsProvider = final timelineUsersIdsProvider =
StateNotifierProvider<TimelineUserIdsProvider, List<int>>((ref) { StateNotifierProvider<TimelineUserIdsProvider, List<String>>((ref) {
return TimelineUserIdsProvider(ref.watch(timelineServiceProvider)); 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/models/albums/album_search.model.dart';
import 'package:immich_mobile/providers/db.provider.dart'; import 'package:immich_mobile/providers/db.provider.dart';
import 'package:immich_mobile/repositories/database.repository.dart'; import 'package:immich_mobile/repositories/database.repository.dart';
import 'package:immich_mobile/utils/hash.dart';
import 'package:isar/isar.dart'; import 'package:isar/isar.dart';
final albumRepositoryProvider = final albumRepositoryProvider =
@ -43,14 +44,11 @@ class AlbumRepository extends DatabaseRepository implements IAlbumRepository {
if (shared != null) { if (shared != null) {
query = query.sharedEqualTo(shared); query = query.sharedEqualTo(shared);
} }
final isarUserId = fastHash(Store.get(StoreKey.currentUser).id);
if (owner == true) { if (owner == true) {
query = query.owner( query = query.owner((q) => q.isarIdEqualTo(isarUserId));
(q) => q.isarIdEqualTo(Store.get(StoreKey.currentUser).id),
);
} else if (owner == false) { } else if (owner == false) {
query = query.owner( query = query.owner((q) => q.not().isarIdEqualTo(isarUserId));
(q) => q.not().isarIdEqualTo(Store.get(StoreKey.currentUser).id),
);
} }
if (remote == true) { if (remote == true) {
query = query.localIdIsNull(); query = query.localIdIsNull();
@ -140,16 +138,13 @@ class AlbumRepository extends DatabaseRepository implements IAlbumRepository {
.filter() .filter()
.nameContains(searchTerm, caseSensitive: false) .nameContains(searchTerm, caseSensitive: false)
.remoteIdIsNotNull(); .remoteIdIsNotNull();
final isarUserId = fastHash(Store.get(StoreKey.currentUser).id);
switch (filterMode) { switch (filterMode) {
case QuickFilterMode.sharedWithMe: case QuickFilterMode.sharedWithMe:
query = query.owner( query = query.owner((q) => q.not().isarIdEqualTo(isarUserId));
(q) => q.not().isarIdEqualTo(Store.get(StoreKey.currentUser).id),
);
case QuickFilterMode.myAlbums: case QuickFilterMode.myAlbums:
query = query.owner( query = query.owner((q) => q.isarIdEqualTo(isarUserId));
(q) => q.isarIdEqualTo(Store.get(StoreKey.currentUser).id),
);
case QuickFilterMode.all: case QuickFilterMode.all:
break; 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/interfaces/asset.interface.dart';
import 'package:immich_mobile/providers/db.provider.dart'; import 'package:immich_mobile/providers/db.provider.dart';
import 'package:immich_mobile/repositories/database.repository.dart'; import 'package:immich_mobile/repositories/database.repository.dart';
import 'package:immich_mobile/utils/hash.dart';
import 'package:isar/isar.dart'; import 'package:isar/isar.dart';
final assetRepositoryProvider = final assetRepositoryProvider =
@ -22,20 +23,21 @@ class AssetRepository extends DatabaseRepository implements IAssetRepository {
@override @override
Future<List<Asset>> getByAlbum( Future<List<Asset>> getByAlbum(
Album album, { Album album, {
Iterable<int> notOwnedBy = const [], Iterable<String> notOwnedBy = const [],
int? ownerId, String? ownerId,
AssetState? state, AssetState? state,
AssetSort? sortBy, AssetSort? sortBy,
}) { }) {
var query = album.assets.filter(); var query = album.assets.filter();
final isarUserIds = notOwnedBy.map(fastHash).toList();
if (notOwnedBy.length == 1) { if (notOwnedBy.length == 1) {
query = query.not().ownerIdEqualTo(notOwnedBy.first); query = query.not().ownerIdEqualTo(isarUserIds.first);
} else if (notOwnedBy.isNotEmpty) { } else if (notOwnedBy.isNotEmpty) {
query = query =
query.not().anyOf(notOwnedBy, (q, int id) => q.ownerIdEqualTo(id)); query.not().anyOf(isarUserIds, (q, int id) => q.ownerIdEqualTo(id));
} }
if (ownerId != null) { if (ownerId != null) {
query = query.ownerIdEqualTo(ownerId); query = query.ownerIdEqualTo(fastHash(ownerId));
} }
if (state != null) { if (state != null) {
@ -87,27 +89,28 @@ class AssetRepository extends DatabaseRepository implements IAssetRepository {
@override @override
Future<List<Asset>> getAll({ Future<List<Asset>> getAll({
required int ownerId, required String ownerId,
AssetState? state, AssetState? state,
AssetSort? sortBy, AssetSort? sortBy,
int? limit, int? limit,
}) { }) {
final baseQuery = db.assets.where(); final baseQuery = db.assets.where();
final isarUserIds = fastHash(ownerId);
final QueryBuilder<Asset, Asset, QAfterFilterCondition> filteredQuery = final QueryBuilder<Asset, Asset, QAfterFilterCondition> filteredQuery =
switch (state) { switch (state) {
null => baseQuery.ownerIdEqualToAnyChecksum(ownerId).noOp(), null => baseQuery.ownerIdEqualToAnyChecksum(isarUserIds).noOp(),
AssetState.local => baseQuery AssetState.local => baseQuery
.remoteIdIsNull() .remoteIdIsNull()
.filter() .filter()
.localIdIsNotNull() .localIdIsNotNull()
.ownerIdEqualTo(ownerId), .ownerIdEqualTo(isarUserIds),
AssetState.remote => baseQuery AssetState.remote => baseQuery
.localIdIsNull() .localIdIsNull()
.filter() .filter()
.remoteIdIsNotNull() .remoteIdIsNotNull()
.ownerIdEqualTo(ownerId), .ownerIdEqualTo(isarUserIds),
AssetState.merged => baseQuery AssetState.merged => baseQuery
.ownerIdEqualToAnyChecksum(ownerId) .ownerIdEqualToAnyChecksum(isarUserIds)
.filter() .filter()
.remoteIdIsNotNull() .remoteIdIsNotNull()
.localIdIsNotNull(), .localIdIsNotNull(),
@ -132,7 +135,7 @@ class AssetRepository extends DatabaseRepository implements IAssetRepository {
@override @override
Future<List<Asset>> getMatches({ Future<List<Asset>> getMatches({
required List<Asset> assets, required List<Asset> assets,
required int ownerId, required String ownerId,
AssetState? state, AssetState? state,
int limit = 100, int limit = 100,
}) { }) {
@ -147,7 +150,7 @@ class AssetRepository extends DatabaseRepository implements IAssetRepository {
AssetState.merged => AssetState.merged =>
baseQuery.localIdIsNotNull().filter().remoteIdIsNotNull(), baseQuery.localIdIsNotNull().filter().remoteIdIsNotNull(),
}; };
return _getMatchesImpl(query, ownerId, assets, limit); return _getMatchesImpl(query, fastHash(ownerId), assets, limit);
} }
@override @override
@ -185,10 +188,10 @@ class AssetRepository extends DatabaseRepository implements IAssetRepository {
@override @override
Future<List<Asset?>> getAllByOwnerIdChecksum( Future<List<Asset?>> getAllByOwnerIdChecksum(
List<int> ids, List<int> ownerIds,
List<String> checksums, List<String> checksums,
) => ) =>
db.assets.getAllByOwnerIdChecksum(ids, checksums); db.assets.getAllByOwnerIdChecksum(ownerIds, checksums);
@override @override
Future<List<Asset>> getAllLocal() => Future<List<Asset>> getAllLocal() =>
@ -224,30 +227,30 @@ class AssetRepository extends DatabaseRepository implements IAssetRepository {
} }
@override @override
Future<List<Asset>> getTrashAssets(int userId) { Future<List<Asset>> getTrashAssets(String userId) {
return db.assets return db.assets
.where() .where()
.remoteIdIsNotNull() .remoteIdIsNotNull()
.filter() .filter()
.ownerIdEqualTo(userId) .ownerIdEqualTo(fastHash(userId))
.isTrashedEqualTo(true) .isTrashedEqualTo(true)
.findAll(); .findAll();
} }
@override @override
Future<List<Asset>> getRecentlyAddedAssets(int userId) { Future<List<Asset>> getRecentlyAddedAssets(String userId) {
return db.assets return db.assets
.where() .where()
.ownerIdEqualToAnyChecksum(userId) .ownerIdEqualToAnyChecksum(fastHash(userId))
.sortByFileCreatedAtDesc() .sortByFileCreatedAtDesc()
.findAll(); .findAll();
} }
@override @override
Future<List<Asset>> getMotionAssets(int userId) { Future<List<Asset>> getMotionAssets(String userId) {
return db.assets return db.assets
.where() .where()
.ownerIdEqualToAnyChecksum(userId) .ownerIdEqualToAnyChecksum(fastHash(userId))
.filter() .filter()
.livePhotoVideoIdIsNotNull() .livePhotoVideoIdIsNotNull()
.findAll(); .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/asset.entity.dart';
import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/interfaces/asset_media.interface.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; import 'package:photo_manager/photo_manager.dart' hide AssetType;
final assetMediaRepositoryProvider = Provider((ref) => AssetMediaRepository()); final assetMediaRepositoryProvider = Provider((ref) => AssetMediaRepository());
@ -24,7 +25,7 @@ class AssetMediaRepository implements IAssetMediaRepository {
final Asset asset = Asset( final Asset asset = Asset(
checksum: "", checksum: "",
localId: local.id, localId: local.id,
ownerId: Store.get(StoreKey.currentUser).id, ownerId: fastHash(Store.get(StoreKey.currentUser).id),
fileCreatedAt: local.createDateTime, fileCreatedAt: local.createDateTime,
fileModifiedAt: local.modifiedDateTime, fileModifiedAt: local.modifiedDateTime,
updatedAt: 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(); Future<List<String>> getAllIds() => db.eTags.where().idProperty().findAll();
@override @override
Future<ETag?> get(int id) => db.eTags.get(id); Future<ETag?> get(String id) => db.eTags.getById(id);
@override @override
Future<void> upsertAll(List<ETag> etags) => txn(() => db.eTags.putAll(etags)); 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/interfaces/timeline.interface.dart';
import 'package:immich_mobile/providers/db.provider.dart'; import 'package:immich_mobile/providers/db.provider.dart';
import 'package:immich_mobile/repositories/database.repository.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:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart';
import 'package:isar/isar.dart'; import 'package:isar/isar.dart';
@ -17,32 +18,32 @@ class TimelineRepository extends DatabaseRepository
TimelineRepository(super.db); TimelineRepository(super.db);
@override @override
Future<List<int>> getTimelineUserIds(int id) { Future<List<String>> getTimelineUserIds(String id) {
return db.users return db.users
.filter() .filter()
.inTimelineEqualTo(true) .inTimelineEqualTo(true)
.or() .or()
.isarIdEqualTo(id) .idEqualTo(id)
.isarIdProperty() .idProperty()
.findAll(); .findAll();
} }
@override @override
Stream<List<int>> watchTimelineUsers(int id) { Stream<List<String>> watchTimelineUsers(String id) {
return db.users return db.users
.filter() .filter()
.inTimelineEqualTo(true) .inTimelineEqualTo(true)
.or() .or()
.isarIdEqualTo(id) .idEqualTo(id)
.isarIdProperty() .idProperty()
.watch(); .watch();
} }
@override @override
Stream<RenderList> watchArchiveTimeline(int userId) { Stream<RenderList> watchArchiveTimeline(String userId) {
final query = db.assets final query = db.assets
.where() .where()
.ownerIdEqualToAnyChecksum(userId) .ownerIdEqualToAnyChecksum(fastHash(userId))
.filter() .filter()
.isArchivedEqualTo(true) .isArchivedEqualTo(true)
.isTrashedEqualTo(false) .isTrashedEqualTo(false)
@ -52,10 +53,10 @@ class TimelineRepository extends DatabaseRepository
} }
@override @override
Stream<RenderList> watchFavoriteTimeline(int userId) { Stream<RenderList> watchFavoriteTimeline(String userId) {
final query = db.assets final query = db.assets
.where() .where()
.ownerIdEqualToAnyChecksum(userId) .ownerIdEqualToAnyChecksum(fastHash(userId))
.filter() .filter()
.isFavoriteEqualTo(true) .isFavoriteEqualTo(true)
.isTrashedEqualTo(false) .isTrashedEqualTo(false)
@ -79,10 +80,10 @@ class TimelineRepository extends DatabaseRepository
} }
@override @override
Stream<RenderList> watchTrashTimeline(int userId) { Stream<RenderList> watchTrashTimeline(String userId) {
final query = db.assets final query = db.assets
.filter() .filter()
.ownerIdEqualTo(userId) .ownerIdEqualTo(fastHash(userId))
.isTrashedEqualTo(true) .isTrashedEqualTo(true)
.sortByFileCreatedAtDesc(); .sortByFileCreatedAtDesc();
@ -103,12 +104,12 @@ class TimelineRepository extends DatabaseRepository
@override @override
Stream<RenderList> watchHomeTimeline( Stream<RenderList> watchHomeTimeline(
int userId, String userId,
GroupAssetsBy groupAssetByOption, GroupAssetsBy groupAssetByOption,
) { ) {
final query = db.assets final query = db.assets
.where() .where()
.ownerIdEqualToAnyChecksum(userId) .ownerIdEqualToAnyChecksum(fastHash(userId))
.filter() .filter()
.isArchivedEqualTo(false) .isArchivedEqualTo(false)
.isTrashedEqualTo(false) .isTrashedEqualTo(false)
@ -120,12 +121,13 @@ class TimelineRepository extends DatabaseRepository
@override @override
Stream<RenderList> watchMultiUsersTimeline( Stream<RenderList> watchMultiUsersTimeline(
List<int> userIds, List<String> userIds,
GroupAssetsBy groupAssetByOption, GroupAssetsBy groupAssetByOption,
) { ) {
final isarUserIds = userIds.map(fastHash).toList();
final query = db.assets final query = db.assets
.where() .where()
.anyOf(userIds, (qb, userId) => qb.ownerIdEqualToAnyChecksum(userId)) .anyOf(isarUserIds, (qb, id) => qb.ownerIdEqualToAnyChecksum(id))
.filter() .filter()
.isArchivedEqualTo(false) .isArchivedEqualTo(false)
.isTrashedEqualTo(false) .isTrashedEqualTo(false)
@ -143,12 +145,12 @@ class TimelineRepository extends DatabaseRepository
} }
@override @override
Stream<RenderList> watchAssetSelectionTimeline(int userId) { Stream<RenderList> watchAssetSelectionTimeline(String userId) {
final query = db.assets final query = db.assets
.where() .where()
.remoteIdIsNotNull() .remoteIdIsNotNull()
.filter() .filter()
.ownerIdEqualTo(userId) .ownerIdEqualTo(fastHash(userId))
.isTrashedEqualTo(false) .isTrashedEqualTo(false)
.stackPrimaryAssetIdIsNull() .stackPrimaryAssetIdIsNull()
.sortByFileCreatedAtDesc(); .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/repositories/backup.repository.dart';
import 'package:immich_mobile/services/entity.service.dart'; import 'package:immich_mobile/services/entity.service.dart';
import 'package:immich_mobile/services/sync.service.dart'; import 'package:immich_mobile/services/sync.service.dart';
import 'package:immich_mobile/utils/hash.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
final albumServiceProvider = Provider( final albumServiceProvider = Provider(
@ -208,7 +209,7 @@ class AlbumService {
final Album album = await _albumApiRepository.create( final Album album = await _albumApiRepository.create(
albumName, albumName,
assetIds: assets.map((asset) => asset.remoteId!), assetIds: assets.map((asset) => asset.remoteId!),
sharedUserIds: sharedUsers.map((user) => user.uid), sharedUserIds: sharedUsers.map((user) => user.id),
); );
await _entityService.fillAlbumWithDatabaseEntities(album); await _entityService.fillAlbumWithDatabaseEntities(album);
return _albumRepository.create(album); return _albumRepository.create(album);
@ -296,7 +297,7 @@ class AlbumService {
Future<bool> deleteAlbum(Album album) async { Future<bool> deleteAlbum(Album album) async {
try { try {
final userId = _userService.getMyUser().id; final userId = _userService.getMyUser().id;
if (album.owner.value?.isarId == userId) { if (album.owner.value?.isarId == fastHash(userId)) {
await _albumApiRepository.delete(album.remoteId!); await _albumApiRepository.delete(album.remoteId!);
} }
if (album.shared) { if (album.shared) {
@ -362,7 +363,7 @@ class AlbumService {
try { try {
await _albumApiRepository.removeUser( await _albumApiRepository.removeUser(
album.remoteId!, album.remoteId!,
userId: user.uid, userId: user.id,
); );
album.sharedUsers.remove(entity.User.fromDto(user)); album.sharedUsers.remove(entity.User.fromDto(user));

View File

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

View File

@ -46,10 +46,10 @@ class PartnerService {
Future<bool> removePartner(UserDto partner) async { Future<bool> removePartner(UserDto partner) async {
try { try {
await _partnerApiRepository.delete(partner.uid); await _partnerApiRepository.delete(partner.id);
await _userRepository.update(partner.copyWith(isPartnerSharedBy: false)); await _userRepository.update(partner.copyWith(isPartnerSharedBy: false));
} catch (e) { } catch (e) {
_log.warning("Failed to remove partner ${partner.uid}", e); _log.warning("Failed to remove partner ${partner.id}", e);
return false; return false;
} }
return true; return true;
@ -57,11 +57,11 @@ class PartnerService {
Future<bool> addPartner(UserDto partner) async { Future<bool> addPartner(UserDto partner) async {
try { try {
await _partnerApiRepository.create(partner.uid); await _partnerApiRepository.create(partner.id);
await _userRepository.update(partner.copyWith(isPartnerSharedBy: true)); await _userRepository.update(partner.copyWith(isPartnerSharedBy: true));
return true; return true;
} catch (e) { } catch (e) {
_log.warning("Failed to add partner ${partner.uid}", e); _log.warning("Failed to add partner ${partner.id}", e);
} }
return false; return false;
} }
@ -72,14 +72,14 @@ class PartnerService {
}) async { }) async {
try { try {
final dto = await _partnerApiRepository.update( final dto = await _partnerApiRepository.update(
partner.uid, partner.id,
inTimeline: inTimeline, inTimeline: inTimeline,
); );
await _userRepository await _userRepository
.update(partner.copyWith(inTimeline: dto.inTimeline)); .update(partner.copyWith(inTimeline: dto.inTimeline));
return true; return true;
} catch (e) { } catch (e) {
_log.warning("Failed to update partner ${partner.uid}", e); _log.warning("Failed to update partner ${partner.id}", e);
} }
return false; 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/async_mutex.dart';
import 'package:immich_mobile/utils/datetime_comparison.dart'; import 'package:immich_mobile/utils/datetime_comparison.dart';
import 'package:immich_mobile/utils/diff.dart'; import 'package:immich_mobile/utils/diff.dart';
import 'package:immich_mobile/utils/hash.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
final syncServiceProvider = Provider( final syncServiceProvider = Provider(
@ -152,14 +153,14 @@ class SyncService {
/// Syncs users from the server to the local database /// Syncs users from the server to the local database
/// Returns `true`if there were any changes /// Returns `true`if there were any changes
Future<bool> _syncUsersFromServer(List<UserDto> users) async { 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 dbUsers = await _userRepository.getAll(sortBy: SortUserBy.id);
final List<int> toDelete = []; final List<String> toDelete = [];
final List<UserDto> toUpsert = []; final List<UserDto> toUpsert = [];
final changes = diffSortedListsSync( final changes = diffSortedListsSync(
users, users,
dbUsers, 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) { both: (UserDto a, UserDto b) {
if (!a.updatedAt.isAtSameMomentAs(b.updatedAt) || if (!a.updatedAt.isAtSameMomentAs(b.updatedAt) ||
a.isPartnerSharedBy != b.isPartnerSharedBy || a.isPartnerSharedBy != b.isPartnerSharedBy ||
@ -319,12 +320,12 @@ class SyncService {
} }
Future<void> _updateUserAssetsETag(List<UserDto> users, DateTime time) { 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); return _eTagRepository.upsertAll(etags);
} }
Future<void> _clearUserAssetsETag(List<UserDto> users) { 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); return _eTagRepository.deleteByIds(ids);
} }
@ -408,7 +409,7 @@ class SyncService {
sharedUsers, sharedUsers,
compare: (UserDto a, UserDto b) => a.id.compareTo(b.id), compare: (UserDto a, UserDto b) => a.id.compareTo(b.id),
both: (a, b) => false, both: (a, b) => false,
onlyFirst: (UserDto a) => userIdsToAdd.add(a.uid), onlyFirst: (UserDto a) => userIdsToAdd.add(a.id),
onlySecond: (UserDto a) => usersToUnlink.add(a), onlySecond: (UserDto a) => usersToUnlink.add(a),
); );
@ -459,7 +460,8 @@ class SyncService {
existing.addAll(foreign); existing.addAll(foreign);
// delete assets in DB unless they belong to this user or part of some other shared album // 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; return true;
@ -878,16 +880,16 @@ class SyncService {
return null; return null;
} }
users.sortBy((u) => u.uid); users.sortBy((u) => u.id);
sharedBy.sortBy((u) => u.uid); sharedBy.sortBy((u) => u.id);
sharedWith.sortBy((u) => u.uid); sharedWith.sortBy((u) => u.id);
final updatedSharedBy = <UserDto>[]; final updatedSharedBy = <UserDto>[];
diffSortedListsSync( diffSortedListsSync(
users, users,
sharedBy, 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) { both: (UserDto a, UserDto b) {
updatedSharedBy.add(a.copyWith(isPartnerSharedBy: true)); updatedSharedBy.add(a.copyWith(isPartnerSharedBy: true));
return true; return true;
@ -901,7 +903,7 @@ class SyncService {
diffSortedListsSync( diffSortedListsSync(
updatedSharedBy, updatedSharedBy,
sharedWith, 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) { both: (UserDto a, UserDto b) {
updatedSharedWith.add( updatedSharedWith.add(
a.copyWith(inTimeline: b.inTimeline, isPartnerSharedWith: true), a.copyWith(inTimeline: b.inTimeline, isPartnerSharedWith: true),

View File

@ -28,21 +28,21 @@ class TimelineService {
this._userService, this._userService,
); );
Future<List<int>> getTimelineUserIds() async { Future<List<String>> getTimelineUserIds() async {
final me = _userService.getMyUser(); final me = _userService.getMyUser();
return _timelineRepository.getTimelineUserIds(me.id); return _timelineRepository.getTimelineUserIds(me.id);
} }
Stream<List<int>> watchTimelineUserIds() async* { Stream<List<String>> watchTimelineUserIds() async* {
final me = _userService.getMyUser(); final me = _userService.getMyUser();
yield* _timelineRepository.watchTimelineUsers(me.id); yield* _timelineRepository.watchTimelineUsers(me.id);
} }
Stream<RenderList> watchHomeTimeline(int userId) { Stream<RenderList> watchHomeTimeline(String userId) {
return _timelineRepository.watchHomeTimeline(userId, _getGroupByOption()); return _timelineRepository.watchHomeTimeline(userId, _getGroupByOption());
} }
Stream<RenderList> watchMultiUsersTimeline(List<int> userIds) { Stream<RenderList> watchMultiUsersTimeline(List<String> userIds) {
return _timelineRepository.watchMultiUsersTimeline( return _timelineRepository.watchMultiUsersTimeline(
userIds, userIds,
_getGroupByOption(), _getGroupByOption(),
@ -83,10 +83,10 @@ class TimelineService {
GroupAssetsBy? groupBy, GroupAssetsBy? groupBy,
) { ) {
GroupAssetsBy groupOption = GroupAssetsBy.none; GroupAssetsBy groupOption = GroupAssetsBy.none;
if (groupBy != null) { if (groupBy == null) {
groupOption = groupBy;
} else {
groupOption = _getGroupByOption(); groupOption = _getGroupByOption();
} else {
groupOption = groupBy;
} }
return _timelineRepository.getTimelineFromAssets( 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/etag.entity.dart';
import 'package:immich_mobile/entities/store.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/exif.entity.dart';
import 'package:immich_mobile/infrastructure/entities/store.entity.dart';
import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
import 'package:isar/isar.dart'; import 'package:isar/isar.dart';
const int targetVersion = 8; const int targetVersion = 9;
Future<void> migrateDatabaseIfNeeded(Isar db) async { Future<void> migrateDatabaseIfNeeded(Isar db) async {
final int version = Store.get(StoreKey.version, 1); 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) { if (version < targetVersion) {
_migrateTo(db, targetVersion); _migrateTo(db, targetVersion);
} }

View File

@ -58,7 +58,7 @@ class AlbumThumbnailCard extends ConsumerWidget {
// Add the owner name to the subtitle // Add the owner name to the subtitle
String? owner; String? owner;
if (showOwner) { if (showOwner) {
if (album.ownerId == ref.read(currentUserProvider)?.uid) { if (album.ownerId == ref.read(currentUserProvider)?.id) {
owner = 'album_thumbnail_owned'.tr(); owner = 'album_thumbnail_owned'.tr();
} else if (album.ownerName != null) { } else if (album.ownerName != null) {
owner = 'album_thumbnail_shared_by'.tr(args: [album.ownerName!]); 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/providers/user.provider.dart';
import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/services/stack.service.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/asset_grid_data_structure.dart';
import 'package:immich_mobile/widgets/asset_grid/delete_dialog.dart'; import 'package:immich_mobile/widgets/asset_grid/delete_dialog.dart';
import 'package:immich_mobile/widgets/asset_viewer/video_controls.dart'; import 'package:immich_mobile/widgets/asset_viewer/video_controls.dart';
@ -49,7 +50,8 @@ class BottomGalleryBar extends ConsumerWidget {
if (asset == null) { if (asset == null) {
return const SizedBox(); 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 showControls = ref.watch(showControlsProvider);
final stackId = asset.stackId; 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/asset.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/services/asset.service.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:immich_mobile/widgets/common/immich_toast.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
@ -81,7 +82,7 @@ class DescriptionInput extends HookConsumerWidget {
} }
return TextField( return TextField(
enabled: owner?.id == asset.ownerId, enabled: fastHash(owner?.id ?? '') == asset.ownerId,
focusNode: focusNode, focusNode: focusNode,
onTap: () => isFocus.value = true, onTap: () => isFocus.value = true,
onChanged: (value) { 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/trash.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/routing/router.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/album/add_to_album_bottom_sheet.dart';
import 'package:immich_mobile/widgets/asset_grid/upload_dialog.dart'; import 'package:immich_mobile/widgets/asset_grid/upload_dialog.dart';
import 'package:immich_mobile/widgets/asset_viewer/top_control_app_bar.dart'; import 'package:immich_mobile/widgets/asset_viewer/top_control_app_bar.dart';
@ -33,12 +34,13 @@ class GalleryAppBar extends ConsumerWidget {
return const SizedBox(); return const SizedBox();
} }
final album = ref.watch(currentAlbumProvider); 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 showControls = ref.watch(showControlsProvider);
final isPartner = ref final isPartner = ref
.watch(partnerSharedWithProvider) .watch(partnerSharedWithProvider)
.map((e) => e.id) .map((e) => fastHash(e.id))
.contains(asset.ownerId); .contains(asset.ownerId);
toggleFavorite(Asset asset) => 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}) { Widget userAvatar(BuildContext context, UserDto u, {double? radius}) {
final url = 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] : ""; final nameFirstLetter = u.name.isNotEmpty ? u.name[0] : "";
return CircleAvatar( return CircleAvatar(
radius: radius, radius: radius,

View File

@ -27,7 +27,7 @@ class UserCircleAvatar extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
bool isDarkTheme = context.themeData.brightness == Brightness.dark; bool isDarkTheme = context.themeData.brightness == Brightness.dark;
final profileImageUrl = 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( final textIcon = DefaultTextStyle(
style: TextStyle( style: TextStyle(

View File

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

View File

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

View File

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

View File

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