Merge pull request #266 from twonlyapp/dev

Dev
This commit is contained in:
Tobi 2025-08-05 14:11:00 +02:00 committed by GitHub
commit 63e4a175d6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
49 changed files with 5320 additions and 739 deletions

View file

@ -1,5 +1,18 @@
# Changelog
## 0.0.60
- Improved logging to debug the 'Tap to load' issue.
==> If you encounter any issues, please send your debug log via the feedback button along with a short description of the error so that we can resolve them. :)
- Display your own avatar in the title bar of the chat list.
- Created a default avatar image in case none was set.
- Improved UI handling when requesting microphone access for the first time.
- Flutter SDK and dependencies upgraded.
- Multiple bug fixes.
## 0.0.59
- Fixing media download error

View file

@ -0,0 +1,27 @@
<!-- Taken from: https://getavataaars.com/ -->
<svg width="264px" height="280px" viewBox="0 0 264 280" version="1.1" xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<path
d="M124,144.610951 L124,163 L128,163 L128,163 C167.764502,163 200,195.235498 200,235 L200,244 L0,244 L0,235 C-4.86974701e-15,195.235498 32.235498,163 72,163 L72,163 L76,163 L76,144.610951 C58.7626345,136.422372 46.3722246,119.687011 44.3051388,99.8812385 C38.4803105,99.0577866 34,94.0521096 34,88 L34,74 C34,68.0540074 38.3245733,63.1180731 44,62.1659169 L44,56 L44,56 C44,25.072054 69.072054,5.68137151e-15 100,0 L100,0 L100,0 C130.927946,-5.68137151e-15 156,25.072054 156,56 L156,62.1659169 C161.675427,63.1180731 166,68.0540074 166,74 L166,88 C166,94.0521096 161.51969,99.0577866 155.694861,99.8812385 C153.627775,119.687011 141.237365,136.422372 124,144.610951 Z"
id="react-path-3"></path>
</defs>
<g id="Avataaar" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g transform="translate(-825.000000, -1100.000000)">
<g transform="translate(825.000000, 1100.000000)">
<g id="Avataaar" stroke-width="1" fill-rule="evenodd">
<g id="Body" transform="translate(32.000000, 36.000000)">
<mask id="react-mask-6" fill="white">
<use xlink:href="#react-path-3"></use>
</mask>
<g id="Skin/👶🏽-03-Brown" mask="url(#react-mask-6)" fill="#57CC99">
<g transform="translate(0.000000, 0.000000)" id="Color">
<rect x="0" y="0" width="264" height="280"></rect>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -19,10 +19,10 @@ PODS:
- Firebase/Messaging (11.15.0):
- Firebase/CoreOnly
- FirebaseMessaging (~> 11.15.0)
- firebase_core (3.15.1):
- firebase_core (3.15.2):
- Firebase/CoreOnly (= 11.15.0)
- Flutter
- firebase_messaging (15.2.9):
- firebase_messaging (15.2.10):
- Firebase/Messaging (= 11.15.0)
- firebase_core
- Flutter
@ -204,28 +204,31 @@ PODS:
- sqflite_darwin (0.0.4):
- Flutter
- FlutterMacOS
- sqlite3 (3.50.2):
- sqlite3/common (= 3.50.2)
- sqlite3/common (3.50.2)
- sqlite3/dbstatvtab (3.50.2):
- sqlite3 (3.50.3):
- sqlite3/common (= 3.50.3)
- sqlite3/common (3.50.3)
- sqlite3/dbstatvtab (3.50.3):
- sqlite3/common
- sqlite3/fts5 (3.50.2):
- sqlite3/fts5 (3.50.3):
- sqlite3/common
- sqlite3/math (3.50.2):
- sqlite3/math (3.50.3):
- sqlite3/common
- sqlite3/perf-threadsafe (3.50.2):
- sqlite3/perf-threadsafe (3.50.3):
- sqlite3/common
- sqlite3/rtree (3.50.2):
- sqlite3/rtree (3.50.3):
- sqlite3/common
- sqlite3/session (3.50.3):
- sqlite3/common
- sqlite3_flutter_libs (0.0.1):
- Flutter
- FlutterMacOS
- sqlite3 (~> 3.50.1)
- sqlite3 (~> 3.50.3)
- sqlite3/dbstatvtab
- sqlite3/fts5
- sqlite3/math
- sqlite3/perf-threadsafe
- sqlite3/rtree
- sqlite3/session
- SwiftProtobuf (1.30.0)
- url_launcher_ios (0.0.1):
- Flutter
@ -364,8 +367,8 @@ SPEC CHECKSUMS:
cryptography_flutter_plus: 44f4e9e4079395fcbb3e7809c0ac2c6ae2d9576f
device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe
Firebase: d99ac19b909cd2c548339c2241ecd0d1599ab02e
firebase_core: ece862f94b2bc72ee0edbeec7ab5c7cb09fe1ab5
firebase_messaging: e1a5fae495603115be1d0183bc849da748734e2b
firebase_core: 995454a784ff288be5689b796deb9e9fa3601818
firebase_messaging: f4a41dd102ac18b840eba3f39d67e77922d3f707
FirebaseAnalytics: 6433dfd311ba78084fc93bdfc145e8cb75740eae
FirebaseCore: efb3893e5b94f32b86e331e3bd6dadf18b66568e
FirebaseCoreInternal: 9afa45b1159304c963da48addb78275ef701c6b4
@ -399,8 +402,8 @@ SPEC CHECKSUMS:
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
sqlite3: 3e82a2daae39ba3b41ae6ee84a130494585460fc
sqlite3_flutter_libs: e7fc8c9ea2200ff3271f08f127842131746b70e2
sqlite3: 83105acd294c9137c026e2da1931c30b4588ab81
sqlite3_flutter_libs: 616267f2fca40e9c6af8c5d82324e05667040b6e
SwiftProtobuf: 3697407f0d5b23bedeba9c2eaaf3ec6fdff69349
url_launcher_ios: 694010445543906933d732453a59da0a173ae33d
video_compress: f2133a07762889d67f0711ac831faa26f956980e

View file

