fixing memories

This commit is contained in:
otsmr 2025-10-24 00:03:42 +02:00
parent 1c154e6c67
commit 84828cd820
21 changed files with 347 additions and 413 deletions

View file

@ -6,7 +6,6 @@ import 'package:twonly/globals.dart';
import 'package:twonly/src/localization/generated/app_localizations.dart'; import 'package:twonly/src/localization/generated/app_localizations.dart';
import 'package:twonly/src/providers/connection.provider.dart'; import 'package:twonly/src/providers/connection.provider.dart';
import 'package:twonly/src/providers/settings.provider.dart'; import 'package:twonly/src/providers/settings.provider.dart';
import 'package:twonly/src/services/api/mediafiles/upload.service.dart';
import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/utils/storage.dart';
import 'package:twonly/src/views/components/app_outdated.dart'; import 'package:twonly/src/views/components/app_outdated.dart';
import 'package:twonly/src/views/home.view.dart'; import 'package:twonly/src/views/home.view.dart';
@ -68,8 +67,6 @@ class _AppState extends State<App> with WidgetsBindingObserver {
await setUserPlan(); await setUserPlan();
await apiService.connect(force: true); await apiService.connect(force: true);
await apiService.listenToNetworkChanges(); await apiService.listenToNetworkChanges();
// call this function so invalid media files are get purged
await retryMediaUpload(true);
} }
@override @override
@ -84,7 +81,6 @@ class _AppState extends State<App> with WidgetsBindingObserver {
} else if (state == AppLifecycleState.paused) { } else if (state == AppLifecycleState.paused) {
wasPaused = true; wasPaused = true;
globalIsAppInBackground = true; globalIsAppInBackground = true;
unawaited(handleUploadWhenAppGoesBackground());
} }
} }

View file

@ -25,6 +25,14 @@ class MediaFilesDao extends DatabaseAccessor<TwonlyDB>
} }
} }
Future<void> deleteMediaFile(String mediaId) async {
await (delete(mediaFiles)
..where(
(t) => t.mediaId.equals(mediaId),
))
.go();
}
Future<void> updateMedia( Future<void> updateMedia(
String mediaId, String mediaId,
MediaFilesCompanion updates, MediaFilesCompanion updates,
@ -57,4 +65,8 @@ class MediaFilesDao extends DatabaseAccessor<TwonlyDB>
..where((t) => t.downloadState.equals(DownloadState.pending.name))) ..where((t) => t.downloadState.equals(DownloadState.pending.name)))
.get(); .get();
} }
Stream<List<MediaFile>> watchAllStoredMediaFiles() {
return (select(mediaFiles)..where((t) => t.stored.equals(true))).watch();
}
} }

View file

