fix avatar render issues, fix list update issues
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run

This commit is contained in:
otsmr 2025-11-18 22:56:42 +01:00
parent e00700d4e3
commit c3e92ac7d1
31 changed files with 129 additions and 124 deletions

View file

@ -0,0 +1 @@
5cde0a78d118f9e043e7443ceca0f306

Binary file not shown.

View file

@ -34,3 +34,6 @@ Map<String, VoidCallback> globalUserDataChangedCallBack = {};
bool globalIsAppInBackground = true;
bool globalAllowErrorTrackingViaSentry = false;
late String globalApplicationCacheDirectory;
late String globalApplicationSupportDirectory;

View file

@ -1,14 +1,7 @@
// ignore_for_file: unused_import
import 'dart:async';
import 'dart:io';
import 'package:camera/camera.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
import 'package:provider/provider.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
@ -25,21 +18,13 @@ import 'package:twonly/src/services/fcm.service.dart';
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
import 'package:twonly/src/services/notifications/setup.notifications.dart';
import 'package:twonly/src/services/twonly_safe/create_backup.twonly_safe.dart';
import 'package:twonly/src/utils/avatars.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/storage.dart';
void main() async {
SentryWidgetsFlutterBinding.ensureInitialized();
// try {
// File(join((await getApplicationSupportDirectory()).path, 'twonly.sqlite'))
// .deleteSync();
// } catch (e) {}
// await updateUserdata((u) {
// u.appVersion = 0;
// return u;
// });
final user = await getUser();
if (user != null) {
gUser = user;
@ -60,6 +45,10 @@ void main() async {
await initFCMService();
globalApplicationCacheDirectory = (await getApplicationCacheDirectory()).path;
globalApplicationSupportDirectory =
(await getApplicationSupportDirectory()).path;
initLogger();
final settingsController = SettingsChangeProvider();

View file

@ -9,7 +9,7 @@ import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart';
import 'package:twonly/src/services/api/messages.dart';
import 'package:twonly/src/services/api/utils.dart';
import 'package:twonly/src/services/notifications/pushkeys.notifications.dart';
import 'package:twonly/src/services/notifications/setup.notifications.dart';
import 'package:twonly/src/utils/avatars.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart';

View file

@ -156,7 +156,7 @@ Future<void> handleMediaUpdate(
);
case EncryptedContent_MediaUpdate_Type.STORED:
Log.info('Got media file stored ${mediaFile.mediaId}');
final mediaService = await MediaFileService.fromMedia(mediaFile);
final mediaService = MediaFileService(mediaFile);
await mediaService.storeMediaFile();
await twonlyDB.messagesDao.updateMessageId(
message.messageId,

View file

@ -117,7 +117,7 @@ Future<void> handleDownloadStatusUpdate(TaskStatusUpdate update) async {
Mutex protectDownload = Mutex();
Future<void> startDownloadMedia(MediaFile media, bool force) async {
final mediaService = await MediaFileService.fromMedia(media);
final mediaService = MediaFileService(media);
if (mediaService.encryptedPath.existsSync()) {
await handleEncryptedFile(media.mediaId);

View file

@ -126,7 +126,7 @@ Future<void> handleUploadStatusUpdate(TaskStatusUpdate update) async {
Log.error(
'Background upload failed for $mediaId with status ${update.status} and ${update.responseStatusCode}. ',
);
final mediaService = await MediaFileService.fromMedia(media);
final mediaService = MediaFileService(media);
await mediaService.setUploadState(UploadState.uploaded);
// In all other cases just try the upload again...

View file

@ -28,7 +28,7 @@ Future<void> finishStartedPreprocessing() async {
continue;
}
try {
final service = await MediaFileService.fromMedia(mediaFile);
final service = MediaFileService(mediaFile);
if (!service.originalPath.existsSync() &&
!service.uploadRequestPath.existsSync()) {
if (service.storedPath.existsSync()) {
@ -78,7 +78,7 @@ Future<MediaFileService?> initializeMediaUpload(
),
);
if (mediaFile == null) return null;
return MediaFileService.fromMedia(mediaFile);
return MediaFileService(mediaFile);
}
Future<void> insertMediaFileInMessagesTable(

View file

@ -2,7 +2,6 @@ import 'dart:async';
import 'dart:io';
import 'package:drift/drift.dart';
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/database/tables/mediafiles.table.dart';
import 'package:twonly/src/database/twonly.db.dart';
@ -11,31 +10,21 @@ import 'package:twonly/src/services/mediafiles/thumbnail.service.dart';
import 'package:twonly/src/utils/log.dart';
class MediaFileService {
MediaFileService(this.mediaFile, {required this.applicationSupportDirectory});
MediaFileService(this.mediaFile);
MediaFile mediaFile;
final Directory applicationSupportDirectory;
static Future<MediaFileService> fromMedia(MediaFile media) async {
return MediaFileService(
media,
applicationSupportDirectory: await getApplicationSupportDirectory(),
);
}
static Future<MediaFileService?> fromMediaId(String mediaId) async {
final mediaFile = await twonlyDB.mediaFilesDao.getMediaFileById(mediaId);
if (mediaFile == null) return null;
return MediaFileService(
mediaFile,
applicationSupportDirectory: await getApplicationSupportDirectory(),
);
}
static Future<void> purgeTempFolder() async {
final tempDirectory = MediaFileService.buildDirectoryPath(
'tmp',
await getApplicationSupportDirectory(),
globalApplicationSupportDirectory,
);
final files = tempDirectory.listSync();
@ -241,11 +230,11 @@ class MediaFileService {
static Directory buildDirectoryPath(
String directory,
Directory applicationSupportDirectory,
String applicationSupportDirectory,
) {
final mediaBaseDir = Directory(
join(
applicationSupportDirectory.path,
applicationSupportDirectory,
'mediafiles',
directory,
),
@ -275,7 +264,7 @@ class MediaFileService {
}
}
final mediaBaseDir =
buildDirectoryPath(directory, applicationSupportDirectory);
buildDirectoryPath(directory, globalApplicationSupportDirectory);
return File(
join(mediaBaseDir.path, '${mediaFile.mediaId}$namePrefix.$extension'),
);

View file

@ -1,13 +1,7 @@
// ignore_for_file: unreachable_from_main
import 'dart:async';
import 'dart:io';
import 'dart:ui' as ui;
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:flutter_svg/svg.dart';
import 'package:path_provider/path_provider.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/utils/misc.dart';
final StreamController<NotificationResponse> selectNotificationStream =
StreamController<NotificationResponse>.broadcast();
@ -54,35 +48,3 @@ Future<void> setupPushNotification() async {
onDidReceiveBackgroundNotificationResponse: notificationTapBackground,
);
}
Future<void> createPushAvatars() async {
if (!Platform.isAndroid) {
return; // avatars currently only shown in Android...
}
final contacts = await twonlyDB.contactsDao.getAllNotBlockedContacts();
for (final contact in contacts) {
if (contact.avatarSvgCompressed == null) continue;
final avatarSvg = getAvatarSvg(contact.avatarSvgCompressed!);
final pictureInfo = await vg.loadPicture(SvgStringLoader(avatarSvg), null);
final image = await pictureInfo.picture.toImage(300, 300);
final byteData = await image.toByteData(format: ui.ImageByteFormat.png);
final pngBytes = byteData!.buffer.asUint8List();
// Get the directory to save the image
final directory = await getApplicationCacheDirectory();
final avatarsDirectory = Directory('${directory.path}/avatars');
// Create the avatars directory if it does not exist
if (!avatarsDirectory.existsSync()) {
await avatarsDirectory.create(recursive: true);
}
final filePath = '${avatarsDirectory.path}/${contact.userId}.png';
await File(filePath).writeAsBytes(pngBytes);
pictureInfo.picture.dispose();
}
}

View file

@ -0,0 +1,36 @@
import 'dart:async';
import 'dart:io';
import 'dart:ui' as ui;
import 'package:flutter_svg/svg.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/utils/misc.dart';
Future<void> createPushAvatars() async {
final contacts = await twonlyDB.contactsDao.getAllNotBlockedContacts();
for (final contact in contacts) {
if (contact.avatarSvgCompressed == null) continue;
final avatarSvg = getAvatarSvg(contact.avatarSvgCompressed!);
final pictureInfo = await vg.loadPicture(SvgStringLoader(avatarSvg), null);
final image = await pictureInfo.picture.toImage(300, 300);
final byteData = await image.toByteData(format: ui.ImageByteFormat.png);
final pngBytes = byteData!.buffer.asUint8List();
await avatarPNGFile(contact.userId).writeAsBytes(pngBytes);
pictureInfo.picture.dispose();
}
}
File avatarPNGFile(int contactId) {
final avatarsDirectory =
Directory('$globalApplicationCacheDirectory/avatars');
if (!avatarsDirectory.existsSync()) {
avatarsDirectory.createSync(recursive: true);
}
return File('${avatarsDirectory.path}/$contactId.png');
}

View file

@ -323,6 +323,7 @@ class UserList extends StatelessWidget {
itemBuilder: (BuildContext context, int i) {
final group = groups[i];
return ListTile(
key: ValueKey(group.groupId),
title: Row(
children: [
Text(substringBy(group.groupName, 12)),

View file

@ -278,6 +278,7 @@ class ContactsListView extends StatelessWidget {
itemBuilder: (context, index) {
final contact = contacts[index];
return ListTile(
key: ValueKey(contact.userId),
title: Text(substringBy(contact.username, 25)),
leading: AvatarIcon(contactId: contact.userId),
trailing: Row(

View file

@ -100,6 +100,7 @@ class _AllReactionsViewState extends State<AllReactionsView> {
child: ListView(
children: reactionsUsers.map((entry) {
return GestureDetector(
key: ValueKey(entry),
onTap: (entry.$2 != null)
? null
: () {

View file

@ -71,9 +71,9 @@ class _ChatListEntryState extends State<ChatListEntry> {
if (widget.message.mediaId != null) {
final mediaFileStream =
twonlyDB.mediaFilesDao.watchMedia(widget.message.mediaId!);
mediaFileSub = mediaFileStream.listen((mediaFiles) async {
mediaFileSub = mediaFileStream.listen((mediaFiles) {
if (mediaFiles != null) {
mediaService = await MediaFileService.fromMedia(mediaFiles);
mediaService = MediaFileService(mediaFiles);
if (mounted) setState(() {});
}
});

View file

@ -123,6 +123,7 @@ class _MessageInfoViewState extends State<MessageInfoView> {
columns.add(
Padding(
key: ValueKey(groupMember.$1.contactId),
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(
children: [

View file

@ -219,7 +219,7 @@ class _StartNewChatView extends State<StartNewChatView> {
if (i < filteredContacts.length) {
return UserContextMenu(
key: Key(filteredContacts[i].userId.toString()),
key: ValueKey(filteredContacts[i].userId),
contact: filteredContacts[i],
child: ListTile(
title: Row(
@ -259,7 +259,7 @@ class _StartNewChatView extends State<StartNewChatView> {
if (i < filteredGroups.length) {
return GroupContextMenu(
key: Key(filteredGroups[i].groupId),
key: ValueKey(filteredGroups[i].groupId),
group: filteredGroups[i],
child: ListTile(
title: Text(

View file

@ -3,7 +3,9 @@ import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/utils/avatars.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:vector_graphics/vector_graphics.dart';
class AvatarIcon extends StatefulWidget {
const AvatarIcon({
@ -25,8 +27,9 @@ class AvatarIcon extends StatefulWidget {
}
class _AvatarIconState extends State<AvatarIcon> {
List<String> _avatarSVGs = [];
List<Contact> _avatarContacts = [];
String? _globalUserDataCallBackId;
String? _avatarSvg;
StreamSubscription<List<Contact>>? groupStream;
StreamSubscription<List<Contact>>? contactsStream;
@ -49,20 +52,43 @@ class _AvatarIconState extends State<AvatarIcon> {
super.dispose();
}
Widget errorBuilder(_, __, ___) {
return const SvgPicture(
AssetBytesLoader('assets/images/default_avatar.svg.vec'),
);
}
Widget getAvatarForContact(Contact contact) {
final avatarFile = avatarPNGFile(contact.userId);
if (avatarFile.existsSync()) {
return Image.file(
avatarFile,
errorBuilder: errorBuilder,
);
}
if (contact.avatarSvgCompressed != null) {
return SvgPicture.string(
getAvatarSvg(contact.avatarSvgCompressed!),
errorBuilder: errorBuilder,
);
}
return errorBuilder(null, null, null);
}
Future<void> initAsync() async {
if (widget.group != null) {
groupStream = twonlyDB.groupsDao
.watchGroupContact(widget.group!.groupId)
.listen((contacts) {
_avatarSVGs = [];
_avatarContacts = [];
if (contacts.length == 1) {
if (contacts.first.avatarSvgCompressed != null) {
_avatarSVGs.add(getAvatarSvg(contacts.first.avatarSvgCompressed!));
_avatarContacts.add(contacts.first);
}
} else {
for (final contact in contacts) {
if (contact.avatarSvgCompressed != null) {
_avatarSVGs.add(getAvatarSvg(contact.avatarSvgCompressed!));
_avatarContacts.add(contact);
}
}
}
@ -73,21 +99,21 @@ class _AvatarIconState extends State<AvatarIcon> {
globalUserDataChangedCallBack[_globalUserDataCallBackId!] = () {
setState(() {
if (gUser.avatarSvg != null) {
_avatarSVGs = [gUser.avatarSvg!];
_avatarSvg = gUser.avatarSvg;
} else {
_avatarSVGs = [];
_avatarContacts = [];
}
});
};
if (gUser.avatarSvg != null) {
_avatarSVGs = [gUser.avatarSvg!];
_avatarSvg = gUser.avatarSvg;
}
} else if (widget.contactId != null) {
contactStream = twonlyDB.contactsDao
.watchContact(widget.contactId!)
.listen((contact) {
if (contact != null && contact.avatarSvgCompressed != null) {
_avatarSVGs = [getAvatarSvg(contact.avatarSvgCompressed!)];
_avatarContacts = [contact];
setState(() {});
}
});
@ -99,27 +125,20 @@ class _AvatarIconState extends State<AvatarIcon> {
Widget build(BuildContext context) {
final proSize = (widget.fontSize == null) ? 40 : (widget.fontSize! * 2);
Widget avatars = SvgPicture.asset('assets/images/default_avatar.svg');
Widget avatars = Container();
if (_avatarSVGs.length == 1) {
if (_avatarSvg != null) {
avatars = SvgPicture.string(
_avatarSVGs.first,
errorBuilder: (a, b, c) => avatars,
);
} else if (_avatarSVGs.length >= 2) {
final a = SvgPicture.string(
_avatarSVGs.first,
errorBuilder: (a, b, c) => avatars,
);
final b = SvgPicture.string(
_avatarSVGs[1],
errorBuilder: (a, b, c) => avatars,
);
if (_avatarSVGs.length >= 3) {
final c = SvgPicture.string(
_avatarSVGs[2],
errorBuilder: (a, b, c) => avatars,
_avatarSvg!,
errorBuilder: errorBuilder,
);
} else if (_avatarContacts.length == 1) {
avatars = getAvatarForContact(_avatarContacts.first);
} else if (_avatarContacts.length >= 2) {
final a = getAvatarForContact(_avatarContacts.first);
final b = getAvatarForContact(_avatarContacts[1]);
if (_avatarContacts.length >= 3) {
final c = getAvatarForContact(_avatarContacts[2]);
avatars = Stack(
children: [
Transform.translate(
@ -153,6 +172,10 @@ class _AvatarIconState extends State<AvatarIcon> {
],
);
}
} else {
avatars = const SvgPicture(
AssetBytesLoader('assets/images/default_avatar.svg.vec'),
);
}
return Container(

View file

@ -100,6 +100,7 @@ class _ContactViewState extends State<ContactView> {
}
final contact = snapshot.data!;
return ListView(
key: ValueKey(contact.userId),
children: [
Padding(
padding: const EdgeInsets.all(10),

View file

@ -228,6 +228,7 @@ class _GroupViewState extends State<GroupView> {
),
...members.map((member) {
return GroupMemberContextMenu(
key: ValueKey(member.$1.userId),
group: group,
contact: member.$1,
member: member.$2,

View file

@ -101,6 +101,7 @@ class _GroupCreateSelectGroupNameViewState
itemBuilder: (BuildContext context, int i) {
final user = widget.selectedUsers[i];
return UserContextMenu(
key: ValueKey(user.userId),
contact: user,
child: ListTile(
title: Row(

View file

@ -188,8 +188,7 @@ class _StartNewChatView extends State<GroupCreateSelectMembersView> {
}
final user = contacts[i];
return UserContextMenu(
// when this is not set, then the avatar is not updated in the list when searching :/
key: Key(user.userId.toString()),
key: ValueKey(user.userId),
contact: user,
child: ListTile(
title: Row(

View file

@ -151,13 +151,12 @@ class HomeViewState extends State<HomeView> {
final draftMedia = await twonlyDB.mediaFilesDao.getDraftMediaFile();
if (draftMedia != null) {
final service = await MediaFileService.fromMedia(draftMedia);
if (!mounted) return;
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ShareImageEditorView(
mediaFileService: service,
mediaFileService: MediaFileService(draftMedia),
sharedFromGallery: true,
),
),

View file

@ -1,7 +1,6 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:path_provider/path_provider.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/database/tables/mediafiles.table.dart';
import 'package:twonly/src/database/twonly.db.dart';
@ -47,13 +46,8 @@ class MemoriesViewState extends State<MemoriesView> {
months = [];
var lastMonth = '';
galleryItems = [];
final applicationSupportDirectory =
await getApplicationSupportDirectory();
for (final mediaFile in mediaFiles) {
final mediaService = MediaFileService(
mediaFile,
applicationSupportDirectory: applicationSupportDirectory,
);
final mediaService = MediaFileService(mediaFile);
if (!mediaService.imagePreviewAvailable) continue;
if (mediaService.mediaFile.type == MediaType.video) {
if (!mediaService.thumbnailPath.existsSync()) {

View file

@ -6,6 +6,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
import 'package:twonly/src/utils/log.dart';
@ -44,12 +45,12 @@ class _ExportMediaViewState extends State<ExportMediaView> {
bool _zipSaved = false;
bool _isStoring = false;
Future<Directory> _mediaFolder() async {
Directory _mediaFolder() {
final dir = MediaFileService.buildDirectoryPath(
'stored',
await getApplicationSupportDirectory(),
globalApplicationSupportDirectory,
);
if (!dir.existsSync()) await dir.create(recursive: true);
if (!dir.existsSync()) dir.createSync(recursive: true);
return dir;
}
@ -62,7 +63,7 @@ class _ExportMediaViewState extends State<ExportMediaView> {
});
try {
final folder = await _mediaFolder();
final folder = _mediaFolder();
final allFiles =
folder.listSync(recursive: true).whereType<File>().toList();

View file

@ -118,7 +118,7 @@ class _ImportMediaViewState extends State<ImportMediaView> {
stored: const Value(true),
),
);
final mediaService = await MediaFileService.fromMedia(mediaFile!);
final mediaService = MediaFileService(mediaFile!);
await mediaService.storedPath.writeAsBytes(file.content);
processed++;

View file

@ -105,6 +105,7 @@ class UserList extends StatelessWidget {
itemBuilder: (BuildContext context, int i) {
final user = users[i];
return UserContextMenu(
key: ValueKey(user.userId),
contact: user,
child: ListTile(
title: Row(

View file

@ -130,7 +130,7 @@ class _DatabaseMigrationViewState extends State<DatabaseMigrationView> {
stored: const Value(true),
),
);
final mediaService = await MediaFileService.fromMedia(mediaFile!);
final mediaService = MediaFileService(mediaFile!);
File(file.path).copySync(mediaService.storedPath.path);
setState(() {
_storedMediaFiles += 1;

View file

@ -1822,7 +1822,7 @@ packages:
source: hosted
version: "4.5.2"
vector_graphics:
dependency: transitive
dependency: "direct main"
description:
name: vector_graphics
sha256: a4f059dc26fc8295b5921376600a194c4ec7d55e72f2fe4c7d2831e103d461e6

View file

@ -75,6 +75,7 @@ dependencies:
share_plus: ^12.0.0
tutorial_coach_mark: ^1.3.0
url_launcher: ^6.3.1
vector_graphics: ^1.1.19
video_player: ^2.9.5
web_socket_channel: ^3.0.1