custom avatar

This commit is contained in:
otsmr 2025-03-22 11:24:59 +01:00
parent 0de6d40aa4
commit ec095fea9c
26 changed files with 374 additions and 66 deletions

View file

@ -46,8 +46,29 @@ end
post_install do |installer| post_install do |installer|
installer.pods_project.targets.each do |target| installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target) flutter_additional_ios_build_settings(target)
target.build_configurations.each do |config| target.build_configurations.each do |config|
config.build_settings['CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES'] = 'YES' config.build_settings['CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES'] = 'YES'
# You can remove unused permissions here
# for more information: https://github.com/Baseflow/flutter-permission-handler/blob/main/permission_handler_apple/ios/Classes/PermissionHandlerEnums.h
# e.g. when you don't need camera permission, just add 'PERMISSION_CAMERA=0'
config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [
'$(inherited)',
## dart: PermissionGroup.camera
'PERMISSION_CAMERA=1',
## dart: PermissionGroup.microphone
'PERMISSION_MICROPHONE=1',
## dart: PermissionGroup.notification
'PERMISSION_NOTIFICATIONS=1',
## dart: PermissionGroup.mediaLibrary
'PERMISSION_PHOTOS=1',
]
end end
end end
end end

View file

@ -339,6 +339,6 @@ SPEC CHECKSUMS:
sqlite3_flutter_libs: f6acaa2172e6bb3e2e70c771661905080e8ebcf2 sqlite3_flutter_libs: f6acaa2172e6bb3e2e70c771661905080e8ebcf2
url_launcher_ios: 694010445543906933d732453a59da0a173ae33d url_launcher_ios: 694010445543906933d732453a59da0a173ae33d
PODFILE CHECKSUM: 84a7d8d37f41d292cbd8505494629ed779242400 PODFILE CHECKSUM: eda8ac661dab0c3d1e1b175d40ebbf2becd0ce86
COCOAPODS: 1.16.2 COCOAPODS: 1.16.2

View file

@ -45,10 +45,13 @@
<true/> <true/>
<key>UIApplicationSupportsIndirectInputEvents</key> <key>UIApplicationSupportsIndirectInputEvents</key>
<true/> <true/>
<key>NSCameraUsageDescription</key> <key>NSCameraUsageDescription</key>
<string>To create photos that can be shared.</string> <string>To create photos that can be shared.</string>
<key>NSMicrophoneUsageDescription</key> <key>NSMicrophoneUsageDescription</key>
<string>To create videos that can be securely shared.</string> <string>To create videos that can be securely shared.</string>
<key>NSPhotoLibraryAddUsageDescription</key>
<string>Store photos in the gallery.</string>
<key>NSFaceIDUsageDescription</key> <key>NSFaceIDUsageDescription</key>
<string>To protect others twonlies!</string> <string>To protect others twonlies!</string>
</dict> </dict>

View file