@ -115,15 +115,6 @@ class ContactsDao extends DatabaseAccessor<TwonlyDatabase>
}
}
Future<void> newMessageExchange(int userId) {
return updateContact(
userId,
ContactsCompanion(
lastMessageExchange: Value(DateTime.now()),
),
);
}
Stream<List<Contact>> watchNotAcceptedContacts() {
return (select(contacts)
..where((t) =>

View file

@ -1,43 +0,0 @@
import 'dart:convert';
import 'package:drift/drift.dart';
import 'package:twonly/src/database/tables/media_download_table.dart';
import 'package:twonly/src/database/twonly_database.dart';
import 'package:twonly/src/utils/log.dart';
part 'media_downloads_dao.g.dart';
@DriftAccessor(tables: [MediaDownloads])
class MediaDownloadsDao extends DatabaseAccessor<TwonlyDatabase>
with _$MediaDownloadsDaoMixin {
MediaDownloadsDao(super.db);
Future<void> updateMediaDownload(
int messageId, MediaDownloadsCompanion updatedValues) {
return (update(mediaDownloads)..where((c) => c.messageId.equals(messageId)))
.write(updatedValues);
}
Future<int?> insertMediaDownload(MediaDownloadsCompanion values) async {
try {
return await into(mediaDownloads).insert(values);
} catch (e) {
Log.error('Error while inserting media upload: $e');
return null;
}
}
Future<void> deleteMediaDownload(int messageId) {
return (delete(mediaDownloads)..where((t) => t.messageId.equals(messageId)))
.go();
}
SingleOrNullSelectable<MediaDownload> getMediaDownloadById(int messageId) {
return select(mediaDownloads)..where((t) => t.messageId.equals(messageId));
}
SingleOrNullSelectable<MediaDownload> getMediaDownloadByDownloadToken(
List<int> downloadToken) {
return select(mediaDownloads)
..where((t) => t.downloadToken.equals(json.encode(downloadToken)));
}
}

View file

@ -1,8 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'media_downloads_dao.dart';
// ignore_for_file: type=lint
mixin _$MediaDownloadsDaoMixin on DatabaseAccessor<TwonlyDatabase> {
$MediaDownloadsTable get mediaDownloads => attachedDatabase.mediaDownloads;
}

View file

@ -193,7 +193,9 @@ class MessagesDao extends DatabaseAccessor<TwonlyDatabase>
Future<int?> insertMessage(MessagesCompanion message) async {
try {
await (update(contacts)
..where((c) => c.userId.equals(message.contactId.value)))
..where(
(c) => c.userId.equals(message.contactId.value),
))
.write(ContactsCompanion(lastMessageExchange: Value(DateTime.now())));
return await into(messages).insert(message);

View file

@ -1,8 +0,0 @@
import 'package:drift/drift.dart';
import 'package:twonly/src/database/tables/media_uploads_table.dart';
@DataClassName('MediaDownload')
class MediaDownloads extends Table {
IntColumn get messageId => integer()();
TextColumn get downloadToken => text().map(IntListTypeConverter())();
}

View file

@ -3,13 +3,11 @@ import 'package:drift_flutter/drift_flutter.dart'
show DriftNativeOptions, driftDatabase;
import 'package:path_provider/path_provider.dart';
import 'package:twonly/src/database/daos/contacts_dao.dart';
import 'package:twonly/src/database/daos/media_downloads_dao.dart';
import 'package:twonly/src/database/daos/media_uploads_dao.dart';
import 'package:twonly/src/database/daos/message_retransmissions.dao.dart';
import 'package:twonly/src/database/daos/messages_dao.dart';
import 'package:twonly/src/database/daos/signal_dao.dart';
import 'package:twonly/src/database/tables/contacts_table.dart';
import 'package:twonly/src/database/tables/media_download_table.dart';
import 'package:twonly/src/database/tables/media_uploads_table.dart';
import 'package:twonly/src/database/tables/message_retransmissions.dart';
import 'package:twonly/src/database/tables/messages_table.dart';
@ -29,7 +27,6 @@ part 'twonly_database.g.dart';
Contacts,
Messages,
MediaUploads,
MediaDownloads,
SignalIdentityKeyStores,
SignalPreKeyStores,
SignalSenderKeyStores,
@ -41,7 +38,6 @@ part 'twonly_database.g.dart';
MessagesDao,
ContactsDao,
MediaUploadsDao,
MediaDownloadsDao,
SignalDao,
MessageRetransmissionDao
])
@ -54,7 +50,7 @@ class TwonlyDatabase extends _$TwonlyDatabase {
TwonlyDatabase.forTesting(DatabaseConnection super.connection);
@override
int get schemaVersion => 15;
int get schemaVersion => 16;
static QueryExecutor _openConnection() {
return driftDatabase(
@ -91,7 +87,7 @@ class TwonlyDatabase extends _$TwonlyDatabase {
));
},
from4To5: (m, schema) async {
await m.createTable(mediaDownloads);
await m.createTable(schema.mediaDownloads);
await m.addColumn(schema.messages, schema.messages.mediaDownloadId);
await m.addColumn(schema.messages, schema.messages.mediaUploadId);
},
@ -140,6 +136,9 @@ class TwonlyDatabase extends _$TwonlyDatabase {
await m.addColumn(
schema.messages, schema.messages.mediaRetransmissionState);
},
from15To16: (m, schema) async {
await m.deleteTable('media_downloads');
},
),
);
}
@ -164,7 +163,6 @@ class TwonlyDatabase extends _$TwonlyDatabase {
Future<void> deleteDataForTwonlySafe() async {
await delete(messages).go();
await delete(messageRetransmissions).go();
await delete(mediaDownloads).go();
await delete(mediaUploads).go();
await update(contacts).write(
const ContactsCompanion(

View file

@ -2341,206 +2341,6 @@ class MediaUploadsCompanion extends UpdateCompanion<MediaUpload> {
}
}
class $MediaDownloadsTable extends MediaDownloads
with TableInfo<$MediaDownloadsTable, MediaDownload> {
@override
final GeneratedDatabase attachedDatabase;
final String? _alias;
$MediaDownloadsTable(this.attachedDatabase, [this._alias]);
static const VerificationMeta _messageIdMeta =
const VerificationMeta('messageId');
@override
late final GeneratedColumn<int> messageId = GeneratedColumn<int>(
'message_id', aliasedName, false,
type: DriftSqlType.int, requiredDuringInsert: true);
@override
late final GeneratedColumnWithTypeConverter<List<int>, String> downloadToken =
GeneratedColumn<String>('download_token', aliasedName, false,
type: DriftSqlType.string, requiredDuringInsert: true)
.withConverter<List<int>>(
$MediaDownloadsTable.$converterdownloadToken);
@override
List<GeneratedColumn> get $columns => [messageId, downloadToken];
@override
String get aliasedName => _alias ?? actualTableName;
@override
String get actualTableName => $name;
static const String $name = 'media_downloads';
@override
VerificationContext validateIntegrity(Insertable<MediaDownload> instance,
{bool isInserting = false}) {
final context = VerificationContext();
final data = instance.toColumns(true);
if (data.containsKey('message_id')) {
context.handle(_messageIdMeta,
messageId.isAcceptableOrUnknown(data['message_id']!, _messageIdMeta));
} else if (isInserting) {
context.missing(_messageIdMeta);
}
return context;
}
@override
Set<GeneratedColumn> get $primaryKey => const {};
@override
MediaDownload map(Map<String, dynamic> data, {String? tablePrefix}) {
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
return MediaDownload(
messageId: attachedDatabase.typeMapping
.read(DriftSqlType.int, data['${effectivePrefix}message_id'])!,
downloadToken: $MediaDownloadsTable.$converterdownloadToken.fromSql(
attachedDatabase.typeMapping.read(
DriftSqlType.string, data['${effectivePrefix}download_token'])!),
);
}
@override
$MediaDownloadsTable createAlias(String alias) {
return $MediaDownloadsTable(attachedDatabase, alias);
}
static TypeConverter<List<int>, String> $converterdownloadToken =
IntListTypeConverter();
}
class MediaDownload extends DataClass implements Insertable<MediaDownload> {
final int messageId;
final List<int> downloadToken;
const MediaDownload({required this.messageId, required this.downloadToken});
@override
Map<String, Expression> toColumns(bool nullToAbsent) {
final map = <String, Expression>{};
map['message_id'] = Variable<int>(messageId);
{
map['download_token'] = Variable<String>(
$MediaDownloadsTable.$converterdownloadToken.toSql(downloadToken));
}
return map;
}
MediaDownloadsCompanion toCompanion(bool nullToAbsent) {
return MediaDownloadsCompanion(
messageId: Value(messageId),
downloadToken: Value(downloadToken),
);
}
factory MediaDownload.fromJson(Map<String, dynamic> json,
{ValueSerializer? serializer}) {
serializer ??= driftRuntimeOptions.defaultSerializer;
return MediaDownload(
messageId: serializer.fromJson<int>(json['messageId']),
downloadToken: serializer.fromJson<List<int>>(json['downloadToken']),
);
}
@override
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
serializer ??= driftRuntimeOptions.defaultSerializer;
return <String, dynamic>{
'messageId': serializer.toJson<int>(messageId),
'downloadToken': serializer.toJson<List<int>>(downloadToken),
};
}
MediaDownload copyWith({int? messageId, List<int>? downloadToken}) =>
MediaDownload(
messageId: messageId ?? this.messageId,
downloadToken: downloadToken ?? this.downloadToken,
);
MediaDownload copyWithCompanion(MediaDownloadsCompanion data) {
return MediaDownload(
messageId: data.messageId.present ? data.messageId.value : this.messageId,
downloadToken: data.downloadToken.present
? data.downloadToken.value
: this.downloadToken,
);
}
@override
String toString() {
return (StringBuffer('MediaDownload(')
..write('messageId: $messageId, ')
..write('downloadToken: $downloadToken')
..write(')'))
.toString();
}
@override
int get hashCode => Object.hash(messageId, downloadToken);
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is MediaDownload &&
other.messageId == this.messageId &&
other.downloadToken == this.downloadToken);
}
class MediaDownloadsCompanion extends UpdateCompanion<MediaDownload> {
final Value<int> messageId;
final Value<List<int>> downloadToken;
final Value<int> rowid;
const MediaDownloadsCompanion({
this.messageId = const Value.absent(),
this.downloadToken = const Value.absent(),
this.rowid = const Value.absent(),
});
MediaDownloadsCompanion.insert({
required int messageId,
required List<int> downloadToken,
this.rowid = const Value.absent(),
}) : messageId = Value(messageId),
downloadToken = Value(downloadToken);
static Insertable<MediaDownload> custom({
Expression<int>? messageId,
Expression<String>? downloadToken,
Expression<int>? rowid,
}) {
return RawValuesInsertable({
if (messageId != null) 'message_id': messageId,
if (downloadToken != null) 'download_token': downloadToken,
if (rowid != null) 'rowid': rowid,
});
}
MediaDownloadsCompanion copyWith(
{Value<int>? messageId,
Value<List<int>>? downloadToken,
Value<int>? rowid}) {
return MediaDownloadsCompanion(
messageId: messageId ?? this.messageId,
downloadToken: downloadToken ?? this.downloadToken,
rowid: rowid ?? this.rowid,
);
}
@override
Map<String, Expression> toColumns(bool nullToAbsent) {
final map = <String, Expression>{};
if (messageId.present) {
map['message_id'] = Variable<int>(messageId.value);
}
if (downloadToken.present) {
map['download_token'] = Variable<String>($MediaDownloadsTable
.$converterdownloadToken
.toSql(downloadToken.value));
}
if (rowid.present) {
map['rowid'] = Variable<int>(rowid.value);
}
return map;
}
@override
String toString() {
return (StringBuffer('MediaDownloadsCompanion(')
..write('messageId: $messageId, ')
..write('downloadToken: $downloadToken, ')
..write('rowid: $rowid')
..write(')'))
.toString();
}
}
class $SignalIdentityKeyStoresTable extends SignalIdentityKeyStores
with TableInfo<$SignalIdentityKeyStoresTable, SignalIdentityKeyStore> {
@override
@ -4568,7 +4368,6 @@ abstract class _$TwonlyDatabase extends GeneratedDatabase {
late final $ContactsTable contacts = $ContactsTable(this);
late final $MessagesTable messages = $MessagesTable(this);
late final $MediaUploadsTable mediaUploads = $MediaUploadsTable(this);
late final $MediaDownloadsTable mediaDownloads = $MediaDownloadsTable(this);
late final $SignalIdentityKeyStoresTable signalIdentityKeyStores =
$SignalIdentityKeyStoresTable(this);
late final $SignalPreKeyStoresTable signalPreKeyStores =
@ -4587,8 +4386,6 @@ abstract class _$TwonlyDatabase extends GeneratedDatabase {
late final ContactsDao contactsDao = ContactsDao(this as TwonlyDatabase);
late final MediaUploadsDao mediaUploadsDao =
MediaUploadsDao(this as TwonlyDatabase);
late final MediaDownloadsDao mediaDownloadsDao =
MediaDownloadsDao(this as TwonlyDatabase);
late final SignalDao signalDao = SignalDao(this as TwonlyDatabase);
late final MessageRetransmissionDao messageRetransmissionDao =
MessageRetransmissionDao(this as TwonlyDatabase);
@ -4600,7 +4397,6 @@ abstract class _$TwonlyDatabase extends GeneratedDatabase {
contacts,
messages,
mediaUploads,
mediaDownloads,
signalIdentityKeyStores,
signalPreKeyStores,
signalSenderKeyStores,
@ -5999,139 +5795,6 @@ typedef $$MediaUploadsTableProcessedTableManager = ProcessedTableManager<
),
MediaUpload,
PrefetchHooks Function()>;
typedef $$MediaDownloadsTableCreateCompanionBuilder = MediaDownloadsCompanion
Function({
required int messageId,
required List<int> downloadToken,
Value<int> rowid,
});
typedef $$MediaDownloadsTableUpdateCompanionBuilder = MediaDownloadsCompanion
Function({
Value<int> messageId,
Value<List<int>> downloadToken,
Value<int> rowid,
});
class $$MediaDownloadsTableFilterComposer
extends Composer<_$TwonlyDatabase, $MediaDownloadsTable> {
$$MediaDownloadsTableFilterComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
ColumnFilters<int> get messageId => $composableBuilder(
column: $table.messageId, builder: (column) => ColumnFilters(column));
ColumnWithTypeConverterFilters<List<int>, List<int>, String>
get downloadToken => $composableBuilder(
column: $table.downloadToken,
builder: (column) => ColumnWithTypeConverterFilters(column));
}
class $$MediaDownloadsTableOrderingComposer
extends Composer<_$TwonlyDatabase, $MediaDownloadsTable> {
$$MediaDownloadsTableOrderingComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
ColumnOrderings<int> get messageId => $composableBuilder(
column: $table.messageId, builder: (column) => ColumnOrderings(column));
ColumnOrderings<String> get downloadToken => $composableBuilder(
column: $table.downloadToken,
builder: (column) => ColumnOrderings(column));
}
class $$MediaDownloadsTableAnnotationComposer
extends Composer<_$TwonlyDatabase, $MediaDownloadsTable> {
$$MediaDownloadsTableAnnotationComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
GeneratedColumn<int> get messageId =>
$composableBuilder(column: $table.messageId, builder: (column) => column);
GeneratedColumnWithTypeConverter<List<int>, String> get downloadToken =>
$composableBuilder(
column: $table.downloadToken, builder: (column) => column);
}
class $$MediaDownloadsTableTableManager extends RootTableManager<
_$TwonlyDatabase,
$MediaDownloadsTable,
MediaDownload,
$$MediaDownloadsTableFilterComposer,
$$MediaDownloadsTableOrderingComposer,
$$MediaDownloadsTableAnnotationComposer,
$$MediaDownloadsTableCreateCompanionBuilder,
$$MediaDownloadsTableUpdateCompanionBuilder,
(
MediaDownload,
BaseReferences<_$TwonlyDatabase, $MediaDownloadsTable, MediaDownload>
),
MediaDownload,
PrefetchHooks Function()> {
$$MediaDownloadsTableTableManager(
_$TwonlyDatabase db, $MediaDownloadsTable table)
: super(TableManagerState(
db: db,
table: table,
createFilteringComposer: () =>
$$MediaDownloadsTableFilterComposer($db: db, $table: table),
createOrderingComposer: () =>
$$MediaDownloadsTableOrderingComposer($db: db, $table: table),
createComputedFieldComposer: () =>
$$MediaDownloadsTableAnnotationComposer($db: db, $table: table),
updateCompanionCallback: ({
Value<int> messageId = const Value.absent(),
Value<List<int>> downloadToken = const Value.absent(),
Value<int> rowid = const Value.absent(),
}) =>
MediaDownloadsCompanion(
messageId: messageId,
downloadToken: downloadToken,
rowid: rowid,
),
createCompanionCallback: ({
required int messageId,
required List<int> downloadToken,
Value<int> rowid = const Value.absent(),
}) =>
MediaDownloadsCompanion.insert(
messageId: messageId,
downloadToken: downloadToken,
rowid: rowid,
),
withReferenceMapper: (p0) => p0
.map((e) => (e.readTable(table), BaseReferences(db, table, e)))
.toList(),
prefetchHooksCallback: null,
));
}
typedef $$MediaDownloadsTableProcessedTableManager = ProcessedTableManager<
_$TwonlyDatabase,
$MediaDownloadsTable,
MediaDownload,
$$MediaDownloadsTableFilterComposer,
$$MediaDownloadsTableOrderingComposer,
$$MediaDownloadsTableAnnotationComposer,
$$MediaDownloadsTableCreateCompanionBuilder,
$$MediaDownloadsTableUpdateCompanionBuilder,
(
MediaDownload,
BaseReferences<_$TwonlyDatabase, $MediaDownloadsTable, MediaDownload>
),
MediaDownload,
PrefetchHooks Function()>;
typedef $$SignalIdentityKeyStoresTableCreateCompanionBuilder
= SignalIdentityKeyStoresCompanion Function({
required int deviceId,
@ -7481,8 +7144,6 @@ class $TwonlyDatabaseManager {
$$MessagesTableTableManager(_db, _db.messages);
$$MediaUploadsTableTableManager get mediaUploads =>
$$MediaUploadsTableTableManager(_db, _db.mediaUploads);
$$MediaDownloadsTableTableManager get mediaDownloads =>
$$MediaDownloadsTableTableManager(_db, _db.mediaDownloads);
$$SignalIdentityKeyStoresTableTableManager get signalIdentityKeyStores =>
$$SignalIdentityKeyStoresTableTableManager(
_db, _db.signalIdentityKeyStores);

View file

@ -3497,6 +3497,222 @@ class Shape20 extends i0.VersionedTable {
columnsByName['encryption_data']! as i1.GeneratedColumn<String>;
}
final class Schema16 extends i0.VersionedSchema {
Schema16({required super.database}) : super(version: 16);
@override
late final List<i1.DatabaseSchemaEntity> entities = [
contacts,
messages,
mediaUploads,
signalIdentityKeyStores,
signalPreKeyStores,
signalSenderKeyStores,
signalSessionStores,
signalContactPreKeys,
signalContactSignedPreKeys,
messageRetransmissions,
];
late final Shape13 contacts = Shape13(
source: i0.VersionedTable(
entityName: 'contacts',
withoutRowId: false,
isStrict: false,
tableConstraints: [
'PRIMARY KEY(user_id)',
],
columns: [
_column_0,
_column_1,
_column_2,
_column_3,
_column_4,
_column_5,
_column_6,
_column_7,
_column_8,
_column_9,
_column_39,
_column_53,
_column_57,
_column_54,
_column_40,
_column_10,
_column_11,
_column_12,
_column_13,
_column_14,
_column_55,
_column_15,
_column_16,
],
attachedDatabase: database,
),
alias: null);
late final Shape19 messages = Shape19(
source: i0.VersionedTable(
entityName: 'messages',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_17,
_column_18,
_column_19,
_column_48,
_column_49,
_column_20,
_column_21,
_column_22,
_column_52,
_column_23,
_column_24,
_column_25,
_column_70,
_column_26,
_column_27,
_column_28,
_column_29,
_column_30,
],
attachedDatabase: database,
),
alias: null);
late final Shape20 mediaUploads = Shape20(
source: i0.VersionedTable(
entityName: 'media_uploads',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_41,
_column_42,
_column_56,
_column_44,
_column_45,
],
attachedDatabase: database,
),
alias: null);
late final Shape2 signalIdentityKeyStores = Shape2(
source: i0.VersionedTable(
entityName: 'signal_identity_key_stores',
withoutRowId: false,
isStrict: false,
tableConstraints: [
'PRIMARY KEY(device_id, name)',
],
columns: [
_column_31,
_column_32,
_column_33,
_column_10,
],
attachedDatabase: database,
),
alias: null);
late final Shape3 signalPreKeyStores = Shape3(
source: i0.VersionedTable(
entityName: 'signal_pre_key_stores',
withoutRowId: false,
isStrict: false,
tableConstraints: [
'PRIMARY KEY(pre_key_id)',
],
columns: [
_column_34,
_column_35,
_column_10,
],
attachedDatabase: database,
),
alias: null);
late final Shape4 signalSenderKeyStores = Shape4(
source: i0.VersionedTable(
entityName: 'signal_sender_key_stores',
withoutRowId: false,
isStrict: false,
tableConstraints: [
'PRIMARY KEY(sender_key_name)',
],
columns: [
_column_36,
_column_37,
],
attachedDatabase: database,
),
alias: null);
late final Shape5 signalSessionStores = Shape5(
source: i0.VersionedTable(
entityName: 'signal_session_stores',
withoutRowId: false,
isStrict: false,
tableConstraints: [
'PRIMARY KEY(device_id, name)',
],
columns: [
_column_31,
_column_32,
_column_38,
_column_10,
],
attachedDatabase: database,
),
alias: null);
late final Shape14 signalContactPreKeys = Shape14(
source: i0.VersionedTable(
entityName: 'signal_contact_pre_keys',
withoutRowId: false,
isStrict: false,
tableConstraints: [
'PRIMARY KEY(contact_id, pre_key_id)',
],
columns: [
_column_58,
_column_34,
_column_35,
_column_10,
],
attachedDatabase: database,
),
alias: null);
late final Shape15 signalContactSignedPreKeys = Shape15(
source: i0.VersionedTable(
entityName: 'signal_contact_signed_pre_keys',
withoutRowId: false,
isStrict: false,
tableConstraints: [
'PRIMARY KEY(contact_id)',
],
columns: [
_column_58,
_column_59,
_column_60,
_column_61,
_column_10,
],
attachedDatabase: database,
),
alias: null);
late final Shape18 messageRetransmissions = Shape18(
source: i0.VersionedTable(
entityName: 'message_retransmissions',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_62,
_column_63,
_column_64,
_column_65,
_column_66,
_column_69,
_column_67,
],
attachedDatabase: database,
),
alias: null);
}
i0.MigrationStepWithVersion migrationSteps({
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
@ -3512,6 +3728,7 @@ i0.MigrationStepWithVersion migrationSteps({
required Future<void> Function(i1.Migrator m, Schema13 schema) from12To13,
required Future<void> Function(i1.Migrator m, Schema14 schema) from13To14,
required Future<void> Function(i1.Migrator m, Schema15 schema) from14To15,
required Future<void> Function(i1.Migrator m, Schema16 schema) from15To16,
}) {
return (currentVersion, database) async {
switch (currentVersion) {
@ -3585,6 +3802,11 @@ i0.MigrationStepWithVersion migrationSteps({
final migrator = i1.Migrator(database, schema);
await from14To15(migrator, schema);
return 15;
case 15:
final schema = Schema16(database: database);
final migrator = i1.Migrator(database, schema);
await from15To16(migrator, schema);
return 16;
default:
throw ArgumentError.value('Unknown migration from $currentVersion');
}
@ -3606,6 +3828,7 @@ i1.OnUpgrade stepByStep({
required Future<void> Function(i1.Migrator m, Schema13 schema) from12To13,
required Future<void> Function(i1.Migrator m, Schema14 schema) from13To14,
required Future<void> Function(i1.Migrator m, Schema15 schema) from14To15,
required Future<void> Function(i1.Migrator m, Schema16 schema) from15To16,
}) =>
i0.VersionedSchema.stepByStepHelper(
step: migrationSteps(
@ -3623,4 +3846,5 @@ i1.OnUpgrade stepByStep({
from12To13: from12To13,
from13To14: from13To14,
from14To15: from14To15,
from15To16: from15To16,
));

View file

@ -15,7 +15,7 @@ Color getMessageColorFromType(MessageContent content, BuildContext context) {
color = context.color.primary;
} else {
if (content.isVideo) {
color = const Color.fromARGB(255, 240, 243, 33);
color = const Color.fromARGB(255, 243, 33, 208);
} else {
color = Colors.redAccent;
}

View file

@ -49,6 +49,9 @@ class UserData {
@JsonKey(defaultValue: true)
bool useHighQuality = true;
@JsonKey(defaultValue: false)
bool requestedAudioPermission = false;
@JsonKey(defaultValue: true)
bool showFeedbackShortcut = true;

View file

@ -26,6 +26,8 @@ UserData _$UserDataFromJson(Map<String, dynamic> json) => UserData(
ThemeMode.system
..defaultShowTime = (json['defaultShowTime'] as num?)?.toInt()
..useHighQuality = json['useHighQuality'] as bool? ?? true
..requestedAudioPermission =
json['requestedAudioPermission'] as bool? ?? false
..showFeedbackShortcut = json['showFeedbackShortcut'] as bool? ?? true
..preSelectedEmojies = (json['preSelectedEmojies'] as List<dynamic>?)
?.map((e) => e as String)
@ -84,6 +86,7 @@ Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{
'themeMode': _$ThemeModeEnumMap[instance.themeMode]!,
'defaultShowTime': instance.defaultShowTime,
'useHighQuality': instance.useHighQuality,
'requestedAudioPermission': instance.requestedAudioPermission,
'showFeedbackShortcut': instance.showFeedbackShortcut,
'preSelectedEmojies': instance.preSelectedEmojies,
'autoDownloadOptions': instance.autoDownloadOptions,

View file

@ -120,32 +120,34 @@ Future<void> handleDownloadStatusUpdateInternal(
Mutex protectDownload = Mutex();
Future<void> startDownloadMedia(Message message, bool force) async {
if (message.contentJson == null) return;
final content = MessageContent.fromJson(
message.kind, jsonDecode(message.contentJson!) as Map);
if (content is! MediaMessageContent) return;
if (content.downloadToken == null) return;
var media = await twonlyDB.mediaDownloadsDao
.getMediaDownloadById(message.messageId)
.getSingleOrNull();
if (media == null) {
await twonlyDB.mediaDownloadsDao.insertMediaDownload(
MediaDownloadsCompanion(
messageId: Value(message.messageId),
downloadToken: Value(content.downloadToken!),
),
);
media = await twonlyDB.mediaDownloadsDao
.getMediaDownloadById(message.messageId)
.getSingleOrNull();
Log.info(
'Download blocked for ${message.messageId} because of network state.');
if (message.contentJson == null) {
Log.error('Content of ${message.messageId} not found.');
await handleMediaError(message);
return;
}
if (media == null) return;
final content = MessageContent.fromJson(
message.kind,
jsonDecode(message.contentJson!) as Map,
);
if (content is! MediaMessageContent) {
Log.error('Content of ${message.messageId} is not media file.');
await handleMediaError(message);
return;
}
if (content.downloadToken == null) {
Log.error('Download token not defined for ${message.messageId}.');
await handleMediaError(message);
return;
}
if (!force && !await isAllowedToDownload(content.isVideo)) {
Log.warn(
'Download blocked for ${message.messageId} because of network state.');
return;
}
@ -185,10 +187,10 @@ Future<void> startDownloadMedia(Message message, bool force) async {
try {
final task = DownloadTask(
url: apiUrl,
taskId: 'download_${media.messageId}',
taskId: 'download_${message.messageId}',
directory: 'media/received/',
baseDirectory: BaseDirectory.applicationSupport,
filename: '${media.messageId}.encrypted',
filename: '${message.messageId}.encrypted',
priority: 0,
retries: 10,
);
@ -198,7 +200,7 @@ Future<void> startDownloadMedia(Message message, bool force) async {
);
try {
await downloadFileFast(media.messageId, apiUrl);
await downloadFileFast(message.messageId, apiUrl);
} catch (e) {
Log.error('Fast download failed: $e');
await FileDownloader().enqueue(task);

View file

@ -555,8 +555,9 @@ Future<void> handleMediaUpload(MediaUpload media) async {
timestamp: media.metadata!.messageSendAt,
);
final plaintextContent =
Uint8List.fromList(gzip.encode(utf8.encode(jsonEncode(msg.toJson()))));
final plaintextContent = Uint8List.fromList(
gzip.encode(utf8.encode(jsonEncode(msg.toJson()))),
);
final contact = await twonlyDB.contactsDao
.getContactByUserId(message.contactId)

View file

@ -398,9 +398,12 @@ Future<client.Response> handleNewMessage(int fromUserId, Uint8List body) async {
);
if (messageId == null) {
Log.error('could not insert message into db');
return client.Response()..error = ErrorCode.InternalError;
}
Log.info('Inserted a new message with id: $messageId');
if (message.kind == MessageKind.media) {
await twonlyDB.contactsDao.incFlameCounter(
fromUserId,

View file

@ -13,6 +13,7 @@ import 'package:twonly/src/database/daos/contacts_dao.dart';
import 'package:twonly/src/database/twonly_database.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/storage.dart';
import 'package:twonly/src/views/camera/camera_preview_components/permissions_view.dart';
import 'package:twonly/src/views/camera/camera_preview_components/send_to.dart';
import 'package:twonly/src/views/camera/camera_preview_components/video_recording_time.dart';
@ -171,6 +172,19 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
Future<void> initAsync() async {
hasAudioPermission = await Permission.microphone.isGranted;
if (!hasAudioPermission) {
final user = await getUser();
if (user != null) {
if (!user.requestedAudioPermission) {
await updateUserdata((u) {
u.requestedAudioPermission = true;
return u;
});
await requestMicrophonePermission();
}
}
}
if (!mounted) return;
setState(() {});
}

View file

@ -10,7 +10,6 @@ import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/components/flame.dart';
import 'package:twonly/src/views/components/headline.dart';
import 'package:twonly/src/views/components/initialsavatar.dart';
import 'package:twonly/src/views/components/verified_shield.dart';
class BestFriendsSelector extends StatelessWidget {
const BestFriendsSelector({
@ -157,13 +156,6 @@ class UserCheckbox extends StatelessWidget {
children: [
Row(
children: [
if (isRealTwonly)
Padding(
padding: const EdgeInsets.only(right: 2),
child: VerifiedShield(
user,
size: 12,
)),
Text(
displayName.length > 8
? '${displayName.substring(0, 8)}...'

View file

@ -21,7 +21,6 @@ import 'package:twonly/src/views/camera/image_editor/data/layer.dart';
import 'package:twonly/src/views/camera/image_editor/layers_viewer.dart';
import 'package:twonly/src/views/camera/image_editor/modules/all_emojis.dart';
import 'package:twonly/src/views/camera/share_image_view.dart';
import 'package:twonly/src/views/components/alert_dialog.dart';
import 'package:twonly/src/views/components/media_view_sizing.dart';
import 'package:twonly/src/views/components/notification_badge.dart';
import 'package:twonly/src/views/settings/subscription/subscription.view.dart';
@ -242,16 +241,6 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
? Theme.of(context).colorScheme.primary
: Colors.white,
onPressed: () async {
if (widget.sendTo != null) {
if (!widget.sendTo!.verified) {
await showAlertDialog(
context,
context.lang.shareImageUserNotVerified,
context.lang.shareImageUserNotVerifiedDesc,
);
return;
}
}
_isRealTwonly = !_isRealTwonly;
if (_isRealTwonly) {
maxShowTime = 12;

View file

@ -54,7 +54,6 @@ class _ShareImageView extends State<ShareImageView> {
bool sendingImage = false;
bool hideArchivedUsers = true;
final TextEditingController searchUserName = TextEditingController();
bool showRealTwonlyWarning = false;
late StreamSubscription<List<Contact>> contactSub;
String lastQuery = '';
@ -150,15 +149,6 @@ class _ShareImageView extends State<ShareImageView> {
}
void updateStatus(int userId, bool checked) {
if (widget.isRealTwonly) {
final user = contacts.firstWhere((x) => x.userId == userId);
if (!user.verified) {
showRealTwonlyWarning = true;
setState(() {});
return;
}
}
showRealTwonlyWarning = false;
widget.updateStatus(userId, checked);
setState(() {});
}
@ -175,13 +165,6 @@ class _ShareImageView extends State<ShareImageView> {
const EdgeInsets.only(bottom: 40, left: 10, top: 20, right: 10),
child: Column(
children: [
if (showRealTwonlyWarning)
Text(
context.lang.shareImageAllTwonlyWarning,
style: const TextStyle(color: Colors.orange, fontSize: 13),
textAlign: TextAlign.center,
),
if (showRealTwonlyWarning) const SizedBox(height: 10),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 10),
child: TextField(

View file

@ -10,6 +10,7 @@ import 'package:twonly/globals.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_database.dart';
import 'package:twonly/src/model/json/userdata.dart';
import 'package:twonly/src/providers/connection.provider.dart';
import 'package:twonly/src/services/api/media_download.dart';
import 'package:twonly/src/utils/misc.dart';
@ -21,14 +22,15 @@ import 'package:twonly/src/views/chats/chat_list_components/connection_info.comp
import 'package:twonly/src/views/chats/chat_list_components/feedback_btn.dart';
import 'package:twonly/src/views/chats/chat_list_components/last_message_time.dart';
import 'package:twonly/src/views/chats/chat_messages.view.dart';
import 'package:twonly/src/views/chats/chat_messages_components/message_send_state_icon.dart';
import 'package:twonly/src/views/chats/media_viewer.view.dart';
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/initialsavatar.dart';
import 'package:twonly/src/views/components/message_send_state_icon.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/settings/help/changelog.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/subscription/subscription.view.dart';
import 'package:twonly/src/views/tutorial/tutorials.dart';
@ -43,6 +45,7 @@ class _ChatListViewState extends State<ChatListView> {
late StreamSubscription<List<Contact>> _contactsSub;
List<Contact> _contacts = [];
List<Contact> _pinnedContacts = [];
UserData? _user;
GlobalKey firstUserListItemKey = GlobalKey();
GlobalKey searchForOtherUsers = GlobalKey();
@ -76,6 +79,9 @@ class _ChatListViewState extends State<ChatListView> {
final user = await getUser();
if (user == null) return;
setState(() {
_user = user;
});
final changeLog = await rootBundle.loadString('CHANGELOG.md');
final changeLogHash =
(await compute(Sha256().hash, changeLog.codeUnits)).bytes;
@ -112,6 +118,23 @@ class _ChatListViewState extends State<ChatListView> {
return Scaffold(
appBar: AppBar(
title: Row(children: [
GestureDetector(
onTap: () async {
await Navigator.push(context,
MaterialPageRoute(builder: (context) {
return const ProfileView();
}));
_user = await getUser();
if (!mounted) return;
setState(() {});
},
child: ContactAvatar(
userData: _user,
fontSize: 14,
color: context.color.onSurface.withAlpha(20),
),
),
const SizedBox(width: 10),
const Text('twonly '),
if (planId != 'Free')
GestureDetector(
@ -164,13 +187,16 @@ class _ChatListViewState extends State<ChatListView> {
},
),
IconButton(
onPressed: () {
Navigator.push(
onPressed: () async {
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const SettingsMainView(),
),
);
_user = await getUser();
if (!mounted) return;
setState(() {});
},
icon: const FaIcon(FontAwesomeIcons.gear, size: 19),
)
@ -298,6 +324,7 @@ class _UserListItem extends State<UserListItem> {
late StreamSubscription<List<Message>> lastMessageStream;
List<Message> previewMessages = [];
bool hasNonOpenedMediaFile = false;
@override
void initState() {
@ -352,6 +379,17 @@ class _UserListItem extends State<UserListItem> {
}
}
final msgs =
previewMessages.where((x) => x.kind == MessageKind.media).toList();
if (msgs.isNotEmpty &&
msgs.first.kind == MessageKind.media &&
msgs.first.messageOtherId != null &&
msgs.first.openedAt == null) {
hasNonOpenedMediaFile = true;
} else {
hasNonOpenedMediaFile = false;
}
lastMessages = newLastMessages;
messagesNotOpened = newMessagesNotOpened;
setState(() {
@ -368,12 +406,10 @@ class _UserListItem extends State<UserListItem> {
));
return;
}
if (hasNonOpenedMediaFile) {
final msgs =
previewMessages.where((x) => x.kind == MessageKind.media).toList();
if (msgs.isNotEmpty &&
msgs.first.kind == MessageKind.media &&
msgs.first.messageOtherId != null &&
msgs.first.openedAt == null) {
switch (msgs.first.downloadState) {
case DownloadState.pending:
await startDownloadMedia(msgs.first, true);
@ -447,12 +483,18 @@ class _UserListItem extends State<UserListItem> {
onPressed: () {
Navigator.push(context, MaterialPageRoute(
builder: (context) {
if (hasNonOpenedMediaFile) {
return ChatMessagesView(widget.user);
} else {
return CameraSendToView(widget.user);
}
},
));
},
icon: FaIcon(
FontAwesomeIcons.camera,
hasNonOpenedMediaFile
? FontAwesomeIcons.solidComments
: FontAwesomeIcons.camera,
color: context.color.outline.withAlpha(150),
),
),

View file

@ -297,7 +297,8 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
children: [
Text(getContactDisplayName(user)),
const SizedBox(width: 10),
VerifiedShield(key: verifyShieldKey, user),
if (user.verified)
VerifiedShield(key: verifyShieldKey, user)
],
),
),

View file

@ -12,20 +12,23 @@ class ChatDateChip extends StatelessWidget {
final formattedDate = item.isTime
? DateFormat.Hm(Localizations.localeOf(context).toLanguageTag())
.format(item.time!)
: '${DateFormat.Hm(Localizations.localeOf(context).toLanguageTag()).format(item.date!)} ${DateFormat.yMd(Localizations.localeOf(context).toLanguageTag()).format(item.date!)}';
: '${DateFormat.Hm(Localizations.localeOf(context).toLanguageTag()).format(item.date!)}\n${DateFormat.yMd(Localizations.localeOf(context).toLanguageTag()).format(item.date!)}';
return Center(
child: Container(
decoration: BoxDecoration(
color: Colors.black.withAlpha(40),
color: isDarkMode(context)
? const Color.fromARGB(255, 38, 38, 38)
: Colors.black.withAlpha(40),
borderRadius: BorderRadius.circular(8),
),
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8),
child: Text(
formattedDate,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 10,
color: isDarkMode(context) ? Colors.white : Colors.black,
color: isDarkMode(context) ? Colors.white : Colors.grey,
),
),
),

View file

@ -4,7 +4,7 @@ import 'package:flutter/material.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/database/twonly_database.dart';
import 'package:twonly/src/model/memory_item.model.dart';
import 'package:twonly/src/views/components/message_send_state_icon.dart';
import 'package:twonly/src/views/chats/chat_messages_components/message_send_state_icon.dart';
import 'package:twonly/src/views/memories/memories_item_thumbnail.dart';
import 'package:twonly/src/views/memories/memories_photo_slider.view.dart';

View file

@ -1,6 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:twonly/src/database/daos/contacts_dao.dart';
import 'package:twonly/src/database/twonly_database.dart';
import 'package:twonly/src/model/json/userdata.dart';
import 'package:twonly/src/utils/log.dart';
@ -11,21 +10,20 @@ class ContactAvatar extends StatelessWidget {
this.contact,
this.userData,
this.fontSize = 20,
this.color,
});
final Contact? contact;
final UserData? userData;
final double? fontSize;
final Color? color;
@override
Widget build(BuildContext context) {
var displayName = '';
String? avatarSvg;
if (contact != null) {
displayName = getContactDisplayName(contact!).replaceAll('\u0336', '');
avatarSvg = contact!.avatarSvg;
} else if (userData != null) {
displayName = userData!.displayName;
avatarSvg = userData!.avatarSvg;
} else {
return Container();
@ -33,63 +31,6 @@ class ContactAvatar extends StatelessWidget {
final proSize = (fontSize == null) ? 40 : (fontSize! * 2);
if (avatarSvg != null) {
return Container(
constraints: BoxConstraints(
minHeight: 2 * (fontSize ?? 20),
minWidth: 2 * (fontSize ?? 20),
maxWidth: 2 * (fontSize ?? 20),
maxHeight: 2 * (fontSize ?? 20),
),
child: Center(
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: SizedBox(
height: proSize as double,
width: proSize,
child: Center(
child: SvgPicture.string(
avatarSvg,
errorBuilder: (context, error, stackTrace) {
Log.error('$error');
return Container();
},
),
),
),
),
),
);
}
// Extract initials from the displayName
final nameParts = displayName.split(' ');
var initials = nameParts.map((part) => part[0]).join().toUpperCase();
if (initials.length > 2) {
initials = initials[0] + initials[1];
} else if (initials.length == 1) {
initials = displayName[0] + displayName[1];
}
initials = initials.toUpperCase();
// Generate a color based on the initials (you can customize this logic)
final avatarColor = _getColorFromUsername(
displayName, Theme.of(context).brightness == Brightness.dark);
final Widget child = Text(
initials,
style: TextStyle(
color: _getTextColor(avatarColor),
fontWeight: FontWeight.normal,
fontSize: fontSize,
),
);
final isPro = initials[0] == 'T';
if (isPro) {
return Container(
constraints: BoxConstraints(
minHeight: 2 * (fontSize ?? 20),
@ -103,78 +44,21 @@ class ContactAvatar extends StatelessWidget {
child: Container(
height: proSize as double,
width: proSize,
//padding: EdgeInsets.symmetric(vertical: 5, horizontal: 10),
color: avatarColor,
child: Center(child: child),
color: color,
child: Center(
child: avatarSvg == null
? SvgPicture.asset('assets/images/default_avatar.svg')
: SvgPicture.string(
avatarSvg,
errorBuilder: (context, error, stackTrace) {
Log.error('$error');
return Container();
},
),
),
),
),
),
);
} else {
return CircleAvatar(
backgroundColor: avatarColor,
radius: fontSize,
child: child,
);
}
}
Color _getTextColor(Color color) {
const value = 100.0;
// Ensure the value does not exceed the RGB limits
final newRed = ((color.r * 255) - value).clamp(0, 255).round();
final newGreen = (color.g * 255 - value).clamp(0, 255).round();
final newBlue = (color.b * 255 - value).clamp(0, 255).round();
return Color.fromARGB((color.a * 255).round(), newRed, newGreen, newBlue);
}
Color _getColorFromUsername(String displayName, bool isDarkMode) {
// Define color lists for light and dark themes
final lightColors = <Color>[
Colors.red,
Colors.green,
Colors.blue,
Colors.orange,
Colors.purple,
Colors.teal,
Colors.amber,
Colors.indigo,
Colors.cyan,
Colors.lime,
Colors.pink,
Colors.brown,
Colors.grey,
];
final darkColors = <Color>[
const Color.fromARGB(255, 246, 227, 254), // Light Lavender
const Color.fromARGB(255, 246, 216, 215), // Light Pink
const Color.fromARGB(255, 226, 236, 235), // Light Teal
const Color.fromARGB(255, 255, 224, 178), // Light Yellow
const Color.fromARGB(255, 255, 182, 193), // Light Pink (Hot Pink)
const Color.fromARGB(255, 173, 216, 230), // Light Blue
const Color.fromARGB(255, 221, 160, 221), // Plum
const Color.fromARGB(255, 255, 228, 196), // Bisque
const Color.fromARGB(255, 240, 230, 140), // Khaki
const Color.fromARGB(255, 255, 192, 203), // Pink
const Color.fromARGB(255, 255, 218, 185), // Peach Puff
const Color.fromARGB(255, 255, 160, 122), // Light Salmon
const Color.fromARGB(255, 135, 206, 250), // Light Sky Blue
const Color.fromARGB(255, 255, 228, 225), // Misty Rose
const Color.fromARGB(255, 240, 248, 255), // Alice Blue
const Color.fromARGB(255, 255, 250, 205), // Lemon Chiffon
const Color.fromARGB(255, 255, 218, 185), // Peach Puff
];
// Simple logic to generate a hash from initials
final hash =
displayName.codeUnits.fold(0, (prev, element) => prev + element);
// Select the appropriate color list based on the current theme brightness
final colors = isDarkMode ? darkColors : lightColors;
// Use the hash to select a color from the list
return colors[hash % colors.length];
}
}

View file

@ -8,7 +8,6 @@ import 'package:twonly/src/database/twonly_database.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';
import 'package:twonly/src/views/contact/contact_verify.view.dart';
class UserContextMenu extends StatefulWidget {
const UserContextMenu({
@ -58,18 +57,6 @@ class _UserContextMenuState extends State<UserContextMenu> {
},
child: const FaIcon(FontAwesomeIcons.boxOpen),
),
if (!widget.contact.verified)
PieAction(
tooltip: Text(context.lang.contextMenuVerifyUser),
onSelect: () {
Navigator.push(context, MaterialPageRoute(
builder: (context) {
return ContactVerifyView(widget.contact);
},
));
},
child: const Icon(Icons.gpp_maybe_rounded),
),
PieAction(
tooltip: Text(context.lang.contextMenuOpenChat),
onSelect: () {

View file

@ -310,7 +310,7 @@ class _SubscriptionViewState extends State<SubscriptionView> {
}));
},
),
if (isPayingUser)
if (isPayingUser || currentPlan == 'Tester')
BetterListTile(
icon: FontAwesomeIcons.userPlus,
text: context.lang.manageAdditionalUsers,
@ -387,8 +387,10 @@ class PlanCard extends StatelessWidget {
];
case 'Family':
features = [
context.lang.familyFeature1,
context.lang.proFeature1,
context.lang.familyFeature2,
context.lang.proFeature3,
context.lang.proFeature4,
];
default:
}

View file

@ -13,10 +13,10 @@ packages:
dependency: transitive
description:
name: _flutterfire_internals
sha256: a5788040810bd84400bc209913fbc40f388cded7cdf95ee2f5d2bff7e38d5241
sha256: ff0a84a2734d9e1089f8aedd5c0af0061b82fb94e95260d943404e0ef2134b11
url: "https://pub.dev"
source: hosted
version: "1.3.58"
version: "1.3.59"
adaptive_number:
dependency: transitive
description:
@ -29,10 +29,10 @@ packages:
dependency: transitive
description:
name: analyzer
sha256: de617bfdc64f3d8b00835ec2957441ceca0a29cdf7881f7ab231bc14f71159c0
sha256: "974859dc0ff5f37bc4313244b3218c791810d03ab3470a579580279ba971a48d"
url: "https://pub.dev"
source: hosted
version: "7.5.6"
version: "7.7.1"
archive:
dependency: transitive
description:
@ -69,10 +69,10 @@ packages:
dependency: "direct main"
description:
name: background_downloader
sha256: d3016a9eb584f6cb16384c8b4a008943c39119730d60046044349b5dbbda4ccb
sha256: c59bff0b66a6704bed8bfb09c67571df88167906e0f5543a722373b3d180a743
url: "https://pub.dev"
source: hosted
version: "9.2.2"
version: "9.2.3"
boolean_selector:
dependency: transitive
description:
@ -85,10 +85,10 @@ packages:
dependency: transitive
description:
name: build
sha256: "51dc711996cbf609b90cbe5b335bbce83143875a9d58e4b5c6d3c4f684d3dda7"
sha256: "7d95cbbb1526ab5ae977df9b4cc660963b9b27f6d1075c0b34653868911385e4"
url: "https://pub.dev"
source: hosted
version: "2.5.4"
version: "3.0.0"
build_config:
dependency: transitive
description:
@ -109,26 +109,26 @@ packages:
dependency: transitive
description:
name: build_resolvers
sha256: ee4257b3f20c0c90e72ed2b57ad637f694ccba48839a821e87db762548c22a62
sha256: "38c9c339333a09b090a638849a4c56e70a404c6bdd3b511493addfbc113b60c2"
url: "https://pub.dev"
source: hosted
version: "2.5.4"
version: "3.0.0"
build_runner:
dependency: "direct dev"
description:
name: build_runner
sha256: "382a4d649addbfb7ba71a3631df0ec6a45d5ab9b098638144faf27f02778eb53"
sha256: b971d4a1c789eba7be3e6fe6ce5e5b50fd3719e3cb485b3fad6d04358304351d
url: "https://pub.dev"
source: hosted
version: "2.5.4"
version: "2.6.0"
build_runner_core:
dependency: transitive
description:
name: build_runner_core
sha256: "85fbbb1036d576d966332a3f5ce83f2ce66a40bea1a94ad2d5fc29a19a0d3792"
sha256: c04e612ca801cd0928ccdb891c263a2b1391cb27940a5ea5afcf9ba894de5d62
url: "https://pub.dev"
source: hosted
version: "9.1.2"
version: "9.2.0"
built_collection:
dependency: transitive
description:
@ -141,10 +141,10 @@ packages:
dependency: transitive
description:
name: built_value
sha256: "082001b5c3dc495d4a42f1d5789990505df20d8547d42507c29050af6933ee27"
sha256: "0b1b12a0a549605e5f04476031cd0bc91ead1d7c8e830773a18ee54179b3cb62"
url: "https://pub.dev"
source: hosted
version: "8.10.1"
version: "8.11.0"
cached_network_image:
dependency: "direct main"
description:
@ -181,18 +181,18 @@ packages:
dependency: transitive
description:
name: camera_android_camerax
sha256: "4b6c1bef4270c39df96402c4d62f2348c3bb2bbaefd0883b9dbd58f426306ad0"
sha256: "58b8fe843a3c83fd1273c00cb35f5a8ae507f6cc9b2029bcf7e2abba499e28d8"
url: "https://pub.dev"
source: hosted
version: "0.6.19"
version: "0.6.19+1"
camera_avfoundation:
dependency: transitive
description:
name: camera_avfoundation
sha256: "14d7698b26e95a7db371bee1b07358245e5100640ab5e07c036be23f51383d43"
sha256: "04e1f052ef268085a4f0550389211cc46005a9af015e444c7b1ee7aa19332e5d"
url: "https://pub.dev"
source: hosted
version: "0.9.20+2"
version: "0.9.20+6"
camera_platform_interface:
dependency: transitive
description:
@ -333,10 +333,10 @@ packages:
dependency: transitive
description:
name: dart_style
sha256: "5b236382b47ee411741447c1f1e111459c941ea1b3f2b540dde54c210a3662af"
sha256: "8a0e5fba27e8ee025d2ffb4ee820b4e6e2cf5e4246a6b1a477eb66866947e0bb"
url: "https://pub.dev"
source: hosted
version: "3.1.0"
version: "3.1.1"
dbus:
dependency: transitive
description:
@ -373,26 +373,26 @@ packages:
dependency: "direct main"
description:
name: drift
sha256: e60c715f045dd33624fc533efb0075e057debec9f39e83843e518f488a0e21fb
sha256: "6aaea757f53bb035e8a3baedf3d1d53a79d6549a6c13d84f7546509da9372c7c"
url: "https://pub.dev"
source: hosted
version: "2.27.0"
version: "2.28.1"
drift_dev:
dependency: "direct dev"
description:
name: drift_dev
sha256: "7ad88b8982e753eadcdbc0ea7c7d30500598af733601428b5c9d264baf5106d6"
sha256: "2fc05ad458a7c562755bf0cae11178dfc58387a416829b78d4da5155a61465fd"
url: "https://pub.dev"
source: hosted
version: "2.27.0"
version: "2.28.1"
drift_flutter:
dependency: "direct main"
description:
name: drift_flutter
sha256: "0cadbf3b8733409a6cf61d18ba2e94e149df81df7de26f48ae0695b48fd71922"
sha256: ccfb42bc942e59f81500b16228df59cf8eb40d2fbd96637ff677df923350af7b
url: "https://pub.dev"
source: hosted
version: "0.2.4"
version: "0.2.5"
ed25519_edwards:
dependency: transitive
description:
@ -461,10 +461,10 @@ packages:
dependency: "direct main"
description:
name: firebase_core
sha256: c6e8a6bf883d8ddd0dec39be90872daca65beaa6f4cff0051ed3b16c56b82e9f
sha256: "7be63a3f841fc9663342f7f3a011a42aef6a61066943c90b1c434d79d5c995c5"
url: "https://pub.dev"
source: hosted
version: "3.15.1"
version: "3.15.2"
firebase_core_platform_interface:
dependency: transitive
description:
@ -485,26 +485,26 @@ packages:
dependency: "direct main"
description:
name: firebase_messaging
sha256: "0f3363f97672eb9f65609fa00ed2f62cc8ec93e7e2d4def99726f9165d3d8a73"
sha256: "60be38574f8b5658e2f22b7e311ff2064bea835c248424a383783464e8e02fcc"
url: "https://pub.dev"
source: hosted
version: "15.2.9"
version: "15.2.10"
firebase_messaging_platform_interface:
dependency: transitive
description:
name: firebase_messaging_platform_interface
sha256: "7a05ef119a14c5f6a9440d1e0223bcba20c8daf555450e119c4c477bf2c3baa9"
sha256: "685e1771b3d1f9c8502771ccc9f91485b376ffe16d553533f335b9183ea99754"
url: "https://pub.dev"
source: hosted
version: "4.6.9"
version: "4.6.10"
firebase_messaging_web:
dependency: transitive
description:
name: firebase_messaging_web
sha256: a4547f76da2a905190f899eb4d0150e1d0fd52206fce469d9f05ae15bb68b2c5
sha256: "0d1be17bc89ed3ff5001789c92df678b2e963a51b6fa2bdb467532cc9dbed390"
url: "https://pub.dev"
source: hosted
version: "3.10.9"
version: "3.10.10"
fixnum:
dependency: "direct main"
description:
@ -642,10 +642,10 @@ packages:
dependency: "direct main"
description:
name: flutter_local_notifications
sha256: edae0c34573233ab03f5ba1f07465e55c384743893042cb19e010b4ee8541c12
sha256: "20ca0a9c82ce0c855ac62a2e580ab867f3fbea82680a90647f7953832d0850ae"
url: "https://pub.dev"
source: hosted
version: "19.3.0"
version: "19.4.0"
flutter_local_notifications_linux:
dependency: transitive
description:
@ -666,10 +666,10 @@ packages:
dependency: transitive
description:
name: flutter_local_notifications_windows
sha256: "593625e6833c0def4853b361c5276464b314983c6c819178bf0fa5aba2540d86"
sha256: ed46d7ae4ec9d19e4c8fa2badac5fe27ba87a3fe387343ce726f927af074ec98
url: "https://pub.dev"
source: hosted
version: "1.0.1"
version: "1.0.2"
flutter_localizations:
dependency: "direct main"
description: flutter
@ -759,10 +759,10 @@ packages:
dependency: "direct main"
description:
name: font_awesome_flutter
sha256: d3a89184101baec7f4600d58840a764d2ef760fe1c5a20ef9e6b0e9b24a07a3a
sha256: f50ce90dbe26d977415b9540400d6778bef00894aced6358ae578abd92b14b10
url: "https://pub.dev"
source: hosted
version: "10.8.0"
version: "10.9.0"
frontend_server_client:
dependency: transitive
description:
@ -775,10 +775,10 @@ packages:
dependency: "direct main"
description:
name: gal
sha256: "2771519c8b29f784d5e27f4efc2667667eef51c6c47cccaa0435a8fe8aa208e4"
sha256: "969598f986789127fd407a750413249e1352116d4c2be66e81837ffeeaafdfee"
url: "https://pub.dev"
source: hosted
version: "2.3.1"
version: "2.3.2"
get:
dependency: "direct main"
description:
@ -815,10 +815,10 @@ packages:
dependency: "direct main"
description:
name: hashlib
sha256: c742f4250067e52686e2bbc73013794e748511473baa7f875289681436daa4ed
sha256: "145889c76c9530481e90b4b97c02ad817b637f25dadcb5795988f5aa0800f173"
url: "https://pub.dev"
source: hosted
version: "2.0.0"
version: "2.1.0"
hashlib_codecs:
dependency: transitive
description:
@ -879,10 +879,10 @@ packages:
dependency: transitive
description:
name: image_picker_android
sha256: "317a5d961cec5b34e777b9252393f2afbd23084aa6e60fcf601dcf6341b9ebeb"
sha256: "6fae381e6af2bbe0365a5e4ce1db3959462fa0c4d234facf070746024bb80c8d"
url: "https://pub.dev"
source: hosted
version: "0.8.12+23"
version: "0.8.12+24"
image_picker_for_web:
dependency: transitive
description:
@ -975,10 +975,10 @@ packages:
dependency: "direct dev"
description:
name: json_serializable
sha256: c50ef5fc083d5b5e12eef489503ba3bf5ccc899e487d691584699b4bdefeea8c
sha256: ce2cf974ccdee13be2a510832d7fba0b94b364e0b0395dee42abaa51b855be27
url: "https://pub.dev"
source: hosted
version: "6.9.5"
version: "6.10.0"
leak_tracker:
dependency: transitive
description:
@ -1031,10 +1031,10 @@ packages:
dependency: transitive
description:
name: local_auth_android
sha256: "63ad7ca6396290626dc0cb34725a939e4cfe965d80d36112f08d49cf13a8136e"
sha256: "82b2bdeee2199a510d3b7716121e96a6609da86693bb0863edd8566355406b79"
url: "https://pub.dev"
source: hosted
version: "1.0.49"
version: "1.0.50"
local_auth_darwin:
dependency: transitive
description:
@ -1358,10 +1358,10 @@ packages:
dependency: "direct main"
description:
name: protobuf
sha256: "579fe5557eae58e3adca2e999e38f02441d8aa908703854a9e0a0f47fa857731"
sha256: "6153efcc92a06910918f3db8231fd2cf828ac81e50ebd87adc8f8a8cb3caff0e"
url: "https://pub.dev"
source: hosted
version: "4.1.0"
version: "4.1.1"
provider:
dependency: "direct main"
description:
@ -1523,10 +1523,10 @@ packages:
dependency: transitive
description:
name: source_gen
sha256: "35c8150ece9e8c8d263337a265153c3329667640850b9304861faea59fc98f6b"
sha256: fc787b1f89ceac9580c3616f899c9a447413cbdac1df071302127764c023a134
url: "https://pub.dev"
source: hosted
version: "2.0.0"
version: "3.0.0"
source_helper:
dependency: transitive
description:
@ -1571,10 +1571,10 @@ packages:
dependency: transitive
description:
name: sqflite_common
sha256: "84731e8bfd8303a3389903e01fb2141b6e59b5973cacbb0929021df08dddbe8b"
sha256: "6ef422a4525ecc601db6c0a2233ff448c731307906e92cabc9ba292afaae16a6"
url: "https://pub.dev"
source: hosted
version: "2.5.5"
version: "2.5.6"
sqflite_darwin:
dependency: transitive
description:
@ -1595,26 +1595,26 @@ packages:
dependency: transitive
description:
name: sqlite3
sha256: c0503c69b44d5714e6abbf4c1f51a3c3cc42b75ce785f44404765e4635481d38
sha256: dd806fff004a0aeb01e208b858dbc649bc72104670d425a81a6dd17698535f6e
url: "https://pub.dev"
source: hosted
version: "2.7.6"
version: "2.8.0"
sqlite3_flutter_libs:
dependency: transitive
description:
name: sqlite3_flutter_libs
sha256: e07232b998755fe795655c56d1f5426e0190c9c435e1752d39e7b1cd33699c71
sha256: fd996da5515a73aacd0a04ae7063db5fe8df42670d974df4c3ee538c652eef2e
url: "https://pub.dev"
source: hosted
version: "0.5.34"
version: "0.5.38"
sqlparser:
dependency: transitive
description:
name: sqlparser
sha256: "27dd0a9f0c02e22ac0eb42a23df9ea079ce69b52bb4a3b478d64e0ef34a263ee"
sha256: "7c859c803cf7e9a84d6db918bac824545045692bbe94a6386bd3a45132235d09"
url: "https://pub.dev"
source: hosted
version: "0.41.0"
version: "0.41.1"
stack_trace:
dependency: transitive
description:
@ -1691,10 +1691,10 @@ packages:
dependency: "direct main"
description:
name: tutorial_coach_mark
sha256: "9cdb721165d1cfb6e9b1910a1af1b3570fa6caa5059cf1506fcbd00bf7102abf"
sha256: ccc4a2026d361d8a71011d0f131a2278add1a154ef43e33dfd165babbb551c17
url: "https://pub.dev"
source: hosted
version: "1.3.0"
version: "1.3.1"
typed_data:
dependency: transitive
description:
@ -1835,18 +1835,18 @@ packages:
dependency: transitive
description:
name: video_player_android
sha256: "4a5135754a62dbc827a64a42ef1f8ed72c962e191c97e2d48744225c2b9ebb73"
sha256: "0fabf59eea728a6a887f29f2818eafbefb4b37c727dbb62dccef56c9287a692f"
url: "https://pub.dev"
source: hosted
version: "2.8.7"
version: "2.8.10"
video_player_avfoundation:
dependency: transitive
description:
name: video_player_avfoundation
sha256: "9fedd55023249f3a02738c195c906b4e530956191febf0838e37d0dac912f953"
sha256: "509ef9cfe7a3379783ccf306d45f5b5fc9db747401f956ce31c963417019e48e"
url: "https://pub.dev"
source: hosted
version: "2.8.0"
version: "2.8.2"
video_player_platform_interface:
dependency: transitive
description:

View file

@ -3,7 +3,7 @@ description: "twonly, a privacy-friendly way to connect with friends through sec
publish_to: 'none'
version: 0.0.59+59
version: 0.0.60+60
environment:
sdk: ^3.6.0

View file

@ -18,6 +18,7 @@ import 'schema_v12.dart' as v12;
import 'schema_v13.dart' as v13;
import 'schema_v14.dart' as v14;
import 'schema_v15.dart' as v15;
import 'schema_v16.dart' as v16;
class GeneratedHelper implements SchemaInstantiationHelper {
@override
@ -53,6 +54,8 @@ class GeneratedHelper implements SchemaInstantiationHelper {
return v14.DatabaseAtV14(db);
case 15:
return v15.DatabaseAtV15(db);
case 16:
return v16.DatabaseAtV16(db);
default:
throw MissingSchemaException(version, versions);
}
@ -73,6 +76,7 @@ class GeneratedHelper implements SchemaInstantiationHelper {
12,
13,
14,
15
15,
16
];
}

File diff suppressed because it is too large Load diff