@ -4,6 +4,8 @@ import 'package:twonly/src/database/tables/contacts.table.dart';
import 'package:twonly/src/database/tables/groups.table.dart'; import 'package:twonly/src/database/tables/groups.table.dart';
import 'package:twonly/src/database/tables/mediafiles.table.dart'; import 'package:twonly/src/database/tables/mediafiles.table.dart';
enum MessageType { media, text }
@DataClassName('Message') @DataClassName('Message')
class Messages extends Table { class Messages extends Table {
TextColumn get groupId => TextColumn get groupId =>
@ -14,9 +16,12 @@ class Messages extends Table {
IntColumn get senderId => IntColumn get senderId =>
integer().nullable().references(Contacts, #userId)(); integer().nullable().references(Contacts, #userId)();
TextColumn get type => textEnum<MessageType>()();
TextColumn get content => text().nullable()(); TextColumn get content => text().nullable()();
TextColumn get mediaId => TextColumn get mediaId => text()
text().nullable().references(MediaFiles, #mediaId)(); .nullable()
.references(MediaFiles, #mediaId, onDelete: KeyAction.cascade)();
BoolColumn get mediaStored => boolean().withDefault(const Constant(false))(); BoolColumn get mediaStored => boolean().withDefault(const Constant(false))();

View file

@ -2254,6 +2254,11 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> {
requiredDuringInsert: false, requiredDuringInsert: false,
defaultConstraints: defaultConstraints:
GeneratedColumn.constraintIsAlways('REFERENCES contacts (user_id)')); GeneratedColumn.constraintIsAlways('REFERENCES contacts (user_id)'));
@override
late final GeneratedColumnWithTypeConverter<MessageType, String> type =
GeneratedColumn<String>('type', aliasedName, false,
type: DriftSqlType.string, requiredDuringInsert: true)
.withConverter<MessageType>($MessagesTable.$convertertype);
static const VerificationMeta _contentMeta = static const VerificationMeta _contentMeta =
const VerificationMeta('content'); const VerificationMeta('content');
@override @override
@ -2268,7 +2273,7 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> {
type: DriftSqlType.string, type: DriftSqlType.string,
requiredDuringInsert: false, requiredDuringInsert: false,
defaultConstraints: GeneratedColumn.constraintIsAlways( defaultConstraints: GeneratedColumn.constraintIsAlways(
'REFERENCES media_files (media_id)')); 'REFERENCES media_files (media_id) ON DELETE CASCADE'));
static const VerificationMeta _mediaStoredMeta = static const VerificationMeta _mediaStoredMeta =
const VerificationMeta('mediaStored'); const VerificationMeta('mediaStored');
@override @override
@ -2327,6 +2332,7 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> {
groupId, groupId,
messageId, messageId,
senderId, senderId,
type,
content, content,
mediaId, mediaId,
mediaStored, mediaStored,
@ -2415,6 +2421,8 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> {
.read(DriftSqlType.string, data['${effectivePrefix}message_id'])!, .read(DriftSqlType.string, data['${effectivePrefix}message_id'])!,
senderId: attachedDatabase.typeMapping senderId: attachedDatabase.typeMapping
.read(DriftSqlType.int, data['${effectivePrefix}sender_id']), .read(DriftSqlType.int, data['${effectivePrefix}sender_id']),
type: $MessagesTable.$convertertype.fromSql(attachedDatabase.typeMapping
.read(DriftSqlType.string, data['${effectivePrefix}type'])!),
content: attachedDatabase.typeMapping content: attachedDatabase.typeMapping
.read(DriftSqlType.string, data['${effectivePrefix}content']), .read(DriftSqlType.string, data['${effectivePrefix}content']),
mediaId: attachedDatabase.typeMapping mediaId: attachedDatabase.typeMapping
@ -2438,12 +2446,16 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> {
$MessagesTable createAlias(String alias) { $MessagesTable createAlias(String alias) {
return $MessagesTable(attachedDatabase, alias); return $MessagesTable(attachedDatabase, alias);
} }
static JsonTypeConverter2<MessageType, String, String> $convertertype =
const EnumNameConverter<MessageType>(MessageType.values);
} }
class Message extends DataClass implements Insertable<Message> { class Message extends DataClass implements Insertable<Message> {
final String groupId; final String groupId;
final String messageId; final String messageId;
final int? senderId; final int? senderId;
final MessageType type;
final String? content; final String? content;
final String? mediaId; final String? mediaId;
final bool mediaStored; final bool mediaStored;
@ -2456,6 +2468,7 @@ class Message extends DataClass implements Insertable<Message> {
{required this.groupId, {required this.groupId,
required this.messageId, required this.messageId,
this.senderId, this.senderId,
required this.type,
this.content, this.content,
this.mediaId, this.mediaId,
required this.mediaStored, required this.mediaStored,
@ -2472,6 +2485,9 @@ class Message extends DataClass implements Insertable<Message> {
if (!nullToAbsent || senderId != null) { if (!nullToAbsent || senderId != null) {
map['sender_id'] = Variable<int>(senderId); map['sender_id'] = Variable<int>(senderId);
} }
{
map['type'] = Variable<String>($MessagesTable.$convertertype.toSql(type));
}
if (!nullToAbsent || content != null) { if (!nullToAbsent || content != null) {
map['content'] = Variable<String>(content); map['content'] = Variable<String>(content);
} }
@ -2498,6 +2514,7 @@ class Message extends DataClass implements Insertable<Message> {
senderId: senderId == null && nullToAbsent senderId: senderId == null && nullToAbsent
? const Value.absent() ? const Value.absent()
: Value(senderId), : Value(senderId),
type: Value(type),
content: content == null && nullToAbsent content: content == null && nullToAbsent
? const Value.absent() ? const Value.absent()
: Value(content), : Value(content),
@ -2524,6 +2541,8 @@ class Message extends DataClass implements Insertable<Message> {
groupId: serializer.fromJson<String>(json['groupId']), groupId: serializer.fromJson<String>(json['groupId']),
messageId: serializer.fromJson<String>(json['messageId']), messageId: serializer.fromJson<String>(json['messageId']),
senderId: serializer.fromJson<int?>(json['senderId']), senderId: serializer.fromJson<int?>(json['senderId']),
type: $MessagesTable.$convertertype
.fromJson(serializer.fromJson<String>(json['type'])),
content: serializer.fromJson<String?>(json['content']), content: serializer.fromJson<String?>(json['content']),
mediaId: serializer.fromJson<String?>(json['mediaId']), mediaId: serializer.fromJson<String?>(json['mediaId']),
mediaStored: serializer.fromJson<bool>(json['mediaStored']), mediaStored: serializer.fromJson<bool>(json['mediaStored']),
@ -2542,6 +2561,8 @@ class Message extends DataClass implements Insertable<Message> {
'groupId': serializer.toJson<String>(groupId), 'groupId': serializer.toJson<String>(groupId),
'messageId': serializer.toJson<String>(messageId), 'messageId': serializer.toJson<String>(messageId),
'senderId': serializer.toJson<int?>(senderId), 'senderId': serializer.toJson<int?>(senderId),
'type':
serializer.toJson<String>($MessagesTable.$convertertype.toJson(type)),
'content': serializer.toJson<String?>(content), 'content': serializer.toJson<String?>(content),
'mediaId': serializer.toJson<String?>(mediaId), 'mediaId': serializer.toJson<String?>(mediaId),
'mediaStored': serializer.toJson<bool>(mediaStored), 'mediaStored': serializer.toJson<bool>(mediaStored),
@ -2557,6 +2578,7 @@ class Message extends DataClass implements Insertable<Message> {
{String? groupId, {String? groupId,
String? messageId, String? messageId,
Value<int?> senderId = const Value.absent(), Value<int?> senderId = const Value.absent(),
MessageType? type,
Value<String?> content = const Value.absent(), Value<String?> content = const Value.absent(),
Value<String?> mediaId = const Value.absent(), Value<String?> mediaId = const Value.absent(),
bool? mediaStored, bool? mediaStored,
@ -2569,6 +2591,7 @@ class Message extends DataClass implements Insertable<Message> {
groupId: groupId ?? this.groupId, groupId: groupId ?? this.groupId,
messageId: messageId ?? this.messageId, messageId: messageId ?? this.messageId,
senderId: senderId.present ? senderId.value : this.senderId, senderId: senderId.present ? senderId.value : this.senderId,
type: type ?? this.type,
content: content.present ? content.value : this.content, content: content.present ? content.value : this.content,
mediaId: mediaId.present ? mediaId.value : this.mediaId, mediaId: mediaId.present ? mediaId.value : this.mediaId,
mediaStored: mediaStored ?? this.mediaStored, mediaStored: mediaStored ?? this.mediaStored,
@ -2586,6 +2609,7 @@ class Message extends DataClass implements Insertable<Message> {
groupId: data.groupId.present ? data.groupId.value : this.groupId, groupId: data.groupId.present ? data.groupId.value : this.groupId,
messageId: data.messageId.present ? data.messageId.value : this.messageId, messageId: data.messageId.present ? data.messageId.value : this.messageId,
senderId: data.senderId.present ? data.senderId.value : this.senderId, senderId: data.senderId.present ? data.senderId.value : this.senderId,
type: data.type.present ? data.type.value : this.type,
content: data.content.present ? data.content.value : this.content, content: data.content.present ? data.content.value : this.content,
mediaId: data.mediaId.present ? data.mediaId.value : this.mediaId, mediaId: data.mediaId.present ? data.mediaId.value : this.mediaId,
mediaStored: mediaStored:
@ -2610,6 +2634,7 @@ class Message extends DataClass implements Insertable<Message> {
..write('groupId: $groupId, ') ..write('groupId: $groupId, ')
..write('messageId: $messageId, ') ..write('messageId: $messageId, ')
..write('senderId: $senderId, ') ..write('senderId: $senderId, ')
..write('type: $type, ')
..write('content: $content, ') ..write('content: $content, ')
..write('mediaId: $mediaId, ') ..write('mediaId: $mediaId, ')
..write('mediaStored: $mediaStored, ') ..write('mediaStored: $mediaStored, ')
@ -2627,6 +2652,7 @@ class Message extends DataClass implements Insertable<Message> {
groupId, groupId,
messageId, messageId,
senderId, senderId,
type,
content, content,
mediaId, mediaId,
mediaStored, mediaStored,
@ -2642,6 +2668,7 @@ class Message extends DataClass implements Insertable<Message> {
other.groupId == this.groupId && other.groupId == this.groupId &&
other.messageId == this.messageId && other.messageId == this.messageId &&
other.senderId == this.senderId && other.senderId == this.senderId &&
other.type == this.type &&
other.content == this.content && other.content == this.content &&
other.mediaId == this.mediaId && other.mediaId == this.mediaId &&
other.mediaStored == this.mediaStored && other.mediaStored == this.mediaStored &&
@ -2656,6 +2683,7 @@ class MessagesCompanion extends UpdateCompanion<Message> {
final Value<String> groupId; final Value<String> groupId;
final Value<String> messageId; final Value<String> messageId;
final Value<int?> senderId; final Value<int?> senderId;
final Value<MessageType> type;
final Value<String?> content; final Value<String?> content;
final Value<String?> mediaId; final Value<String?> mediaId;
final Value<bool> mediaStored; final Value<bool> mediaStored;
@ -2669,6 +2697,7 @@ class MessagesCompanion extends UpdateCompanion<Message> {
this.groupId = const Value.absent(), this.groupId = const Value.absent(),
this.messageId = const Value.absent(), this.messageId = const Value.absent(),
this.senderId = const Value.absent(), this.senderId = const Value.absent(),
this.type = const Value.absent(),
this.content = const Value.absent(), this.content = const Value.absent(),
this.mediaId = const Value.absent(), this.mediaId = const Value.absent(),
this.mediaStored = const Value.absent(), this.mediaStored = const Value.absent(),
@ -2683,6 +2712,7 @@ class MessagesCompanion extends UpdateCompanion<Message> {
required String groupId, required String groupId,
this.messageId = const Value.absent(), this.messageId = const Value.absent(),
this.senderId = const Value.absent(), this.senderId = const Value.absent(),
required MessageType type,
this.content = const Value.absent(), this.content = const Value.absent(),
this.mediaId = const Value.absent(), this.mediaId = const Value.absent(),
this.mediaStored = const Value.absent(), this.mediaStored = const Value.absent(),
@ -2692,11 +2722,13 @@ class MessagesCompanion extends UpdateCompanion<Message> {
this.isEdited = const Value.absent(), this.isEdited = const Value.absent(),
this.createdAt = const Value.absent(), this.createdAt = const Value.absent(),
this.rowid = const Value.absent(), this.rowid = const Value.absent(),
}) : groupId = Value(groupId); }) : groupId = Value(groupId),
type = Value(type);
static Insertable<Message> custom({ static Insertable<Message> custom({
Expression<String>? groupId, Expression<String>? groupId,
Expression<String>? messageId, Expression<String>? messageId,
Expression<int>? senderId, Expression<int>? senderId,
Expression<String>? type,
Expression<String>? content, Expression<String>? content,
Expression<String>? mediaId, Expression<String>? mediaId,
Expression<bool>? mediaStored, Expression<bool>? mediaStored,
@ -2711,6 +2743,7 @@ class MessagesCompanion extends UpdateCompanion<Message> {
if (groupId != null) 'group_id': groupId, if (groupId != null) 'group_id': groupId,
if (messageId != null) 'message_id': messageId, if (messageId != null) 'message_id': messageId,
if (senderId != null) 'sender_id': senderId, if (senderId != null) 'sender_id': senderId,
if (type != null) 'type': type,
if (content != null) 'content': content, if (content != null) 'content': content,
if (mediaId != null) 'media_id': mediaId, if (mediaId != null) 'media_id': mediaId,
if (mediaStored != null) 'media_stored': mediaStored, if (mediaStored != null) 'media_stored': mediaStored,
@ -2728,6 +2761,7 @@ class MessagesCompanion extends UpdateCompanion<Message> {
{Value<String>? groupId, {Value<String>? groupId,
Value<String>? messageId, Value<String>? messageId,
Value<int?>? senderId, Value<int?>? senderId,
Value<MessageType>? type,
Value<String?>? content, Value<String?>? content,
Value<String?>? mediaId, Value<String?>? mediaId,
Value<bool>? mediaStored, Value<bool>? mediaStored,
@ -2741,6 +2775,7 @@ class MessagesCompanion extends UpdateCompanion<Message> {
groupId: groupId ?? this.groupId, groupId: groupId ?? this.groupId,
messageId: messageId ?? this.messageId, messageId: messageId ?? this.messageId,
senderId: senderId ?? this.senderId, senderId: senderId ?? this.senderId,
type: type ?? this.type,
content: content ?? this.content, content: content ?? this.content,
mediaId: mediaId ?? this.mediaId, mediaId: mediaId ?? this.mediaId,
mediaStored: mediaStored ?? this.mediaStored, mediaStored: mediaStored ?? this.mediaStored,
@ -2765,6 +2800,10 @@ class MessagesCompanion extends UpdateCompanion<Message> {
if (senderId.present) { if (senderId.present) {
map['sender_id'] = Variable<int>(senderId.value); map['sender_id'] = Variable<int>(senderId.value);
} }
if (type.present) {
map['type'] =
Variable<String>($MessagesTable.$convertertype.toSql(type.value));
}
if (content.present) { if (content.present) {
map['content'] = Variable<String>(content.value); map['content'] = Variable<String>(content.value);
} }
@ -2801,6 +2840,7 @@ class MessagesCompanion extends UpdateCompanion<Message> {
..write('groupId: $groupId, ') ..write('groupId: $groupId, ')
..write('messageId: $messageId, ') ..write('messageId: $messageId, ')
..write('senderId: $senderId, ') ..write('senderId: $senderId, ')
..write('type: $type, ')
..write('content: $content, ') ..write('content: $content, ')
..write('mediaId: $mediaId, ') ..write('mediaId: $mediaId, ')
..write('mediaStored: $mediaStored, ') ..write('mediaStored: $mediaStored, ')
@ -6149,6 +6189,13 @@ abstract class _$TwonlyDB extends GeneratedDatabase {
TableUpdate('messages', kind: UpdateKind.delete), TableUpdate('messages', kind: UpdateKind.delete),
], ],
), ),
WritePropagation(
on: TableUpdateQuery.onTableName('media_files',
limitUpdateKind: UpdateKind.delete),
result: [
TableUpdate('messages', kind: UpdateKind.delete),
],
),
WritePropagation( WritePropagation(
on: TableUpdateQuery.onTableName('messages', on: TableUpdateQuery.onTableName('messages',
limitUpdateKind: UpdateKind.delete), limitUpdateKind: UpdateKind.delete),
@ -7817,6 +7864,7 @@ typedef $$MessagesTableCreateCompanionBuilder = MessagesCompanion Function({
required String groupId, required String groupId,
Value<String> messageId, Value<String> messageId,
Value<int?> senderId, Value<int?> senderId,
required MessageType type,
Value<String?> content, Value<String?> content,
Value<String?> mediaId, Value<String?> mediaId,
Value<bool> mediaStored, Value<bool> mediaStored,
@ -7831,6 +7879,7 @@ typedef $$MessagesTableUpdateCompanionBuilder = MessagesCompanion Function({
Value<String> groupId, Value<String> groupId,
Value<String> messageId, Value<String> messageId,
Value<int?> senderId, Value<int?> senderId,
Value<MessageType> type,
Value<String?> content, Value<String?> content,
Value<String?> mediaId, Value<String?> mediaId,
Value<bool> mediaStored, Value<bool> mediaStored,
@ -7984,6 +8033,11 @@ class $$MessagesTableFilterComposer
ColumnFilters<String> get messageId => $composableBuilder( ColumnFilters<String> get messageId => $composableBuilder(
column: $table.messageId, builder: (column) => ColumnFilters(column)); column: $table.messageId, builder: (column) => ColumnFilters(column));
ColumnWithTypeConverterFilters<MessageType, MessageType, String> get type =>
$composableBuilder(
column: $table.type,
builder: (column) => ColumnWithTypeConverterFilters(column));
ColumnFilters<String> get content => $composableBuilder( ColumnFilters<String> get content => $composableBuilder(
column: $table.content, builder: (column) => ColumnFilters(column)); column: $table.content, builder: (column) => ColumnFilters(column));
@ -8180,6 +8234,9 @@ class $$MessagesTableOrderingComposer
ColumnOrderings<String> get messageId => $composableBuilder( ColumnOrderings<String> get messageId => $composableBuilder(
column: $table.messageId, builder: (column) => ColumnOrderings(column)); column: $table.messageId, builder: (column) => ColumnOrderings(column));
ColumnOrderings<String> get type => $composableBuilder(
column: $table.type, builder: (column) => ColumnOrderings(column));
ColumnOrderings<String> get content => $composableBuilder( ColumnOrderings<String> get content => $composableBuilder(
column: $table.content, builder: (column) => ColumnOrderings(column)); column: $table.content, builder: (column) => ColumnOrderings(column));
@ -8293,6 +8350,9 @@ class $$MessagesTableAnnotationComposer
GeneratedColumn<String> get messageId => GeneratedColumn<String> get messageId =>
$composableBuilder(column: $table.messageId, builder: (column) => column); $composableBuilder(column: $table.messageId, builder: (column) => column);
GeneratedColumnWithTypeConverter<MessageType, String> get type =>
$composableBuilder(column: $table.type, builder: (column) => column);
GeneratedColumn<String> get content => GeneratedColumn<String> get content =>
$composableBuilder(column: $table.content, builder: (column) => column); $composableBuilder(column: $table.content, builder: (column) => column);
@ -8510,6 +8570,7 @@ class $$MessagesTableTableManager extends RootTableManager<
Value<String> groupId = const Value.absent(), Value<String> groupId = const Value.absent(),
Value<String> messageId = const Value.absent(), Value<String> messageId = const Value.absent(),
Value<int?> senderId = const Value.absent(), Value<int?> senderId = const Value.absent(),
Value<MessageType> type = const Value.absent(),
Value<String?> content = const Value.absent(), Value<String?> content = const Value.absent(),
Value<String?> mediaId = const Value.absent(), Value<String?> mediaId = const Value.absent(),
Value<bool> mediaStored = const Value.absent(), Value<bool> mediaStored = const Value.absent(),
@ -8524,6 +8585,7 @@ class $$MessagesTableTableManager extends RootTableManager<
groupId: groupId, groupId: groupId,
messageId: messageId, messageId: messageId,
senderId: senderId, senderId: senderId,
type: type,
content: content, content: content,
mediaId: mediaId, mediaId: mediaId,
mediaStored: mediaStored, mediaStored: mediaStored,
@ -8538,6 +8600,7 @@ class $$MessagesTableTableManager extends RootTableManager<
required String groupId, required String groupId,
Value<String> messageId = const Value.absent(), Value<String> messageId = const Value.absent(),
Value<int?> senderId = const Value.absent(), Value<int?> senderId = const Value.absent(),
required MessageType type,
Value<String?> content = const Value.absent(), Value<String?> content = const Value.absent(),
Value<String?> mediaId = const Value.absent(), Value<String?> mediaId = const Value.absent(),
Value<bool> mediaStored = const Value.absent(), Value<bool> mediaStored = const Value.absent(),
@ -8552,6 +8615,7 @@ class $$MessagesTableTableManager extends RootTableManager<
groupId: groupId, groupId: groupId,
messageId: messageId, messageId: messageId,
senderId: senderId, senderId: senderId,
type: type,
content: content, content: content,
mediaId: mediaId, mediaId: mediaId,
mediaStored: mediaStored, mediaStored: mediaStored,

View file

@ -1,88 +1,30 @@
import 'dart:convert';
import 'dart:io';
import 'package:drift/drift.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/json/message_old.dart'; import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
import 'package:twonly/src/services/api/mediafiles/upload.service.dart' as send;
import 'package:twonly/src/services/mediafiles/thumbnail.service.dart';
class MemoryItem { class MemoryItem {
MemoryItem({ MemoryItem({
required this.id, required this.mediaService,
required this.messages, required this.messages,
required this.date,
required this.mirrorVideo,
required this.thumbnailPath,
this.imagePath,
this.videoPath,
}); });
final int id;
final bool mirrorVideo;
final List<Message> messages; final List<Message> messages;
final DateTime date; final MediaFileService mediaService;
final File thumbnailPath;
final File? imagePath;
final File? videoPath;
static Future<Map<int, MemoryItem>> convertFromMessages( static Future<Map<String, MemoryItem>> convertFromMessages(
List<Message> messages, List<Message> messages,
) async { ) async {
final items = <int, MemoryItem>{}; final items = <String, MemoryItem>{};
for (final message in messages) { for (final message in messages) {
final isSend = message.messageOtherId == null; if (message.mediaId == null) continue;
final id = message.mediaUploadId ?? message.messageId;
final basePath = await send.getMediaFilePath( final mediaService = await MediaFileService.fromMediaId(message.mediaId!);
id, if (mediaService == null) continue;
isSend ? 'send' : 'received',
);
File? imagePath;
late File thumbnailFile;
File? videoPath;
if (File('$basePath.mp4').existsSync()) {
videoPath = File('$basePath.mp4');
thumbnailFile = getThumbnailPath(videoPath);
if (!thumbnailFile.existsSync()) {
await createThumbnailsForVideo(videoPath);
}
} else if (File('$basePath.png').existsSync()) {
imagePath = File('$basePath.png');
thumbnailFile = getThumbnailPath(imagePath);
if (!thumbnailFile.existsSync()) {
await createThumbnailsForImage(imagePath);
}
} else {
if (message.mediaStored) {
/// media file was deleted, ... remove the file
await twonlyDB.messagesDao.updateMessageByMessageId(
message.messageId,
const MessagesCompanion(
mediaStored: Value(false),
),
);
}
continue;
}
var mirrorVideo = false;
if (videoPath != null) {
final content = MediaMessageContent.fromJson(
jsonDecode(message.contentJson!) as Map,
);
mirrorVideo = content.mirrorVideo;
}
items items
.putIfAbsent( .putIfAbsent(
id, message.mediaId!,
() => MemoryItem( () => MemoryItem(
id: id, mediaService: mediaService,
messages: [], messages: [],
date: message.sendAt,
mirrorVideo: mirrorVideo,
thumbnailPath: thumbnailFile,
imagePath: imagePath,
videoPath: videoPath,
), ),
) )
.messages .messages

View file

@ -6,6 +6,7 @@ import 'package:flutter_image_compress/flutter_image_compress.dart';
import 'package:gal/gal.dart'; import 'package:gal/gal.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:local_auth/local_auth.dart'; import 'package:local_auth/local_auth.dart';
import 'package:pie_menu/pie_menu.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/localization/generated/app_localizations.dart'; import 'package:twonly/src/localization/generated/app_localizations.dart';
@ -258,3 +259,28 @@ Uint8List hexToUint8List(String hex) => Uint8List.fromList(
(i) => int.parse(hex.substring(i * 2, i * 2 + 2), radix: 16), (i) => int.parse(hex.substring(i * 2, i * 2 + 2), radix: 16),
), ),
); );
PieTheme getPieCanvasTheme(BuildContext context) {
return PieTheme(
brightness: Theme.of(context).brightness,
rightClickShowsMenu: true,
radius: 70,
buttonTheme: PieButtonTheme(
backgroundColor: Theme.of(context).colorScheme.tertiary,
iconColor: Theme.of(context).colorScheme.surfaceBright,
),
buttonThemeHovered: PieButtonTheme(
backgroundColor: Theme.of(context).colorScheme.primary,
iconColor: Theme.of(context).colorScheme.surfaceBright,
),
tooltipPadding: const EdgeInsets.all(20),
overlayColor: isDarkMode(context)
? const Color.fromARGB(69, 0, 0, 0)
: const Color.fromARGB(40, 0, 0, 0),
// spacing: 0,
tooltipTextStyle: const TextStyle(
fontSize: 32,
fontWeight: FontWeight.w600,
),
);
}

View file

@ -71,8 +71,14 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
selectedGroupIds.add(widget.sendToGroup!.groupId); selectedGroupIds.add(widget.sendToGroup!.groupId);
} }
if (widget.mediaFileService.mediaFile.type == MediaType.image) {
if (widget.imageBytesFuture != null) { if (widget.imageBytesFuture != null) {
unawaited(loadImage(widget.imageBytesFuture!)); loadImage(widget.imageBytesFuture!);
} else {
if (widget.mediaFileService.storedPath.existsSync()) {
loadImage(widget.mediaFileService.storedPath.readAsBytes());
}
}
} }
if (media.type == MediaType.video) { if (media.type == MediaType.video) {

View file

@ -8,7 +8,6 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/database/daos/contacts.dao.dart'; import 'package:twonly/src/database/daos/contacts.dao.dart';
import 'package:twonly/src/database/tables/messages_table.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/json/userdata.dart'; import 'package:twonly/src/model/json/userdata.dart';
import 'package:twonly/src/providers/connection.provider.dart'; import 'package:twonly/src/providers/connection.provider.dart';
@ -28,7 +27,7 @@ import 'package:twonly/src/views/chats/start_new_chat.view.dart';
import 'package:twonly/src/views/components/flame.dart'; import 'package:twonly/src/views/components/flame.dart';
import 'package:twonly/src/views/components/initialsavatar.dart'; import 'package:twonly/src/views/components/initialsavatar.dart';
import 'package:twonly/src/views/components/notification_badge.dart'; import 'package:twonly/src/views/components/notification_badge.dart';
import 'package:twonly/src/views/components/user_context_menu.dart'; import 'package:twonly/src/views/components/user_context_menu.component.dart';
import 'package:twonly/src/views/settings/help/changelog.view.dart'; import 'package:twonly/src/views/settings/help/changelog.view.dart';
import 'package:twonly/src/views/settings/profile/profile.view.dart'; import 'package:twonly/src/views/settings/profile/profile.view.dart';
import 'package:twonly/src/views/settings/settings_main.view.dart'; import 'package:twonly/src/views/settings/settings_main.view.dart';

View file

@ -23,7 +23,7 @@ import 'package:twonly/src/views/chats/chat_messages_components/chat_list_entry.
import 'package:twonly/src/views/chats/chat_messages_components/response_container.dart'; import 'package:twonly/src/views/chats/chat_messages_components/response_container.dart';
import 'package:twonly/src/views/components/animate_icon.dart'; import 'package:twonly/src/views/components/animate_icon.dart';
import 'package:twonly/src/views/components/initialsavatar.dart'; import 'package:twonly/src/views/components/initialsavatar.dart';
import 'package:twonly/src/views/components/user_context_menu.dart'; import 'package:twonly/src/views/components/user_context_menu.component.dart';
import 'package:twonly/src/views/components/verified_shield.dart'; import 'package:twonly/src/views/components/verified_shield.dart';
import 'package:twonly/src/views/contact/contact.view.dart'; import 'package:twonly/src/views/contact/contact.view.dart';
import 'package:twonly/src/views/tutorial/tutorials.dart'; import 'package:twonly/src/views/tutorial/tutorials.dart';
@ -61,9 +61,9 @@ class ChatItem {
/// Displays detailed information about a SampleItem. /// Displays detailed information about a SampleItem.
class ChatMessagesView extends StatefulWidget { class ChatMessagesView extends StatefulWidget {
const ChatMessagesView(this.contact, {super.key}); const ChatMessagesView(this.group, {super.key});
final Contact contact; final Group group;
@override @override
State<ChatMessagesView> createState() => _ChatMessagesViewState(); State<ChatMessagesView> createState() => _ChatMessagesViewState();

View file

@ -12,7 +12,7 @@ import 'package:twonly/src/views/chats/add_new_user.view.dart';
import 'package:twonly/src/views/chats/chat_messages.view.dart'; import 'package:twonly/src/views/chats/chat_messages.view.dart';
import 'package:twonly/src/views/components/flame.dart'; import 'package:twonly/src/views/components/flame.dart';
import 'package:twonly/src/views/components/initialsavatar.dart'; import 'package:twonly/src/views/components/initialsavatar.dart';
import 'package:twonly/src/views/components/user_context_menu.dart'; import 'package:twonly/src/views/components/user_context_menu.component.dart';
class StartNewChatView extends StatefulWidget { class StartNewChatView extends StatefulWidget {
const StartNewChatView({super.key}); const StartNewChatView({super.key});

View file

@ -0,0 +1,96 @@
import 'package:drift/drift.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:pie_menu/pie_menu.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/chats/chat_messages.view.dart';
class GroupContextMenu extends StatefulWidget {
const GroupContextMenu({
required this.group,
required this.child,
super.key,
});
final Widget child;
final Group group;
@override
State<GroupContextMenu> createState() => _GroupContextMenuState();
}
class _GroupContextMenuState extends State<GroupContextMenu> {
@override
Widget build(BuildContext context) {
return PieMenu(
onPressed: () => (),
onToggle: (menuOpen) async {
if (menuOpen) {
await HapticFeedback.heavyImpact();
}
},
actions: [
if (!widget.group.archived)
PieAction(
tooltip: Text(context.lang.contextMenuArchiveUser),
onSelect: () async {
const update = GroupsCompanion(archived: Value(true));
if (context.mounted) {
await twonlyDB.groupsDao
.updateGroup(widget.group.groupId, update);
}
},
child: const FaIcon(FontAwesomeIcons.boxArchive),
),
if (widget.group.archived)
PieAction(
tooltip: Text(context.lang.contextMenuUndoArchiveUser),
onSelect: () async {
const update = GroupsCompanion(archived: Value(false));
if (context.mounted) {
await twonlyDB.groupsDao
.updateGroup(widget.group.groupId, update);
}
},
child: const FaIcon(FontAwesomeIcons.boxOpen),
),
PieAction(
tooltip: Text(context.lang.contextMenuOpenChat),
onSelect: () async {
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
return ChatMessagesView(widget.group);
},
),
);
},
child: const FaIcon(FontAwesomeIcons.solidComments),
),
PieAction(
tooltip: Text(
widget.group.pinned
? context.lang.contextMenuUnpin
: context.lang.contextMenuPin,
),
onSelect: () async {
final update = GroupsCompanion(pinned: Value(!widget.group.pinned));
if (context.mounted) {
await twonlyDB.groupsDao
.updateGroup(widget.group.groupId, update);
}
},
child: FaIcon(
widget.group.pinned
? FontAwesomeIcons.thumbtackSlash
: FontAwesomeIcons.thumbtack,
),
),
],
child: widget.child,
);
}
}

View file

@ -0,0 +1,45 @@
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:pie_menu/pie_menu.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/contact/contact.view.dart';
class UserContextMenuBlocked extends StatefulWidget {
const UserContextMenuBlocked({
required this.contact,
required this.child,
super.key,
});
final Widget child;
final Contact contact;
@override
State<UserContextMenuBlocked> createState() => _UserContextMenuBlocked();
}
class _UserContextMenuBlocked extends State<UserContextMenuBlocked> {
@override
Widget build(BuildContext context) {
return PieMenu(
onPressed: () => (),
actions: [
PieAction(
tooltip: Text(context.lang.contextMenuUserProfile),
onSelect: () async {
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
return ContactView(widget.contact.userId);
},
),
);
},
child: const FaIcon(FontAwesomeIcons.user),
),
],
child: widget.child,
);
}
}

View file

@ -1,186 +0,0 @@
import 'package:drift/drift.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:pie_menu/pie_menu.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/chats/chat_messages.view.dart';
import 'package:twonly/src/views/contact/contact.view.dart';
class UserContextMenu extends StatefulWidget {
const UserContextMenu({
required this.contact,
required this.child,
super.key,
});
final Widget child;
final Contact contact;
@override
State<UserContextMenu> createState() => _UserContextMenuState();
}
class _UserContextMenuState extends State<UserContextMenu> {
@override
Widget build(BuildContext context) {
return PieMenu(
onPressed: () => (),
onToggle: (menuOpen) async {
if (menuOpen) {
await HapticFeedback.heavyImpact();
}
},
actions: [
if (!widget.contact.archived)
PieAction(
tooltip: Text(context.lang.contextMenuArchiveUser),
onSelect: () async {
const update = ContactsCompanion(archived: Value(true));
if (context.mounted) {
await twonlyDB.contactsDao
.updateContact(widget.contact.userId, update);
}
},
child: const FaIcon(FontAwesomeIcons.boxArchive),
),
if (widget.contact.archived)
PieAction(
tooltip: Text(context.lang.contextMenuUndoArchiveUser),
onSelect: () async {
const update = ContactsCompanion(archived: Value(false));
if (context.mounted) {
await twonlyDB.contactsDao
.updateContact(widget.contact.userId, update);
}
},
child: const FaIcon(FontAwesomeIcons.boxOpen),
),
PieAction(
tooltip: Text(context.lang.contextMenuOpenChat),
onSelect: () async {
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
return ChatMessagesView(widget.contact);
},
),
);
},
child: const FaIcon(FontAwesomeIcons.solidComments),
),
PieAction(
tooltip: Text(
widget.contact.pinned
? context.lang.contextMenuUnpin
: context.lang.contextMenuPin,
),
onSelect: () async {
final update =
ContactsCompanion(pinned: Value(!widget.contact.pinned));
if (context.mounted) {
await twonlyDB.contactsDao
.updateContact(widget.contact.userId, update);
}
},
child: FaIcon(
widget.contact.pinned
? FontAwesomeIcons.thumbtackSlash
: FontAwesomeIcons.thumbtack,
),
),
],
child: widget.child,
);
}
}
class UserContextMenuBlocked extends StatefulWidget {
const UserContextMenuBlocked({
required this.contact,
required this.child,
super.key,
});
final Widget child;
final Contact contact;
@override
State<UserContextMenuBlocked> createState() => _UserContextMenuBlocked();
}
class _UserContextMenuBlocked extends State<UserContextMenuBlocked> {
@override
Widget build(BuildContext context) {
return PieMenu(
onPressed: () => (),
actions: [
if (!widget.contact.archived)
PieAction(
tooltip: Text(context.lang.contextMenuArchiveUser),
onSelect: () async {
const update = ContactsCompanion(archived: Value(true));
if (context.mounted) {
await twonlyDB.contactsDao
.updateContact(widget.contact.userId, update);
}
},
child: const FaIcon(FontAwesomeIcons.boxArchive),
),
if (widget.contact.archived)
PieAction(
tooltip: Text(context.lang.contextMenuUndoArchiveUser),
onSelect: () async {
const update = ContactsCompanion(archived: Value(false));
if (context.mounted) {
await twonlyDB.contactsDao
.updateContact(widget.contact.userId, update);
}
},
child: const FaIcon(FontAwesomeIcons.boxOpen),
),
PieAction(
tooltip: Text(context.lang.contextMenuUserProfile),
onSelect: () async {
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
return ContactView(widget.contact.userId);
},
),
);
},
child: const FaIcon(FontAwesomeIcons.user),
),
],
child: widget.child,
);
}
}
PieTheme getPieCanvasTheme(BuildContext context) {
return PieTheme(
brightness: Theme.of(context).brightness,
rightClickShowsMenu: true,
radius: 70,
buttonTheme: PieButtonTheme(
backgroundColor: Theme.of(context).colorScheme.tertiary,
iconColor: Theme.of(context).colorScheme.surfaceBright,
),
buttonThemeHovered: PieButtonTheme(
backgroundColor: Theme.of(context).colorScheme.primary,
iconColor: Theme.of(context).colorScheme.surfaceBright,
),
tooltipPadding: const EdgeInsets.all(20),
overlayColor: isDarkMode(context)
? const Color.fromARGB(69, 0, 0, 0)
: const Color.fromARGB(40, 0, 0, 0),
// spacing: 0,
tooltipTextStyle: const TextStyle(
fontSize: 32,
fontWeight: FontWeight.w600,
),
);
}

View file

@ -7,11 +7,9 @@ import 'package:video_player/video_player.dart';
class VideoPlayerWrapper extends StatefulWidget { class VideoPlayerWrapper extends StatefulWidget {
const VideoPlayerWrapper({ const VideoPlayerWrapper({
required this.videoPath, required this.videoPath,
required this.mirrorVideo,
super.key, super.key,
}); });
final File videoPath; final File videoPath;
final bool mirrorVideo;
@override @override
State<VideoPlayerWrapper> createState() => _VideoPlayerWrapperState(); State<VideoPlayerWrapper> createState() => _VideoPlayerWrapperState();
@ -48,10 +46,7 @@ class _VideoPlayerWrapperState extends State<VideoPlayerWrapper> {
child: _controller.value.isInitialized child: _controller.value.isInitialized
? AspectRatio( ? AspectRatio(
aspectRatio: _controller.value.aspectRatio, aspectRatio: _controller.value.aspectRatio,
child: Transform.flip(
flipX: widget.mirrorVideo,
child: VideoPlayer(_controller), child: VideoPlayer(_controller),
),
) )
: const CircularProgressIndicator(), // Show loading indicator while initializing : const CircularProgressIndicator(), // Show loading indicator while initializing
); );

View file

@ -163,26 +163,26 @@ class _ContactViewState extends State<ContactView> {
); );
}, },
), ),
BetterListTile( // BetterListTile(
icon: FontAwesomeIcons.eraser, // icon: FontAwesomeIcons.eraser,
iconSize: 16, // iconSize: 16,
text: context.lang.deleteAllContactMessages, // text: context.lang.deleteAllContactMessages,
onTap: () async { // onTap: () async {
final block = await showAlertDialog( // final block = await showAlertDialog(
context, // context,
context.lang.deleteAllContactMessages, // context.lang.deleteAllContactMessages,
context.lang.deleteAllContactMessagesBody( // context.lang.deleteAllContactMessagesBody(
getContactDisplayName(contact), // getContactDisplayName(contact),
), // ),
); // );
if (block) { // if (block) {
if (context.mounted) { // if (context.mounted) {
await twonlyDB.messagesDao // await twonlyDB.messagesDao
.deleteMessagesByContactId(contact.userId); // .deleteMessagesByContactId(contact.userId);
} // }
} // }
}, // },
), // ),
BetterListTile( BetterListTile(
icon: FontAwesomeIcons.flag, icon: FontAwesomeIcons.flag,
text: context.lang.reportUser, text: context.lang.reportUser,

View file

@ -10,7 +10,6 @@ import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/camera/camera_preview_components/camera_preview.dart'; import 'package:twonly/src/views/camera/camera_preview_components/camera_preview.dart';
import 'package:twonly/src/views/camera/camera_preview_controller_view.dart'; import 'package:twonly/src/views/camera/camera_preview_controller_view.dart';
import 'package:twonly/src/views/chats/chat_list.view.dart'; import 'package:twonly/src/views/chats/chat_list.view.dart';
import 'package:twonly/src/views/components/user_context_menu.dart';
import 'package:twonly/src/views/memories/memories.view.dart'; import 'package:twonly/src/views/memories/memories.view.dart';
void Function(int) globalUpdateOfHomeViewPageIndex = (a) {}; void Function(int) globalUpdateOfHomeViewPageIndex = (a) {};

View file

@ -1,13 +1,11 @@
import 'dart:async'; import 'dart:async';
import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:path_provider/path_provider.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/memory_item.model.dart'; import 'package:twonly/src/model/memory_item.model.dart';
import 'package:twonly/src/services/api/mediafiles/upload.service.dart' as send; import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
import 'package:twonly/src/services/mediafiles/thumbnail.service.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/memories/memories_item_thumbnail.dart'; import 'package:twonly/src/views/memories/memories_item_thumbnail.dart';
import 'package:twonly/src/views/memories/memories_photo_slider.view.dart'; import 'package:twonly/src/views/memories/memories_photo_slider.view.dart';
@ -24,7 +22,7 @@ class MemoriesViewState extends State<MemoriesView> {
List<MemoryItem> galleryItems = []; List<MemoryItem> galleryItems = [];
Map<String, List<int>> orderedByMonth = {}; Map<String, List<int>> orderedByMonth = {};
List<String> months = []; List<String> months = [];
StreamSubscription<List<Message>>? messageSub; StreamSubscription<List<MediaFile>>? messageSub;
@override @override
void initState() { void initState() {
@ -38,76 +36,37 @@ class MemoriesViewState extends State<MemoriesView> {
super.dispose(); super.dispose();
} }
Future<List<MemoryItem>> loadMemoriesDirectory() async {
final directoryPath = await send.getMediaBaseFilePath('memories');
final directory = Directory(directoryPath);
final items = <MemoryItem>[];
if (directory.existsSync()) {
final files = directory.listSync();
for (final file in files) {
if (file is File) {
final fileName = file.uri.pathSegments.last;
File? imagePath;
File? videoPath;
late File thumbnailFile;
if (fileName.contains('.thumbnail.')) {
continue;
}
if (fileName.contains('.png')) {
imagePath = file;
thumbnailFile = file;
// if (!await thumbnailFile.exists()) {
// await createThumbnailsForImage(imagePath);
// }
} else if (fileName.contains('.mp4')) {
videoPath = file;
thumbnailFile = getThumbnailPath(videoPath);
if (!thumbnailFile.existsSync()) {
await createThumbnailsForVideo(videoPath);
}
} else {
break;
}
final creationDate = file.lastModifiedSync();
items.add(
MemoryItem(
id: int.parse(fileName.split('.')[0]),
messages: [],
date: creationDate,
mirrorVideo: false,
thumbnailPath: thumbnailFile,
imagePath: imagePath,
videoPath: videoPath,
),
);
}
}
}
return items;
}
Future<void> initAsync() async { Future<void> initAsync() async {
await messageSub?.cancel(); await messageSub?.cancel();
final msgStream = twonlyDB.messagesDao.getAllStoredMediaFiles(); final msgStream = twonlyDB.mediaFilesDao.watchAllStoredMediaFiles();
messageSub = msgStream.listen((msgs) async { messageSub = msgStream.listen((mediaFiles) async {
final items = await MemoryItem.convertFromMessages(msgs);
// Group items by month // Group items by month
orderedByMonth = {}; orderedByMonth = {};
months = []; months = [];
var lastMonth = ''; var lastMonth = '';
galleryItems = await loadMemoriesDirectory(); galleryItems = [];
for (final item in galleryItems) { final applicationSupportDirectory =
items.remove( await getApplicationSupportDirectory();
item.id, for (final mediaFile in mediaFiles) {
); // prefer the stored one and not the saved on in the chat.... galleryItems.add(
MemoryItem(
mediaService: MediaFileService(
mediaFile,
applicationSupportDirectory: applicationSupportDirectory,
),
messages: [],
),
);
} }
galleryItems += items.values.toList(); galleryItems.sort(
galleryItems.sort((a, b) => b.date.compareTo(a.date)); (a, b) => b.mediaService.mediaFile.createdAt.compareTo(
a.mediaService.mediaFile.createdAt,
),
);
for (var i = 0; i < galleryItems.length; i++) { for (var i = 0; i < galleryItems.length; i++) {
final month = DateFormat('MMMM yyyy').format(galleryItems[i].date); final month = DateFormat('MMMM yyyy')
.format(galleryItems[i].mediaService.mediaFile.createdAt);
if (lastMonth != month) { if (lastMonth != month) {
lastMonth = month; lastMonth = month;
months.add(month); months.add(month);

View file

@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:twonly/src/database/tables/mediafiles.table.dart';
import 'package:twonly/src/model/memory_item.model.dart'; import 'package:twonly/src/model/memory_item.model.dart';
class MemoriesItemThumbnail extends StatefulWidget { class MemoriesItemThumbnail extends StatefulWidget {
@ -39,11 +40,12 @@ class _MemoriesItemThumbnailState extends State<MemoriesItemThumbnail> {
return GestureDetector( return GestureDetector(
onTap: widget.onTap, onTap: widget.onTap,
child: Hero( child: Hero(
tag: widget.galleryItem.id.toString(), tag: widget.galleryItem.mediaService.mediaFile.mediaId,
child: Stack( child: Stack(
children: [ children: [
Image.file(widget.galleryItem.thumbnailPath), Image.file(widget.galleryItem.mediaService.thumbnailPath),
if (widget.galleryItem.videoPath != null) if (widget.galleryItem.mediaService.mediaFile.type ==
MediaType.video)
const Positioned.fill( const Positioned.fill(
child: Center( child: Center(
child: FaIcon(FontAwesomeIcons.circlePlay), child: FaIcon(FontAwesomeIcons.circlePlay),

View file

@ -1,14 +1,10 @@
import 'package:drift/drift.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:photo_view/photo_view.dart'; import 'package:photo_view/photo_view.dart';
import 'package:photo_view/photo_view_gallery.dart'; import 'package:photo_view/photo_view_gallery.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/tables/mediafiles.table.dart';
import 'package:twonly/src/model/memory_item.model.dart'; import 'package:twonly/src/model/memory_item.model.dart';
import 'package:twonly/src/services/api/mediafiles/download.service.dart'
as received;
import 'package:twonly/src/services/api/mediafiles/upload.service.dart' as send;
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/camera/share_image_editor_view.dart'; import 'package:twonly/src/views/camera/share_image_editor_view.dart';
import 'package:twonly/src/views/components/alert_dialog.dart'; import 'package:twonly/src/views/components/alert_dialog.dart';
@ -51,7 +47,6 @@ class _MemoriesPhotoSliderViewState extends State<MemoriesPhotoSliderView> {
} }
Future<void> deleteFile() async { Future<void> deleteFile() async {
final messages = widget.galleryItems[currentIndex].messages;
final confirmed = await showAlertDialog( final confirmed = await showAlertDialog(
context, context,
context.lang.deleteImageTitle, context.lang.deleteImageTitle,
@ -60,32 +55,26 @@ class _MemoriesPhotoSliderViewState extends State<MemoriesPhotoSliderView> {
if (!confirmed) return; if (!confirmed) return;
widget.galleryItems[currentIndex].imagePath?.deleteSync(); widget.galleryItems[currentIndex].mediaService.fullMediaRemoval();
widget.galleryItems[currentIndex].videoPath?.deleteSync(); await twonlyDB.mediaFilesDao.deleteMediaFile(
for (final message in messages) { widget.galleryItems[currentIndex].mediaService.mediaFile.mediaId,
await twonlyDB.messagesDao.updateMessageByMessageId(
message.messageId,
const MessagesCompanion(mediaStored: Value(false)),
); );
}
widget.galleryItems.removeAt(currentIndex); widget.galleryItems.removeAt(currentIndex);
setState(() {}); setState(() {});
await send.purgeSendMediaFiles();
await received.purgeReceivedMediaFiles();
if (mounted) { if (mounted) {
Navigator.pop(context, true); Navigator.pop(context, true);
} }
} }
Future<void> exportFile() async { Future<void> exportFile() async {
final item = widget.galleryItems[currentIndex]; final item = widget.galleryItems[currentIndex].mediaService;
try { try {
if (item.videoPath != null) { if (item.mediaFile.type == MediaType.video) {
await saveVideoToGallery(item.videoPath!.path); await saveVideoToGallery(item.storedPath.path);
} else if (item.imagePath != null) { } else if (item.mediaFile.type == MediaType.image) {
final imageBytes = await item.imagePath!.readAsBytes(); final imageBytes = await item.storedPath.readAsBytes();
await saveImageToGallery(imageBytes); await saveImageToGallery(imageBytes);
} }
if (!mounted) return; if (!mounted) return;
@ -132,14 +121,9 @@ class _MemoriesPhotoSliderViewState extends State<MemoriesPhotoSliderView> {
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => ShareImageEditorView( builder: (context) => ShareImageEditorView(
videoFilePath: mediaFileService: widget
widget.galleryItems[currentIndex].videoPath, .galleryItems[currentIndex].mediaService,
imageBytes: widget
.galleryItems[currentIndex].imagePath
?.readAsBytes(),
mirrorVideo: false,
sharedFromGallery: true, sharedFromGallery: true,
useHighQuality: true,
), ),
), ),
); );
@ -214,24 +198,25 @@ class _MemoriesPhotoSliderViewState extends State<MemoriesPhotoSliderView> {
PhotoViewGalleryPageOptions _buildItem(BuildContext context, int index) { PhotoViewGalleryPageOptions _buildItem(BuildContext context, int index) {
final item = widget.galleryItems[index]; final item = widget.galleryItems[index];
return item.videoPath != null return item.mediaService.mediaFile.type == MediaType.video
? PhotoViewGalleryPageOptions.customChild( ? PhotoViewGalleryPageOptions.customChild(
child: VideoPlayerWrapper( child: VideoPlayerWrapper(
videoPath: item.videoPath!, videoPath: item.mediaService.storedPath,
mirrorVideo: item.mirrorVideo,
), ),
// childSize: const Size(300, 300), // childSize: const Size(300, 300),
initialScale: PhotoViewComputedScale.contained, initialScale: PhotoViewComputedScale.contained,
minScale: PhotoViewComputedScale.contained, minScale: PhotoViewComputedScale.contained,
maxScale: PhotoViewComputedScale.covered * 4.1, maxScale: PhotoViewComputedScale.covered * 4.1,
heroAttributes: PhotoViewHeroAttributes(tag: item.id), heroAttributes: PhotoViewHeroAttributes(
tag: item.mediaService.mediaFile.mediaId),
) )
: PhotoViewGalleryPageOptions( : PhotoViewGalleryPageOptions(
imageProvider: FileImage(item.imagePath!), imageProvider: FileImage(item.mediaService.storedPath),
initialScale: PhotoViewComputedScale.contained, initialScale: PhotoViewComputedScale.contained,
minScale: PhotoViewComputedScale.contained, minScale: PhotoViewComputedScale.contained,
maxScale: PhotoViewComputedScale.covered * 4.1, maxScale: PhotoViewComputedScale.covered * 4.1,
heroAttributes: PhotoViewHeroAttributes(tag: item.id), heroAttributes: PhotoViewHeroAttributes(
tag: item.mediaService.mediaFile.mediaId),
); );
} }
} }

View file

@ -6,7 +6,7 @@ import 'package:twonly/src/database/daos/contacts.dao.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/components/initialsavatar.dart'; import 'package:twonly/src/views/components/initialsavatar.dart';
import 'package:twonly/src/views/components/user_context_menu.dart'; import 'package:twonly/src/views/components/user_context_menu.component.dart';
class PrivacyViewBlockUsers extends StatefulWidget { class PrivacyViewBlockUsers extends StatefulWidget {
const PrivacyViewBlockUsers({super.key}); const PrivacyViewBlockUsers({super.key});

View file

@ -1,10 +1,8 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:hashlib/random.dart'; import 'package:hashlib/random.dart';
import 'package:twonly/src/services/api/mediafiles/upload.service.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/pow.dart'; import 'package:twonly/src/utils/pow.dart';
import 'package:twonly/src/views/components/animate_icon.dart'; import 'package:twonly/src/views/components/animate_icon.dart';
@ -22,15 +20,6 @@ void main() {
expect(await calculatePoW(Uint8List.fromList([41, 41, 41, 41]), 6), 33); expect(await calculatePoW(Uint8List.fromList([41, 41, 41, 41]), 6), 33);
}); });
test('test utils', () async {
final list1 = Uint8List.fromList([41, 41, 41, 41, 41, 41, 41]);
final list2 = Uint8List.fromList([42, 42, 42]);
final combined = combineUint8Lists(list1, list2);
final lists = extractUint8Lists(combined);
expect(list1, lists[0]);
expect(list2, lists[1]);
});
test('encode hex', () async { test('encode hex', () async {
final list1 = Uint8List.fromList([41, 41, 41, 41, 41, 41, 41]); final list1 = Uint8List.fromList([41, 41, 41, 41, 41, 41, 41]);
expect(list1, hexToUint8List(uint8ListToHex(list1))); expect(list1, hexToUint8List(uint8ListToHex(list1)));