@ -115,8 +115,8 @@ class UserCheckbox extends StatelessWidget {
), ),
child: Row( child: Row(
children: [ children: [
InitialsAvatar( ContactAvatar(
displayName, contact: user,
fontSize: 12, fontSize: 12,
), ),
SizedBox(width: 8), SizedBox(width: 8),

View file

@ -1,13 +1,66 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:twonly/src/database/tables/contacts_table.dart';
import 'package:twonly/src/database/twonly_database.dart';
import 'package:twonly/src/json_models/userdata.dart';
class InitialsAvatar extends StatelessWidget { class ContactAvatar extends StatelessWidget {
final String displayName; final Contact? contact;
final UserData? userData;
final double? fontSize; final double? fontSize;
const InitialsAvatar(this.displayName, {super.key, this.fontSize = 20}); const ContactAvatar(
{super.key, this.contact, this.userData, this.fontSize = 20});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
String displayName = "";
String? avatarSvg;
if (contact != null) {
displayName = getContactDisplayName(contact!);
avatarSvg = contact!.avatarSvg;
} else if (userData != null) {
displayName = userData!.displayName;
avatarSvg = userData!.avatarSvg;
} else {
return Container();
}
double 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.0),
child: SizedBox(
height: proSize,
width: proSize,
child: Center(
// child: Container(
// color: Colors.green,
// ),
child: SvgPicture.string(
avatarSvg,
errorBuilder: (context, error, stackTrace) {
print("Error: $error");
return Container();
},
),
),
),
),
),
);
}
// Extract initials from the displayName // Extract initials from the displayName
List<String> nameParts = displayName.split(' '); List<String> nameParts = displayName.split(' ');
String initials = nameParts.map((part) => part[0]).join().toUpperCase(); String initials = nameParts.map((part) => part[0]).join().toUpperCase();
@ -35,8 +88,6 @@ class InitialsAvatar extends StatelessWidget {
bool isPro = initials[0] == "T"; bool isPro = initials[0] == "T";
double proSize = (fontSize == null) ? 40 : (fontSize! * 2);
return isPro return isPro
? //or 15.0 ? //or 15.0
Container( Container(
@ -60,7 +111,10 @@ class InitialsAvatar extends StatelessWidget {
), ),
) )
: CircleAvatar( : CircleAvatar(
backgroundColor: avatarColor, radius: fontSize, child: child); backgroundColor: avatarColor,
radius: fontSize,
child: child,
);
} }
Color _getTextColor(Color color) { Color _getTextColor(Color color) {

View file

@ -122,6 +122,13 @@ class ContactsDao extends DatabaseAccessor<TwonlyDatabase>
.watch(); .watch();
} }
Future<List<Contact>> getAllNotBlockedContacts() {
return (select(contacts)
..where((t) => t.accepted.equals(true) & t.blocked.equals(false))
..orderBy([(t) => OrderingTerm.desc(t.lastMessageExchange)]))
.get();
}
Stream<int?> watchContactsBlocked() { Stream<int?> watchContactsBlocked() {
final count = contacts.blocked.count(distinct: true); final count = contacts.blocked.count(distinct: true);
final query = selectOnly(contacts)..where(contacts.blocked.equals(true)); final query = selectOnly(contacts)..where(contacts.blocked.equals(true));

View file

@ -7,6 +7,9 @@ class Contacts extends Table {
TextColumn get username => text().unique()(); TextColumn get username => text().unique()();
TextColumn get displayName => text().nullable()(); TextColumn get displayName => text().nullable()();
TextColumn get nickName => text().nullable()(); TextColumn get nickName => text().nullable()();
TextColumn get avatarSvg => text().nullable()();
IntColumn get myAvatarCounter => integer().withDefault(Constant(0))();
BoolColumn get accepted => boolean().withDefault(Constant(false))(); BoolColumn get accepted => boolean().withDefault(Constant(false))();
BoolColumn get requested => boolean().withDefault(Constant(false))(); BoolColumn get requested => boolean().withDefault(Constant(false))();

View file

@ -33,6 +33,20 @@ class $ContactsTable extends Contacts with TableInfo<$ContactsTable, Contact> {
late final GeneratedColumn<String> nickName = GeneratedColumn<String>( late final GeneratedColumn<String> nickName = GeneratedColumn<String>(
'nick_name', aliasedName, true, 'nick_name', aliasedName, true,
type: DriftSqlType.string, requiredDuringInsert: false); type: DriftSqlType.string, requiredDuringInsert: false);
static const VerificationMeta _avatarSvgMeta =
const VerificationMeta('avatarSvg');
@override
late final GeneratedColumn<String> avatarSvg = GeneratedColumn<String>(
'avatar_svg', aliasedName, true,
type: DriftSqlType.string, requiredDuringInsert: false);
static const VerificationMeta _myAvatarCounterMeta =
const VerificationMeta('myAvatarCounter');
@override
late final GeneratedColumn<int> myAvatarCounter = GeneratedColumn<int>(
'my_avatar_counter', aliasedName, false,
type: DriftSqlType.int,
requiredDuringInsert: false,
defaultValue: Constant(0));
static const VerificationMeta _acceptedMeta = static const VerificationMeta _acceptedMeta =
const VerificationMeta('accepted'); const VerificationMeta('accepted');
@override @override
@ -129,6 +143,8 @@ class $ContactsTable extends Contacts with TableInfo<$ContactsTable, Contact> {
username, username,
displayName, displayName,
nickName, nickName,
avatarSvg,
myAvatarCounter,
accepted, accepted,
requested, requested,
blocked, blocked,
@ -171,6 +187,16 @@ class $ContactsTable extends Contacts with TableInfo<$ContactsTable, Contact> {
context.handle(_nickNameMeta, context.handle(_nickNameMeta,
nickName.isAcceptableOrUnknown(data['nick_name']!, _nickNameMeta)); nickName.isAcceptableOrUnknown(data['nick_name']!, _nickNameMeta));
} }
if (data.containsKey('avatar_svg')) {
context.handle(_avatarSvgMeta,
avatarSvg.isAcceptableOrUnknown(data['avatar_svg']!, _avatarSvgMeta));
}
if (data.containsKey('my_avatar_counter')) {
context.handle(
_myAvatarCounterMeta,
myAvatarCounter.isAcceptableOrUnknown(
data['my_avatar_counter']!, _myAvatarCounterMeta));
}
if (data.containsKey('accepted')) { if (data.containsKey('accepted')) {
context.handle(_acceptedMeta, context.handle(_acceptedMeta,
accepted.isAcceptableOrUnknown(data['accepted']!, _acceptedMeta)); accepted.isAcceptableOrUnknown(data['accepted']!, _acceptedMeta));
@ -244,6 +270,10 @@ class $ContactsTable extends Contacts with TableInfo<$ContactsTable, Contact> {
.read(DriftSqlType.string, data['${effectivePrefix}display_name']), .read(DriftSqlType.string, data['${effectivePrefix}display_name']),
nickName: attachedDatabase.typeMapping nickName: attachedDatabase.typeMapping
.read(DriftSqlType.string, data['${effectivePrefix}nick_name']), .read(DriftSqlType.string, data['${effectivePrefix}nick_name']),
avatarSvg: attachedDatabase.typeMapping
.read(DriftSqlType.string, data['${effectivePrefix}avatar_svg']),
myAvatarCounter: attachedDatabase.typeMapping
.read(DriftSqlType.int, data['${effectivePrefix}my_avatar_counter'])!,
accepted: attachedDatabase.typeMapping accepted: attachedDatabase.typeMapping
.read(DriftSqlType.bool, data['${effectivePrefix}accepted'])!, .read(DriftSqlType.bool, data['${effectivePrefix}accepted'])!,
requested: attachedDatabase.typeMapping requested: attachedDatabase.typeMapping
@ -283,6 +313,8 @@ class Contact extends DataClass implements Insertable<Contact> {
final String username; final String username;
final String? displayName; final String? displayName;
final String? nickName; final String? nickName;
final String? avatarSvg;
final int myAvatarCounter;
final bool accepted; final bool accepted;
final bool requested; final bool requested;
final bool blocked; final bool blocked;
@ -299,6 +331,8 @@ class Contact extends DataClass implements Insertable<Contact> {
required this.username, required this.username,
this.displayName, this.displayName,
this.nickName, this.nickName,
this.avatarSvg,
required this.myAvatarCounter,
required this.accepted, required this.accepted,
required this.requested, required this.requested,
required this.blocked, required this.blocked,
@ -321,6 +355,10 @@ class Contact extends DataClass implements Insertable<Contact> {
if (!nullToAbsent || nickName != null) { if (!nullToAbsent || nickName != null) {
map['nick_name'] = Variable<String>(nickName); map['nick_name'] = Variable<String>(nickName);
} }
if (!nullToAbsent || avatarSvg != null) {
map['avatar_svg'] = Variable<String>(avatarSvg);
}
map['my_avatar_counter'] = Variable<int>(myAvatarCounter);
map['accepted'] = Variable<bool>(accepted); map['accepted'] = Variable<bool>(accepted);
map['requested'] = Variable<bool>(requested); map['requested'] = Variable<bool>(requested);
map['blocked'] = Variable<bool>(blocked); map['blocked'] = Variable<bool>(blocked);
@ -352,6 +390,10 @@ class Contact extends DataClass implements Insertable<Contact> {
nickName: nickName == null && nullToAbsent nickName: nickName == null && nullToAbsent
? const Value.absent() ? const Value.absent()
: Value(nickName), : Value(nickName),
avatarSvg: avatarSvg == null && nullToAbsent
? const Value.absent()
: Value(avatarSvg),
myAvatarCounter: Value(myAvatarCounter),
accepted: Value(accepted), accepted: Value(accepted),
requested: Value(requested), requested: Value(requested),
blocked: Value(blocked), blocked: Value(blocked),
@ -380,6 +422,8 @@ class Contact extends DataClass implements Insertable<Contact> {
username: serializer.fromJson<String>(json['username']), username: serializer.fromJson<String>(json['username']),
displayName: serializer.fromJson<String?>(json['displayName']), displayName: serializer.fromJson<String?>(json['displayName']),
nickName: serializer.fromJson<String?>(json['nickName']), nickName: serializer.fromJson<String?>(json['nickName']),
avatarSvg: serializer.fromJson<String?>(json['avatarSvg']),
myAvatarCounter: serializer.fromJson<int>(json['myAvatarCounter']),
accepted: serializer.fromJson<bool>(json['accepted']), accepted: serializer.fromJson<bool>(json['accepted']),
requested: serializer.fromJson<bool>(json['requested']), requested: serializer.fromJson<bool>(json['requested']),
blocked: serializer.fromJson<bool>(json['blocked']), blocked: serializer.fromJson<bool>(json['blocked']),
@ -404,6 +448,8 @@ class Contact extends DataClass implements Insertable<Contact> {
'username': serializer.toJson<String>(username), 'username': serializer.toJson<String>(username),
'displayName': serializer.toJson<String?>(displayName), 'displayName': serializer.toJson<String?>(displayName),
'nickName': serializer.toJson<String?>(nickName), 'nickName': serializer.toJson<String?>(nickName),
'avatarSvg': serializer.toJson<String?>(avatarSvg),
'myAvatarCounter': serializer.toJson<int>(myAvatarCounter),
'accepted': serializer.toJson<bool>(accepted), 'accepted': serializer.toJson<bool>(accepted),
'requested': serializer.toJson<bool>(requested), 'requested': serializer.toJson<bool>(requested),
'blocked': serializer.toJson<bool>(blocked), 'blocked': serializer.toJson<bool>(blocked),
@ -424,6 +470,8 @@ class Contact extends DataClass implements Insertable<Contact> {
String? username, String? username,
Value<String?> displayName = const Value.absent(), Value<String?> displayName = const Value.absent(),
Value<String?> nickName = const Value.absent(), Value<String?> nickName = const Value.absent(),
Value<String?> avatarSvg = const Value.absent(),
int? myAvatarCounter,
bool? accepted, bool? accepted,
bool? requested, bool? requested,
bool? blocked, bool? blocked,
@ -440,6 +488,8 @@ class Contact extends DataClass implements Insertable<Contact> {
username: username ?? this.username, username: username ?? this.username,
displayName: displayName.present ? displayName.value : this.displayName, displayName: displayName.present ? displayName.value : this.displayName,
nickName: nickName.present ? nickName.value : this.nickName, nickName: nickName.present ? nickName.value : this.nickName,
avatarSvg: avatarSvg.present ? avatarSvg.value : this.avatarSvg,
myAvatarCounter: myAvatarCounter ?? this.myAvatarCounter,
accepted: accepted ?? this.accepted, accepted: accepted ?? this.accepted,
requested: requested ?? this.requested, requested: requested ?? this.requested,
blocked: blocked ?? this.blocked, blocked: blocked ?? this.blocked,
@ -465,6 +515,10 @@ class Contact extends DataClass implements Insertable<Contact> {
displayName: displayName:
data.displayName.present ? data.displayName.value : this.displayName, data.displayName.present ? data.displayName.value : this.displayName,
nickName: data.nickName.present ? data.nickName.value : this.nickName, nickName: data.nickName.present ? data.nickName.value : this.nickName,
avatarSvg: data.avatarSvg.present ? data.avatarSvg.value : this.avatarSvg,
myAvatarCounter: data.myAvatarCounter.present
? data.myAvatarCounter.value
: this.myAvatarCounter,
accepted: data.accepted.present ? data.accepted.value : this.accepted, accepted: data.accepted.present ? data.accepted.value : this.accepted,
requested: data.requested.present ? data.requested.value : this.requested, requested: data.requested.present ? data.requested.value : this.requested,
blocked: data.blocked.present ? data.blocked.value : this.blocked, blocked: data.blocked.present ? data.blocked.value : this.blocked,
@ -498,6 +552,8 @@ class Contact extends DataClass implements Insertable<Contact> {
..write('username: $username, ') ..write('username: $username, ')
..write('displayName: $displayName, ') ..write('displayName: $displayName, ')
..write('nickName: $nickName, ') ..write('nickName: $nickName, ')
..write('avatarSvg: $avatarSvg, ')
..write('myAvatarCounter: $myAvatarCounter, ')
..write('accepted: $accepted, ') ..write('accepted: $accepted, ')
..write('requested: $requested, ') ..write('requested: $requested, ')
..write('blocked: $blocked, ') ..write('blocked: $blocked, ')
@ -519,6 +575,8 @@ class Contact extends DataClass implements Insertable<Contact> {
username, username,
displayName, displayName,
nickName, nickName,
avatarSvg,
myAvatarCounter,
accepted, accepted,
requested, requested,
blocked, blocked,
@ -538,6 +596,8 @@ class Contact extends DataClass implements Insertable<Contact> {
other.username == this.username && other.username == this.username &&
other.displayName == this.displayName && other.displayName == this.displayName &&
other.nickName == this.nickName && other.nickName == this.nickName &&
other.avatarSvg == this.avatarSvg &&
other.myAvatarCounter == this.myAvatarCounter &&
other.accepted == this.accepted && other.accepted == this.accepted &&
other.requested == this.requested && other.requested == this.requested &&
other.blocked == this.blocked && other.blocked == this.blocked &&
@ -556,6 +616,8 @@ class ContactsCompanion extends UpdateCompanion<Contact> {
final Value<String> username; final Value<String> username;
final Value<String?> displayName; final Value<String?> displayName;
final Value<String?> nickName; final Value<String?> nickName;
final Value<String?> avatarSvg;
final Value<int> myAvatarCounter;
final Value<bool> accepted; final Value<bool> accepted;
final Value<bool> requested; final Value<bool> requested;
final Value<bool> blocked; final Value<bool> blocked;
@ -572,6 +634,8 @@ class ContactsCompanion extends UpdateCompanion<Contact> {
this.username = const Value.absent(), this.username = const Value.absent(),
this.displayName = const Value.absent(), this.displayName = const Value.absent(),
this.nickName = const Value.absent(), this.nickName = const Value.absent(),
this.avatarSvg = const Value.absent(),
this.myAvatarCounter = const Value.absent(),
this.accepted = const Value.absent(), this.accepted = const Value.absent(),
this.requested = const Value.absent(), this.requested = const Value.absent(),
this.blocked = const Value.absent(), this.blocked = const Value.absent(),
@ -589,6 +653,8 @@ class ContactsCompanion extends UpdateCompanion<Contact> {
required String username, required String username,
this.displayName = const Value.absent(), this.displayName = const Value.absent(),
this.nickName = const Value.absent(), this.nickName = const Value.absent(),
this.avatarSvg = const Value.absent(),
this.myAvatarCounter = const Value.absent(),
this.accepted = const Value.absent(), this.accepted = const Value.absent(),
this.requested = const Value.absent(), this.requested = const Value.absent(),
this.blocked = const Value.absent(), this.blocked = const Value.absent(),
@ -606,6 +672,8 @@ class ContactsCompanion extends UpdateCompanion<Contact> {
Expression<String>? username, Expression<String>? username,
Expression<String>? displayName, Expression<String>? displayName,
Expression<String>? nickName, Expression<String>? nickName,
Expression<String>? avatarSvg,
Expression<int>? myAvatarCounter,
Expression<bool>? accepted, Expression<bool>? accepted,
Expression<bool>? requested, Expression<bool>? requested,
Expression<bool>? blocked, Expression<bool>? blocked,
@ -623,6 +691,8 @@ class ContactsCompanion extends UpdateCompanion<Contact> {
if (username != null) 'username': username, if (username != null) 'username': username,
if (displayName != null) 'display_name': displayName, if (displayName != null) 'display_name': displayName,
if (nickName != null) 'nick_name': nickName, if (nickName != null) 'nick_name': nickName,
if (avatarSvg != null) 'avatar_svg': avatarSvg,
if (myAvatarCounter != null) 'my_avatar_counter': myAvatarCounter,
if (accepted != null) 'accepted': accepted, if (accepted != null) 'accepted': accepted,
if (requested != null) 'requested': requested, if (requested != null) 'requested': requested,
if (blocked != null) 'blocked': blocked, if (blocked != null) 'blocked': blocked,
@ -645,6 +715,8 @@ class ContactsCompanion extends UpdateCompanion<Contact> {
Value<String>? username, Value<String>? username,
Value<String?>? displayName, Value<String?>? displayName,
Value<String?>? nickName, Value<String?>? nickName,
Value<String?>? avatarSvg,
Value<int>? myAvatarCounter,
Value<bool>? accepted, Value<bool>? accepted,
Value<bool>? requested, Value<bool>? requested,
Value<bool>? blocked, Value<bool>? blocked,
@ -661,6 +733,8 @@ class ContactsCompanion extends UpdateCompanion<Contact> {
username: username ?? this.username, username: username ?? this.username,
displayName: displayName ?? this.displayName, displayName: displayName ?? this.displayName,
nickName: nickName ?? this.nickName, nickName: nickName ?? this.nickName,
avatarSvg: avatarSvg ?? this.avatarSvg,
myAvatarCounter: myAvatarCounter ?? this.myAvatarCounter,
accepted: accepted ?? this.accepted, accepted: accepted ?? this.accepted,
requested: requested ?? this.requested, requested: requested ?? this.requested,
blocked: blocked ?? this.blocked, blocked: blocked ?? this.blocked,
@ -691,6 +765,12 @@ class ContactsCompanion extends UpdateCompanion<Contact> {
if (nickName.present) { if (nickName.present) {
map['nick_name'] = Variable<String>(nickName.value); map['nick_name'] = Variable<String>(nickName.value);
} }
if (avatarSvg.present) {
map['avatar_svg'] = Variable<String>(avatarSvg.value);
}
if (myAvatarCounter.present) {
map['my_avatar_counter'] = Variable<int>(myAvatarCounter.value);
}
if (accepted.present) { if (accepted.present) {
map['accepted'] = Variable<bool>(accepted.value); map['accepted'] = Variable<bool>(accepted.value);
} }
@ -737,6 +817,8 @@ class ContactsCompanion extends UpdateCompanion<Contact> {
..write('username: $username, ') ..write('username: $username, ')
..write('displayName: $displayName, ') ..write('displayName: $displayName, ')
..write('nickName: $nickName, ') ..write('nickName: $nickName, ')
..write('avatarSvg: $avatarSvg, ')
..write('myAvatarCounter: $myAvatarCounter, ')
..write('accepted: $accepted, ') ..write('accepted: $accepted, ')
..write('requested: $requested, ') ..write('requested: $requested, ')
..write('blocked: $blocked, ') ..write('blocked: $blocked, ')
@ -805,8 +887,6 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> {
defaultConstraints: GeneratedColumn.constraintIsAlways( defaultConstraints: GeneratedColumn.constraintIsAlways(
'CHECK ("acknowledge_by_user" IN (0, 1))'), 'CHECK ("acknowledge_by_user" IN (0, 1))'),
defaultValue: Constant(false)); defaultValue: Constant(false));
static const VerificationMeta _downloadStateMeta =
const VerificationMeta('downloadState');
@override @override
late final GeneratedColumnWithTypeConverter<DownloadState, int> late final GeneratedColumnWithTypeConverter<DownloadState, int>
downloadState = GeneratedColumn<int>('download_state', aliasedName, false, downloadState = GeneratedColumn<int>('download_state', aliasedName, false,
@ -824,7 +904,6 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> {
defaultConstraints: GeneratedColumn.constraintIsAlways( defaultConstraints: GeneratedColumn.constraintIsAlways(
'CHECK ("acknowledge_by_server" IN (0, 1))'), 'CHECK ("acknowledge_by_server" IN (0, 1))'),
defaultValue: Constant(false)); defaultValue: Constant(false));
static const VerificationMeta _kindMeta = const VerificationMeta('kind');
@override @override
late final GeneratedColumnWithTypeConverter<MessageKind, String> kind = late final GeneratedColumnWithTypeConverter<MessageKind, String> kind =
GeneratedColumn<String>('kind', aliasedName, false, GeneratedColumn<String>('kind', aliasedName, false,
@ -918,14 +997,12 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> {
acknowledgeByUser.isAcceptableOrUnknown( acknowledgeByUser.isAcceptableOrUnknown(
data['acknowledge_by_user']!, _acknowledgeByUserMeta)); data['acknowledge_by_user']!, _acknowledgeByUserMeta));
} }
context.handle(_downloadStateMeta, const VerificationResult.success());
if (data.containsKey('acknowledge_by_server')) { if (data.containsKey('acknowledge_by_server')) {
context.handle( context.handle(
_acknowledgeByServerMeta, _acknowledgeByServerMeta,
acknowledgeByServer.isAcceptableOrUnknown( acknowledgeByServer.isAcceptableOrUnknown(
data['acknowledge_by_server']!, _acknowledgeByServerMeta)); data['acknowledge_by_server']!, _acknowledgeByServerMeta));
} }
context.handle(_kindMeta, const VerificationResult.success());
if (data.containsKey('content_json')) { if (data.containsKey('content_json')) {
context.handle( context.handle(
_contentJsonMeta, _contentJsonMeta,
@ -2448,6 +2525,8 @@ typedef $$ContactsTableCreateCompanionBuilder = ContactsCompanion Function({
required String username, required String username,
Value<String?> displayName, Value<String?> displayName,
Value<String?> nickName, Value<String?> nickName,
Value<String?> avatarSvg,
Value<int> myAvatarCounter,
Value<bool> accepted, Value<bool> accepted,
Value<bool> requested, Value<bool> requested,
Value<bool> blocked, Value<bool> blocked,
@ -2465,6 +2544,8 @@ typedef $$ContactsTableUpdateCompanionBuilder = ContactsCompanion Function({
Value<String> username, Value<String> username,
Value<String?> displayName, Value<String?> displayName,
Value<String?> nickName, Value<String?> nickName,
Value<String?> avatarSvg,
Value<int> myAvatarCounter,
Value<bool> accepted, Value<bool> accepted,
Value<bool> requested, Value<bool> requested,
Value<bool> blocked, Value<bool> blocked,
@ -2519,6 +2600,13 @@ class $$ContactsTableFilterComposer
ColumnFilters<String> get nickName => $composableBuilder( ColumnFilters<String> get nickName => $composableBuilder(
column: $table.nickName, builder: (column) => ColumnFilters(column)); column: $table.nickName, builder: (column) => ColumnFilters(column));
ColumnFilters<String> get avatarSvg => $composableBuilder(
column: $table.avatarSvg, builder: (column) => ColumnFilters(column));
ColumnFilters<int> get myAvatarCounter => $composableBuilder(
column: $table.myAvatarCounter,
builder: (column) => ColumnFilters(column));
ColumnFilters<bool> get accepted => $composableBuilder( ColumnFilters<bool> get accepted => $composableBuilder(
column: $table.accepted, builder: (column) => ColumnFilters(column)); column: $table.accepted, builder: (column) => ColumnFilters(column));
@ -2600,6 +2688,13 @@ class $$ContactsTableOrderingComposer
ColumnOrderings<String> get nickName => $composableBuilder( ColumnOrderings<String> get nickName => $composableBuilder(
column: $table.nickName, builder: (column) => ColumnOrderings(column)); column: $table.nickName, builder: (column) => ColumnOrderings(column));
ColumnOrderings<String> get avatarSvg => $composableBuilder(
column: $table.avatarSvg, builder: (column) => ColumnOrderings(column));
ColumnOrderings<int> get myAvatarCounter => $composableBuilder(
column: $table.myAvatarCounter,
builder: (column) => ColumnOrderings(column));
ColumnOrderings<bool> get accepted => $composableBuilder( ColumnOrderings<bool> get accepted => $composableBuilder(
column: $table.accepted, builder: (column) => ColumnOrderings(column)); column: $table.accepted, builder: (column) => ColumnOrderings(column));
@ -2661,6 +2756,12 @@ class $$ContactsTableAnnotationComposer
GeneratedColumn<String> get nickName => GeneratedColumn<String> get nickName =>
$composableBuilder(column: $table.nickName, builder: (column) => column); $composableBuilder(column: $table.nickName, builder: (column) => column);
GeneratedColumn<String> get avatarSvg =>
$composableBuilder(column: $table.avatarSvg, builder: (column) => column);
GeneratedColumn<int> get myAvatarCounter => $composableBuilder(
column: $table.myAvatarCounter, builder: (column) => column);
GeneratedColumn<bool> get accepted => GeneratedColumn<bool> get accepted =>
$composableBuilder(column: $table.accepted, builder: (column) => column); $composableBuilder(column: $table.accepted, builder: (column) => column);
@ -2743,6 +2844,8 @@ class $$ContactsTableTableManager extends RootTableManager<
Value<String> username = const Value.absent(), Value<String> username = const Value.absent(),
Value<String?> displayName = const Value.absent(), Value<String?> displayName = const Value.absent(),
Value<String?> nickName = const Value.absent(), Value<String?> nickName = const Value.absent(),
Value<String?> avatarSvg = const Value.absent(),
Value<int> myAvatarCounter = const Value.absent(),
Value<bool> accepted = const Value.absent(), Value<bool> accepted = const Value.absent(),
Value<bool> requested = const Value.absent(), Value<bool> requested = const Value.absent(),
Value<bool> blocked = const Value.absent(), Value<bool> blocked = const Value.absent(),
@ -2760,6 +2863,8 @@ class $$ContactsTableTableManager extends RootTableManager<
username: username, username: username,
displayName: displayName, displayName: displayName,
nickName: nickName, nickName: nickName,
avatarSvg: avatarSvg,
myAvatarCounter: myAvatarCounter,
accepted: accepted, accepted: accepted,
requested: requested, requested: requested,
blocked: blocked, blocked: blocked,
@ -2777,6 +2882,8 @@ class $$ContactsTableTableManager extends RootTableManager<
required String username, required String username,
Value<String?> displayName = const Value.absent(), Value<String?> displayName = const Value.absent(),
Value<String?> nickName = const Value.absent(), Value<String?> nickName = const Value.absent(),
Value<String?> avatarSvg = const Value.absent(),
Value<int> myAvatarCounter = const Value.absent(),
Value<bool> accepted = const Value.absent(), Value<bool> accepted = const Value.absent(),
Value<bool> requested = const Value.absent(), Value<bool> requested = const Value.absent(),
Value<bool> blocked = const Value.absent(), Value<bool> blocked = const Value.absent(),
@ -2794,6 +2901,8 @@ class $$ContactsTableTableManager extends RootTableManager<
username: username, username: username,
displayName: displayName, displayName: displayName,
nickName: nickName, nickName: nickName,
avatarSvg: avatarSvg,
myAvatarCounter: myAvatarCounter,
accepted: accepted, accepted: accepted,
requested: requested, requested: requested,
blocked: blocked, blocked: blocked,

View file

@ -4,6 +4,7 @@ enum MessageKind {
textMessage, textMessage,
media, media,
contactRequest, contactRequest,
avatarChange,
rejectRequest, rejectRequest,
acceptRequest, acceptRequest,
opened, opened,
@ -93,6 +94,8 @@ class MessageContent {
return MediaMessageContent.fromJson(json); return MediaMessageContent.fromJson(json);
case MessageKind.textMessage: case MessageKind.textMessage:
return TextMessageContent.fromJson(json); return TextMessageContent.fromJson(json);
case MessageKind.avatarChange:
return AvatarContent.fromJson(json);
default: default:
return null; return null;
} }
@ -160,15 +163,25 @@ class TextMessageContent extends MessageContent {
TextMessageContent({required this.text}); TextMessageContent({required this.text});
static TextMessageContent fromJson(Map json) { static TextMessageContent fromJson(Map json) {
return TextMessageContent( return TextMessageContent(text: json['text']);
text: json['text'],
);
} }
@override @override
Map toJson() { Map toJson() {
return { return {'text': text};
'text': text, }
}; }
class AvatarContent extends MessageContent {
String svg;
AvatarContent({required this.svg});
static AvatarContent fromJson(Map json) {
return AvatarContent(svg: json['svg']);
}
@override
Map toJson() {
return {'svg': svg};
} }
} }

View file

@ -1,14 +1,20 @@
import 'package:json_annotation/json_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
part 'user_data.g.dart'; part 'userdata.g.dart';
@JsonSerializable() @JsonSerializable()
class UserData { class UserData {
const UserData( UserData({
{required this.userId, required this.userId,
required this.username, required this.username,
required this.displayName}); required this.displayName,
final String username; });
final String displayName;
String username;
String displayName;
String? avatarSvg;
String? avatarJson;
int? avatarCounter;
final int userId; final int userId;

View file

@ -10,10 +10,16 @@ UserData _$UserDataFromJson(Map<String, dynamic> json) => UserData(
userId: (json['userId'] as num).toInt(), userId: (json['userId'] as num).toInt(),
username: json['username'] as String, username: json['username'] as String,
displayName: json['displayName'] as String, displayName: json['displayName'] as String,
); )
..avatarSvg = json['avatarSvg'] as String?
..avatarJson = json['avatarJson'] as String?
..avatarCounter = (json['avatarCounter'] as num?)?.toInt();
Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{ Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{
'username': instance.username, 'username': instance.username,
'displayName': instance.displayName, 'displayName': instance.displayName,
'avatarSvg': instance.avatarSvg,
'avatarJson': instance.avatarJson,
'avatarCounter': instance.avatarCounter,
'userId': instance.userId, 'userId': instance.userId,
}; };

View file

@ -6,11 +6,13 @@ import 'package:twonly/globals.dart';
import 'package:twonly/src/database/twonly_database.dart'; import 'package:twonly/src/database/twonly_database.dart';
import 'package:twonly/src/database/tables/messages_table.dart'; import 'package:twonly/src/database/tables/messages_table.dart';
import 'package:twonly/src/json_models/message.dart'; import 'package:twonly/src/json_models/message.dart';
import 'package:twonly/src/json_models/userdata.dart';
import 'package:twonly/src/proto/api/error.pb.dart'; import 'package:twonly/src/proto/api/error.pb.dart';
import 'package:twonly/src/providers/api/api_utils.dart'; import 'package:twonly/src/providers/api/api_utils.dart';
import 'package:twonly/src/providers/hive.dart'; import 'package:twonly/src/providers/hive.dart';
// ignore: library_prefixes // ignore: library_prefixes
import 'package:twonly/src/utils/signal.dart' as SignalHelper; import 'package:twonly/src/utils/signal.dart' as SignalHelper;
import 'package:twonly/src/utils/storage.dart';
Future tryTransmitMessages() async { Future tryTransmitMessages() async {
// List<Message> retransmit = // List<Message> retransmit =
@ -122,3 +124,27 @@ Future notifyContactAboutOpeningMessage(
), ),
); );
} }
Future notifyContactsAboutAvatarChange() async {
List<Contact> contacts =
await twonlyDatabase.contactsDao.getAllNotBlockedContacts();
UserData? user = await getUser();
if (user == null) return;
if (user.avatarCounter == null) return;
if (user.avatarSvg == null) return;
for (Contact contact in contacts) {
if (contact.myAvatarCounter < user.avatarCounter!) {
encryptAndSendMessage(
null,
contact.userId,
MessageJson(
kind: MessageKind.avatarChange,
content: AvatarContent(svg: user.avatarSvg!),
timestamp: DateTime.now(),
),
);
}
}
}

View file

@ -199,6 +199,14 @@ Future<client.Response> handleNewMessage(int fromUserId, Uint8List body) async {
final update = ContactsCompanion(accepted: Value(true)); final update = ContactsCompanion(accepted: Value(true));
twonlyDatabase.contactsDao.updateContact(fromUserId, update); twonlyDatabase.contactsDao.updateContact(fromUserId, update);
localPushNotificationNewMessage(fromUserId.toInt(), message, 8888888); localPushNotificationNewMessage(fromUserId.toInt(), message, 8888888);
notifyContactsAboutAvatarChange();
break;
case MessageKind.avatarChange:
var content = message.content;
if (content is AvatarContent) {
final update = ContactsCompanion(avatarSvg: Value(content.svg));
twonlyDatabase.contactsDao.updateContact(fromUserId, update);
}
break; break;
case MessageKind.ack: case MessageKind.ack:
final update = MessagesCompanion(acknowledgeByUser: Value(true)); final update = MessagesCompanion(acknowledgeByUser: Value(true));

View file

@ -75,6 +75,7 @@ class ApiProvider {
if (!globalIsAppInBackground) { if (!globalIsAppInBackground) {
tryTransmitMessages(); tryTransmitMessages();
tryDownloadAllMediaFiles(); tryDownloadAllMediaFiles();
notifyContactsAboutAvatarChange();
} }
} }

View file

@ -28,6 +28,11 @@ Future<UserData?> getUser() async {
} }
} }
Future updateUser(UserData userData) async {
final storage = getSecureStorage();
storage.write(key: "userData", value: jsonEncode(userData));
}
Future<bool> deleteLocalUserData() async { Future<bool> deleteLocalUserData() async {
final appDir = await getApplicationSupportDirectory(); final appDir = await getApplicationSupportDirectory();
if (appDir.existsSync()) { if (appDir.existsSync()) {

View file

@ -66,7 +66,14 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
} }
void selectCamera(int sCameraId, {bool init = false}) { void selectCamera(int sCameraId, {bool init = false}) {
if (sCameraId > gCameras.length) return; if (sCameraId >= gCameras.length) return;
if (init) {
for (; sCameraId < gCameras.length; sCameraId++) {
if (gCameras[sCameraId].lensDirection == CameraLensDirection.back) {
break;
}
}
}
setState(() { setState(() {
isZoomAble = false; isZoomAble = false;
}); });
@ -115,12 +122,19 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
@override @override
void dispose() { void dispose() {
if (cameraId < gCameras.length) {
controller.dispose(); controller.dispose();
}
super.dispose(); super.dispose();
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (cameraId >= gCameras.length) {
return Center(
child: Text("No camera found."),
);
}
return MediaViewSizing( return MediaViewSizing(
Stack( Stack(
children: [ children: [

View file

@ -303,8 +303,8 @@ class UserList extends StatelessWidget {
), ),
], ],
), ),
leading: InitialsAvatar( leading: ContactAvatar(
getContactDisplayName(user), contact: user,
fontSize: 15, fontSize: 15,
), ),
trailing: Checkbox( trailing: Checkbox(

View file

@ -214,8 +214,8 @@ class _ChatItemDetailsViewState extends State<ChatItemDetailsView> {
? Container() ? Container()
: Row( : Row(
children: [ children: [
InitialsAvatar( ContactAvatar(
getContactDisplayName(user!), contact: user!,
fontSize: 19, fontSize: 19,
), ),
SizedBox(width: 10), SizedBox(width: 10),

View file

@ -268,7 +268,7 @@ class _UserListItem extends State<UserListItem> {
); );
}, },
), ),
leading: InitialsAvatar(getContactDisplayName(widget.user)), leading: ContactAvatar(contact: widget.user),
onTap: () { onTap: () {
if (currentMessage == null) { if (currentMessage == null) {
context context

View file

@ -184,7 +184,7 @@ class _ContactsListViewState extends State<ContactsListView> {
final displayName = getContactDisplayName(contact); final displayName = getContactDisplayName(contact);
return ListTile( return ListTile(
title: Text(displayName), title: Text(displayName),
leading: InitialsAvatar(displayName), leading: ContactAvatar(contact: contact),
trailing: Row( trailing: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [

View file

@ -44,10 +44,7 @@ class _ContactViewState extends State<ContactView> {
children: [ children: [
Padding( Padding(
padding: const EdgeInsets.all(10), padding: const EdgeInsets.all(10),
child: InitialsAvatar( child: ContactAvatar(contact: contact, fontSize: 30),
getContactDisplayName(contact),
fontSize: 30,
),
), ),
Row( Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,

View file

@ -2,6 +2,9 @@ import 'dart:math';
import 'package:avatar_maker/avatar_maker.dart'; import 'package:avatar_maker/avatar_maker.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:twonly/src/json_models/userdata.dart';
import 'package:twonly/src/providers/api/api.dart';
import 'package:twonly/src/utils/storage.dart';
class AvatarCreator extends StatefulWidget { class AvatarCreator extends StatefulWidget {
const AvatarCreator({super.key}); const AvatarCreator({super.key});
@ -28,7 +31,7 @@ class _AvatarCreatorState extends State<AvatarCreator> {
height: 25, height: 25,
), ),
AvatarMakerAvatar( AvatarMakerAvatar(
backgroundColor: Colors.grey[200], backgroundColor: Colors.transparent,
radius: 100, radius: 100,
), ),
SizedBox( SizedBox(
@ -39,13 +42,17 @@ class _AvatarCreatorState extends State<AvatarCreator> {
Spacer(flex: 2), Spacer(flex: 2),
Expanded( Expanded(
flex: 3, flex: 3,
child: Container( child: SizedBox(
height: 35, height: 35,
child: ElevatedButton.icon( child: ElevatedButton.icon(
icon: Icon(Icons.edit), icon: Icon(Icons.edit),
label: Text("Customize"), label: Text("Customize"),
onPressed: () => Navigator.push(context, onPressed: () => Navigator.push(
new MaterialPageRoute(builder: (context) => NewPage())), context,
MaterialPageRoute(
builder: (context) => NewPage(),
),
),
), ),
), ),
), ),
@ -64,6 +71,21 @@ class _AvatarCreatorState extends State<AvatarCreator> {
class NewPage extends StatelessWidget { class NewPage extends StatelessWidget {
const NewPage({super.key}); const NewPage({super.key});
Future updateUserAvatar(String json, String svg) async {
UserData? user = await getUser();
if (user == null) return null;
user.avatarJson = json;
user.avatarSvg = svg;
if (user.avatarCounter == null) {
user.avatarCounter = 1;
} else {
user.avatarCounter = user.avatarCounter! + 1;
}
await updateUser(user);
await notifyContactsAboutAvatarChange();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var _width = MediaQuery.of(context).size.width; var _width = MediaQuery.of(context).size.width;
@ -75,9 +97,9 @@ class NewPage extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
Padding( Padding(
padding: const EdgeInsets.symmetric(vertical: 30), padding: const EdgeInsets.symmetric(vertical: 00),
child: AvatarMakerAvatar( child: AvatarMakerAvatar(
radius: 100, radius: 130,
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
), ),
), ),
@ -85,12 +107,18 @@ class NewPage extends StatelessWidget {
width: min(600, _width * 0.85), width: min(600, _width * 0.85),
child: Row( child: Row(
children: [ children: [
Text(
"Customize:",
style: Theme.of(context).textTheme.titleLarge,
),
Spacer(), Spacer(),
AvatarMakerSaveWidget(), AvatarMakerSaveWidget(
onTap: () async {
final json =
await AvatarMakerController.getJsonOptions();
final svg = await AvatarMakerController.getAvatarSVG();
await updateUserAvatar(json, svg);
if (context.mounted) {
Navigator.pop(context);
}
},
),
AvatarMakerRandomWidget(), AvatarMakerRandomWidget(),
AvatarMakerResetWidget(), AvatarMakerResetWidget(),
], ],
@ -106,12 +134,20 @@ class NewPage extends StatelessWidget {
boxDecoration: BoxDecoration( boxDecoration: BoxDecoration(
boxShadow: [BoxShadow()], boxShadow: [BoxShadow()],
), ),
// primaryBgColor: unselectedTileDecoration: BoxDecoration(
// Theme.of(context).colorScheme.surfaceContainerLowest, color: const Color.fromARGB(255, 83, 83, 83),
// secondaryBgColor: borderRadius: BorderRadius.circular(10),
// const Color.fromARGB(255, 203, 203, 203), ),
// labelTextStyle: TextStyle( selectedTileDecoration: BoxDecoration(
// color: Theme.of(context).colorScheme.tertiary), color: const Color.fromARGB(255, 117, 117, 117),
borderRadius: BorderRadius.circular(10),
),
selectedIconColor: Colors.white,
unselectedIconColor: Colors.grey,
primaryBgColor: Colors.transparent,
secondaryBgColor: Colors.transparent,
labelTextStyle: TextStyle(
color: Theme.of(context).colorScheme.tertiary),
), ),
), ),
), ),

View file

@ -124,10 +124,7 @@ class UserList extends StatelessWidget {
title: Row(children: [ title: Row(children: [
Text(getContactDisplayName(user)), Text(getContactDisplayName(user)),
]), ]),
leading: InitialsAvatar( leading: ContactAvatar(contact: user, fontSize: 15),
getContactDisplayName(user),
fontSize: 15,
),
trailing: Checkbox( trailing: Checkbox(
value: user.blocked, value: user.blocked,
onChanged: (bool? value) { onChanged: (bool? value) {

View file

@ -47,14 +47,15 @@ class _ProfileViewState extends State<ProfileView> {
child: Row( child: Row(
children: [ children: [
GestureDetector( GestureDetector(
onTap: () { onTap: () async {
Navigator.push(context, await Navigator.push(context,
MaterialPageRoute(builder: (context) { MaterialPageRoute(builder: (context) {
return AvatarCreator(); return AvatarCreator();
})); }));
initAsync();
}, },
child: InitialsAvatar( child: ContactAvatar(
userData!.username, userData: userData!,
fontSize: 30, fontSize: 30,
), ),
), ),

View file

@ -636,7 +636,7 @@ packages:
source: hosted source: hosted
version: "4.0.0" version: "4.0.0"
flutter_svg: flutter_svg:
dependency: transitive dependency: "direct main"
description: description:
name: flutter_svg name: flutter_svg
sha256: c200fd79c918a40c5cd50ea0877fa13f81bdaf6f0a5d3dbcc2a13e3285d6aa1b sha256: c200fd79c918a40c5cd50ea0877fa13f81bdaf6f0a5d3dbcc2a13e3285d6aa1b

View file

@ -53,6 +53,7 @@ dependencies:
web_socket_channel: ^3.0.1 web_socket_channel: ^3.0.1
camera: ^0.11.1 camera: ^0.11.1
avatar_maker: ^0.2.0 avatar_maker: ^0.2.0
flutter_svg: ^2.0.17
# avatar_maker # avatar_maker
# avatar_maker: # avatar_maker:
# path: ./dependencies/avatar_maker/ # path: ./dependencies/avatar_maker/