From ec095fea9cc4d7df1bae3342a4790d913a294b92 Mon Sep 17 00:00:00 2001 From: otsmr Date: Sat, 22 Mar 2025 11:24:59 +0100 Subject: [PATCH] custom avatar --- ios/Podfile | 21 ++++ ios/Podfile.lock | 2 +- ios/Runner/Info.plist | 3 + lib/src/components/best_friends_selector.dart | 4 +- lib/src/components/initialsavatar.dart | 66 +++++++++- lib/src/database/daos/contacts_dao.dart | 7 ++ lib/src/database/tables/contacts_table.dart | 3 + lib/src/database/twonly_database.g.dart | 119 +++++++++++++++++- lib/src/json_models/message.dart | 25 +++- lib/src/json_models/userdata.dart | 20 +-- .../{user_data.g.dart => userdata.g.dart} | 8 +- lib/src/providers/api/api.dart | 26 ++++ lib/src/providers/api/server_messages.dart | 8 ++ lib/src/providers/api_provider.dart | 1 + lib/src/utils/storage.dart | 5 + .../camera_to_share/camera_preview_view.dart | 18 ++- .../camera_to_share/share_image_view.dart | 4 +- .../views/chats/chat_item_details_view.dart | 4 +- lib/src/views/chats/chat_list_view.dart | 2 +- lib/src/views/chats/search_username_view.dart | 2 +- lib/src/views/contact/contact_view.dart | 5 +- lib/src/views/settings/avatar_creator.dart | 70 ++++++++--- .../settings/privacy_view_block_users.dart | 5 +- .../views/settings/settings_main_view.dart | 9 +- pubspec.lock | 2 +- pubspec.yaml | 1 + 26 files changed, 374 insertions(+), 66 deletions(-) rename lib/src/json_models/{user_data.g.dart => userdata.g.dart} (68%) diff --git a/ios/Podfile b/ios/Podfile index e348949..06cfecf 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -46,8 +46,29 @@ end post_install do |installer| installer.pods_project.targets.each do |target| flutter_additional_ios_build_settings(target) + target.build_configurations.each do |config| 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 diff --git a/ios/Podfile.lock b/ios/Podfile.lock index ba2256a..bc929c8 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -339,6 +339,6 @@ SPEC CHECKSUMS: sqlite3_flutter_libs: f6acaa2172e6bb3e2e70c771661905080e8ebcf2 url_launcher_ios: 694010445543906933d732453a59da0a173ae33d -PODFILE CHECKSUM: 84a7d8d37f41d292cbd8505494629ed779242400 +PODFILE CHECKSUM: eda8ac661dab0c3d1e1b175d40ebbf2becd0ce86 COCOAPODS: 1.16.2 diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 3b1d6f0..afc890d 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -45,10 +45,13 @@ UIApplicationSupportsIndirectInputEvents + NSCameraUsageDescription To create photos that can be shared. NSMicrophoneUsageDescription To create videos that can be securely shared. + NSPhotoLibraryAddUsageDescription + Store photos in the gallery. NSFaceIDUsageDescription To protect others twonlies! diff --git a/lib/src/components/best_friends_selector.dart b/lib/src/components/best_friends_selector.dart index d233b9f..5d3717d 100644 --- a/lib/src/components/best_friends_selector.dart +++ b/lib/src/components/best_friends_selector.dart @@ -115,8 +115,8 @@ class UserCheckbox extends StatelessWidget { ), child: Row( children: [ - InitialsAvatar( - displayName, + ContactAvatar( + contact: user, fontSize: 12, ), SizedBox(width: 8), diff --git a/lib/src/components/initialsavatar.dart b/lib/src/components/initialsavatar.dart index 7636c49..8e5c5ab 100644 --- a/lib/src/components/initialsavatar.dart +++ b/lib/src/components/initialsavatar.dart @@ -1,13 +1,66 @@ 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 { - final String displayName; +class ContactAvatar extends StatelessWidget { + final Contact? contact; + final UserData? userData; final double? fontSize; - const InitialsAvatar(this.displayName, {super.key, this.fontSize = 20}); + const ContactAvatar( + {super.key, this.contact, this.userData, this.fontSize = 20}); @override 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 List nameParts = displayName.split(' '); String initials = nameParts.map((part) => part[0]).join().toUpperCase(); @@ -35,8 +88,6 @@ class InitialsAvatar extends StatelessWidget { bool isPro = initials[0] == "T"; - double proSize = (fontSize == null) ? 40 : (fontSize! * 2); - return isPro ? //or 15.0 Container( @@ -60,7 +111,10 @@ class InitialsAvatar extends StatelessWidget { ), ) : CircleAvatar( - backgroundColor: avatarColor, radius: fontSize, child: child); + backgroundColor: avatarColor, + radius: fontSize, + child: child, + ); } Color _getTextColor(Color color) { diff --git a/lib/src/database/daos/contacts_dao.dart b/lib/src/database/daos/contacts_dao.dart index 28fc3c4..9679081 100644 --- a/lib/src/database/daos/contacts_dao.dart +++ b/lib/src/database/daos/contacts_dao.dart @@ -122,6 +122,13 @@ class ContactsDao extends DatabaseAccessor .watch(); } + Future> getAllNotBlockedContacts() { + return (select(contacts) + ..where((t) => t.accepted.equals(true) & t.blocked.equals(false)) + ..orderBy([(t) => OrderingTerm.desc(t.lastMessageExchange)])) + .get(); + } + Stream watchContactsBlocked() { final count = contacts.blocked.count(distinct: true); final query = selectOnly(contacts)..where(contacts.blocked.equals(true)); diff --git a/lib/src/database/tables/contacts_table.dart b/lib/src/database/tables/contacts_table.dart index 340608a..d9a9f1c 100644 --- a/lib/src/database/tables/contacts_table.dart +++ b/lib/src/database/tables/contacts_table.dart @@ -7,6 +7,9 @@ class Contacts extends Table { TextColumn get username => text().unique()(); TextColumn get displayName => 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 requested => boolean().withDefault(Constant(false))(); diff --git a/lib/src/database/twonly_database.g.dart b/lib/src/database/twonly_database.g.dart index 926fbb0..835816f 100644 --- a/lib/src/database/twonly_database.g.dart +++ b/lib/src/database/twonly_database.g.dart @@ -33,6 +33,20 @@ class $ContactsTable extends Contacts with TableInfo<$ContactsTable, Contact> { late final GeneratedColumn nickName = GeneratedColumn( 'nick_name', aliasedName, true, type: DriftSqlType.string, requiredDuringInsert: false); + static const VerificationMeta _avatarSvgMeta = + const VerificationMeta('avatarSvg'); + @override + late final GeneratedColumn avatarSvg = GeneratedColumn( + 'avatar_svg', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + static const VerificationMeta _myAvatarCounterMeta = + const VerificationMeta('myAvatarCounter'); + @override + late final GeneratedColumn myAvatarCounter = GeneratedColumn( + 'my_avatar_counter', aliasedName, false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: Constant(0)); static const VerificationMeta _acceptedMeta = const VerificationMeta('accepted'); @override @@ -129,6 +143,8 @@ class $ContactsTable extends Contacts with TableInfo<$ContactsTable, Contact> { username, displayName, nickName, + avatarSvg, + myAvatarCounter, accepted, requested, blocked, @@ -171,6 +187,16 @@ class $ContactsTable extends Contacts with TableInfo<$ContactsTable, Contact> { context.handle(_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')) { context.handle(_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']), nickName: attachedDatabase.typeMapping .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 .read(DriftSqlType.bool, data['${effectivePrefix}accepted'])!, requested: attachedDatabase.typeMapping @@ -283,6 +313,8 @@ class Contact extends DataClass implements Insertable { final String username; final String? displayName; final String? nickName; + final String? avatarSvg; + final int myAvatarCounter; final bool accepted; final bool requested; final bool blocked; @@ -299,6 +331,8 @@ class Contact extends DataClass implements Insertable { required this.username, this.displayName, this.nickName, + this.avatarSvg, + required this.myAvatarCounter, required this.accepted, required this.requested, required this.blocked, @@ -321,6 +355,10 @@ class Contact extends DataClass implements Insertable { if (!nullToAbsent || nickName != null) { map['nick_name'] = Variable(nickName); } + if (!nullToAbsent || avatarSvg != null) { + map['avatar_svg'] = Variable(avatarSvg); + } + map['my_avatar_counter'] = Variable(myAvatarCounter); map['accepted'] = Variable(accepted); map['requested'] = Variable(requested); map['blocked'] = Variable(blocked); @@ -352,6 +390,10 @@ class Contact extends DataClass implements Insertable { nickName: nickName == null && nullToAbsent ? const Value.absent() : Value(nickName), + avatarSvg: avatarSvg == null && nullToAbsent + ? const Value.absent() + : Value(avatarSvg), + myAvatarCounter: Value(myAvatarCounter), accepted: Value(accepted), requested: Value(requested), blocked: Value(blocked), @@ -380,6 +422,8 @@ class Contact extends DataClass implements Insertable { username: serializer.fromJson(json['username']), displayName: serializer.fromJson(json['displayName']), nickName: serializer.fromJson(json['nickName']), + avatarSvg: serializer.fromJson(json['avatarSvg']), + myAvatarCounter: serializer.fromJson(json['myAvatarCounter']), accepted: serializer.fromJson(json['accepted']), requested: serializer.fromJson(json['requested']), blocked: serializer.fromJson(json['blocked']), @@ -404,6 +448,8 @@ class Contact extends DataClass implements Insertable { 'username': serializer.toJson(username), 'displayName': serializer.toJson(displayName), 'nickName': serializer.toJson(nickName), + 'avatarSvg': serializer.toJson(avatarSvg), + 'myAvatarCounter': serializer.toJson(myAvatarCounter), 'accepted': serializer.toJson(accepted), 'requested': serializer.toJson(requested), 'blocked': serializer.toJson(blocked), @@ -424,6 +470,8 @@ class Contact extends DataClass implements Insertable { String? username, Value displayName = const Value.absent(), Value nickName = const Value.absent(), + Value avatarSvg = const Value.absent(), + int? myAvatarCounter, bool? accepted, bool? requested, bool? blocked, @@ -440,6 +488,8 @@ class Contact extends DataClass implements Insertable { username: username ?? this.username, displayName: displayName.present ? displayName.value : this.displayName, nickName: nickName.present ? nickName.value : this.nickName, + avatarSvg: avatarSvg.present ? avatarSvg.value : this.avatarSvg, + myAvatarCounter: myAvatarCounter ?? this.myAvatarCounter, accepted: accepted ?? this.accepted, requested: requested ?? this.requested, blocked: blocked ?? this.blocked, @@ -465,6 +515,10 @@ class Contact extends DataClass implements Insertable { displayName: data.displayName.present ? data.displayName.value : this.displayName, 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, requested: data.requested.present ? data.requested.value : this.requested, blocked: data.blocked.present ? data.blocked.value : this.blocked, @@ -498,6 +552,8 @@ class Contact extends DataClass implements Insertable { ..write('username: $username, ') ..write('displayName: $displayName, ') ..write('nickName: $nickName, ') + ..write('avatarSvg: $avatarSvg, ') + ..write('myAvatarCounter: $myAvatarCounter, ') ..write('accepted: $accepted, ') ..write('requested: $requested, ') ..write('blocked: $blocked, ') @@ -519,6 +575,8 @@ class Contact extends DataClass implements Insertable { username, displayName, nickName, + avatarSvg, + myAvatarCounter, accepted, requested, blocked, @@ -538,6 +596,8 @@ class Contact extends DataClass implements Insertable { other.username == this.username && other.displayName == this.displayName && other.nickName == this.nickName && + other.avatarSvg == this.avatarSvg && + other.myAvatarCounter == this.myAvatarCounter && other.accepted == this.accepted && other.requested == this.requested && other.blocked == this.blocked && @@ -556,6 +616,8 @@ class ContactsCompanion extends UpdateCompanion { final Value username; final Value displayName; final Value nickName; + final Value avatarSvg; + final Value myAvatarCounter; final Value accepted; final Value requested; final Value blocked; @@ -572,6 +634,8 @@ class ContactsCompanion extends UpdateCompanion { this.username = const Value.absent(), this.displayName = const Value.absent(), this.nickName = const Value.absent(), + this.avatarSvg = const Value.absent(), + this.myAvatarCounter = const Value.absent(), this.accepted = const Value.absent(), this.requested = const Value.absent(), this.blocked = const Value.absent(), @@ -589,6 +653,8 @@ class ContactsCompanion extends UpdateCompanion { required String username, this.displayName = const Value.absent(), this.nickName = const Value.absent(), + this.avatarSvg = const Value.absent(), + this.myAvatarCounter = const Value.absent(), this.accepted = const Value.absent(), this.requested = const Value.absent(), this.blocked = const Value.absent(), @@ -606,6 +672,8 @@ class ContactsCompanion extends UpdateCompanion { Expression? username, Expression? displayName, Expression? nickName, + Expression? avatarSvg, + Expression? myAvatarCounter, Expression? accepted, Expression? requested, Expression? blocked, @@ -623,6 +691,8 @@ class ContactsCompanion extends UpdateCompanion { if (username != null) 'username': username, if (displayName != null) 'display_name': displayName, 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 (requested != null) 'requested': requested, if (blocked != null) 'blocked': blocked, @@ -645,6 +715,8 @@ class ContactsCompanion extends UpdateCompanion { Value? username, Value? displayName, Value? nickName, + Value? avatarSvg, + Value? myAvatarCounter, Value? accepted, Value? requested, Value? blocked, @@ -661,6 +733,8 @@ class ContactsCompanion extends UpdateCompanion { username: username ?? this.username, displayName: displayName ?? this.displayName, nickName: nickName ?? this.nickName, + avatarSvg: avatarSvg ?? this.avatarSvg, + myAvatarCounter: myAvatarCounter ?? this.myAvatarCounter, accepted: accepted ?? this.accepted, requested: requested ?? this.requested, blocked: blocked ?? this.blocked, @@ -691,6 +765,12 @@ class ContactsCompanion extends UpdateCompanion { if (nickName.present) { map['nick_name'] = Variable(nickName.value); } + if (avatarSvg.present) { + map['avatar_svg'] = Variable(avatarSvg.value); + } + if (myAvatarCounter.present) { + map['my_avatar_counter'] = Variable(myAvatarCounter.value); + } if (accepted.present) { map['accepted'] = Variable(accepted.value); } @@ -737,6 +817,8 @@ class ContactsCompanion extends UpdateCompanion { ..write('username: $username, ') ..write('displayName: $displayName, ') ..write('nickName: $nickName, ') + ..write('avatarSvg: $avatarSvg, ') + ..write('myAvatarCounter: $myAvatarCounter, ') ..write('accepted: $accepted, ') ..write('requested: $requested, ') ..write('blocked: $blocked, ') @@ -805,8 +887,6 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> { defaultConstraints: GeneratedColumn.constraintIsAlways( 'CHECK ("acknowledge_by_user" IN (0, 1))'), defaultValue: Constant(false)); - static const VerificationMeta _downloadStateMeta = - const VerificationMeta('downloadState'); @override late final GeneratedColumnWithTypeConverter downloadState = GeneratedColumn('download_state', aliasedName, false, @@ -824,7 +904,6 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> { defaultConstraints: GeneratedColumn.constraintIsAlways( 'CHECK ("acknowledge_by_server" IN (0, 1))'), defaultValue: Constant(false)); - static const VerificationMeta _kindMeta = const VerificationMeta('kind'); @override late final GeneratedColumnWithTypeConverter kind = GeneratedColumn('kind', aliasedName, false, @@ -918,14 +997,12 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> { acknowledgeByUser.isAcceptableOrUnknown( data['acknowledge_by_user']!, _acknowledgeByUserMeta)); } - context.handle(_downloadStateMeta, const VerificationResult.success()); if (data.containsKey('acknowledge_by_server')) { context.handle( _acknowledgeByServerMeta, acknowledgeByServer.isAcceptableOrUnknown( data['acknowledge_by_server']!, _acknowledgeByServerMeta)); } - context.handle(_kindMeta, const VerificationResult.success()); if (data.containsKey('content_json')) { context.handle( _contentJsonMeta, @@ -2448,6 +2525,8 @@ typedef $$ContactsTableCreateCompanionBuilder = ContactsCompanion Function({ required String username, Value displayName, Value nickName, + Value avatarSvg, + Value myAvatarCounter, Value accepted, Value requested, Value blocked, @@ -2465,6 +2544,8 @@ typedef $$ContactsTableUpdateCompanionBuilder = ContactsCompanion Function({ Value username, Value displayName, Value nickName, + Value avatarSvg, + Value myAvatarCounter, Value accepted, Value requested, Value blocked, @@ -2519,6 +2600,13 @@ class $$ContactsTableFilterComposer ColumnFilters get nickName => $composableBuilder( column: $table.nickName, builder: (column) => ColumnFilters(column)); + ColumnFilters get avatarSvg => $composableBuilder( + column: $table.avatarSvg, builder: (column) => ColumnFilters(column)); + + ColumnFilters get myAvatarCounter => $composableBuilder( + column: $table.myAvatarCounter, + builder: (column) => ColumnFilters(column)); + ColumnFilters get accepted => $composableBuilder( column: $table.accepted, builder: (column) => ColumnFilters(column)); @@ -2600,6 +2688,13 @@ class $$ContactsTableOrderingComposer ColumnOrderings get nickName => $composableBuilder( column: $table.nickName, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get avatarSvg => $composableBuilder( + column: $table.avatarSvg, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get myAvatarCounter => $composableBuilder( + column: $table.myAvatarCounter, + builder: (column) => ColumnOrderings(column)); + ColumnOrderings get accepted => $composableBuilder( column: $table.accepted, builder: (column) => ColumnOrderings(column)); @@ -2661,6 +2756,12 @@ class $$ContactsTableAnnotationComposer GeneratedColumn get nickName => $composableBuilder(column: $table.nickName, builder: (column) => column); + GeneratedColumn get avatarSvg => + $composableBuilder(column: $table.avatarSvg, builder: (column) => column); + + GeneratedColumn get myAvatarCounter => $composableBuilder( + column: $table.myAvatarCounter, builder: (column) => column); + GeneratedColumn get accepted => $composableBuilder(column: $table.accepted, builder: (column) => column); @@ -2743,6 +2844,8 @@ class $$ContactsTableTableManager extends RootTableManager< Value username = const Value.absent(), Value displayName = const Value.absent(), Value nickName = const Value.absent(), + Value avatarSvg = const Value.absent(), + Value myAvatarCounter = const Value.absent(), Value accepted = const Value.absent(), Value requested = const Value.absent(), Value blocked = const Value.absent(), @@ -2760,6 +2863,8 @@ class $$ContactsTableTableManager extends RootTableManager< username: username, displayName: displayName, nickName: nickName, + avatarSvg: avatarSvg, + myAvatarCounter: myAvatarCounter, accepted: accepted, requested: requested, blocked: blocked, @@ -2777,6 +2882,8 @@ class $$ContactsTableTableManager extends RootTableManager< required String username, Value displayName = const Value.absent(), Value nickName = const Value.absent(), + Value avatarSvg = const Value.absent(), + Value myAvatarCounter = const Value.absent(), Value accepted = const Value.absent(), Value requested = const Value.absent(), Value blocked = const Value.absent(), @@ -2794,6 +2901,8 @@ class $$ContactsTableTableManager extends RootTableManager< username: username, displayName: displayName, nickName: nickName, + avatarSvg: avatarSvg, + myAvatarCounter: myAvatarCounter, accepted: accepted, requested: requested, blocked: blocked, diff --git a/lib/src/json_models/message.dart b/lib/src/json_models/message.dart index eb7f89e..95473eb 100644 --- a/lib/src/json_models/message.dart +++ b/lib/src/json_models/message.dart @@ -4,6 +4,7 @@ enum MessageKind { textMessage, media, contactRequest, + avatarChange, rejectRequest, acceptRequest, opened, @@ -93,6 +94,8 @@ class MessageContent { return MediaMessageContent.fromJson(json); case MessageKind.textMessage: return TextMessageContent.fromJson(json); + case MessageKind.avatarChange: + return AvatarContent.fromJson(json); default: return null; } @@ -160,15 +163,25 @@ class TextMessageContent extends MessageContent { TextMessageContent({required this.text}); static TextMessageContent fromJson(Map json) { - return TextMessageContent( - text: json['text'], - ); + return TextMessageContent(text: json['text']); } @override Map toJson() { - return { - 'text': text, - }; + return {'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}; } } diff --git a/lib/src/json_models/userdata.dart b/lib/src/json_models/userdata.dart index 4499060..07a2d49 100644 --- a/lib/src/json_models/userdata.dart +++ b/lib/src/json_models/userdata.dart @@ -1,14 +1,20 @@ import 'package:json_annotation/json_annotation.dart'; -part 'user_data.g.dart'; +part 'userdata.g.dart'; @JsonSerializable() class UserData { - const UserData( - {required this.userId, - required this.username, - required this.displayName}); - final String username; - final String displayName; + UserData({ + required this.userId, + required this.username, + required this.displayName, + }); + + String username; + String displayName; + + String? avatarSvg; + String? avatarJson; + int? avatarCounter; final int userId; diff --git a/lib/src/json_models/user_data.g.dart b/lib/src/json_models/userdata.g.dart similarity index 68% rename from lib/src/json_models/user_data.g.dart rename to lib/src/json_models/userdata.g.dart index 2ddef7d..2d2e586 100644 --- a/lib/src/json_models/user_data.g.dart +++ b/lib/src/json_models/userdata.g.dart @@ -10,10 +10,16 @@ UserData _$UserDataFromJson(Map json) => UserData( userId: (json['userId'] as num).toInt(), username: json['username'] as String, displayName: json['displayName'] as String, - ); + ) + ..avatarSvg = json['avatarSvg'] as String? + ..avatarJson = json['avatarJson'] as String? + ..avatarCounter = (json['avatarCounter'] as num?)?.toInt(); Map _$UserDataToJson(UserData instance) => { 'username': instance.username, 'displayName': instance.displayName, + 'avatarSvg': instance.avatarSvg, + 'avatarJson': instance.avatarJson, + 'avatarCounter': instance.avatarCounter, 'userId': instance.userId, }; diff --git a/lib/src/providers/api/api.dart b/lib/src/providers/api/api.dart index 80dd0b8..fa4b985 100644 --- a/lib/src/providers/api/api.dart +++ b/lib/src/providers/api/api.dart @@ -6,11 +6,13 @@ import 'package:twonly/globals.dart'; import 'package:twonly/src/database/twonly_database.dart'; import 'package:twonly/src/database/tables/messages_table.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/providers/api/api_utils.dart'; import 'package:twonly/src/providers/hive.dart'; // ignore: library_prefixes import 'package:twonly/src/utils/signal.dart' as SignalHelper; +import 'package:twonly/src/utils/storage.dart'; Future tryTransmitMessages() async { // List retransmit = @@ -122,3 +124,27 @@ Future notifyContactAboutOpeningMessage( ), ); } + +Future notifyContactsAboutAvatarChange() async { + List 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(), + ), + ); + } + } +} diff --git a/lib/src/providers/api/server_messages.dart b/lib/src/providers/api/server_messages.dart index 32a2e90..aa083cf 100644 --- a/lib/src/providers/api/server_messages.dart +++ b/lib/src/providers/api/server_messages.dart @@ -199,6 +199,14 @@ Future handleNewMessage(int fromUserId, Uint8List body) async { final update = ContactsCompanion(accepted: Value(true)); twonlyDatabase.contactsDao.updateContact(fromUserId, update); 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; case MessageKind.ack: final update = MessagesCompanion(acknowledgeByUser: Value(true)); diff --git a/lib/src/providers/api_provider.dart b/lib/src/providers/api_provider.dart index 5beb737..d6277bd 100644 --- a/lib/src/providers/api_provider.dart +++ b/lib/src/providers/api_provider.dart @@ -75,6 +75,7 @@ class ApiProvider { if (!globalIsAppInBackground) { tryTransmitMessages(); tryDownloadAllMediaFiles(); + notifyContactsAboutAvatarChange(); } } diff --git a/lib/src/utils/storage.dart b/lib/src/utils/storage.dart index 2aa643d..5cee117 100644 --- a/lib/src/utils/storage.dart +++ b/lib/src/utils/storage.dart @@ -28,6 +28,11 @@ Future getUser() async { } } +Future updateUser(UserData userData) async { + final storage = getSecureStorage(); + storage.write(key: "userData", value: jsonEncode(userData)); +} + Future deleteLocalUserData() async { final appDir = await getApplicationSupportDirectory(); if (appDir.existsSync()) { diff --git a/lib/src/views/camera_to_share/camera_preview_view.dart b/lib/src/views/camera_to_share/camera_preview_view.dart index 7bb1f06..92e77f3 100644 --- a/lib/src/views/camera_to_share/camera_preview_view.dart +++ b/lib/src/views/camera_to_share/camera_preview_view.dart @@ -66,7 +66,14 @@ class _CameraPreviewViewState extends State { } 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(() { isZoomAble = false; }); @@ -115,12 +122,19 @@ class _CameraPreviewViewState extends State { @override void dispose() { - controller.dispose(); + if (cameraId < gCameras.length) { + controller.dispose(); + } super.dispose(); } @override Widget build(BuildContext context) { + if (cameraId >= gCameras.length) { + return Center( + child: Text("No camera found."), + ); + } return MediaViewSizing( Stack( children: [ diff --git a/lib/src/views/camera_to_share/share_image_view.dart b/lib/src/views/camera_to_share/share_image_view.dart index 927c634..8c0c4c1 100644 --- a/lib/src/views/camera_to_share/share_image_view.dart +++ b/lib/src/views/camera_to_share/share_image_view.dart @@ -303,8 +303,8 @@ class UserList extends StatelessWidget { ), ], ), - leading: InitialsAvatar( - getContactDisplayName(user), + leading: ContactAvatar( + contact: user, fontSize: 15, ), trailing: Checkbox( diff --git a/lib/src/views/chats/chat_item_details_view.dart b/lib/src/views/chats/chat_item_details_view.dart index 24e3879..37b1c81 100644 --- a/lib/src/views/chats/chat_item_details_view.dart +++ b/lib/src/views/chats/chat_item_details_view.dart @@ -214,8 +214,8 @@ class _ChatItemDetailsViewState extends State { ? Container() : Row( children: [ - InitialsAvatar( - getContactDisplayName(user!), + ContactAvatar( + contact: user!, fontSize: 19, ), SizedBox(width: 10), diff --git a/lib/src/views/chats/chat_list_view.dart b/lib/src/views/chats/chat_list_view.dart index c32ca65..ed1e2ea 100644 --- a/lib/src/views/chats/chat_list_view.dart +++ b/lib/src/views/chats/chat_list_view.dart @@ -268,7 +268,7 @@ class _UserListItem extends State { ); }, ), - leading: InitialsAvatar(getContactDisplayName(widget.user)), + leading: ContactAvatar(contact: widget.user), onTap: () { if (currentMessage == null) { context diff --git a/lib/src/views/chats/search_username_view.dart b/lib/src/views/chats/search_username_view.dart index dbdb997..f89e7e0 100644 --- a/lib/src/views/chats/search_username_view.dart +++ b/lib/src/views/chats/search_username_view.dart @@ -184,7 +184,7 @@ class _ContactsListViewState extends State { final displayName = getContactDisplayName(contact); return ListTile( title: Text(displayName), - leading: InitialsAvatar(displayName), + leading: ContactAvatar(contact: contact), trailing: Row( mainAxisSize: MainAxisSize.min, children: [ diff --git a/lib/src/views/contact/contact_view.dart b/lib/src/views/contact/contact_view.dart index 84e05a5..5cfac1b 100644 --- a/lib/src/views/contact/contact_view.dart +++ b/lib/src/views/contact/contact_view.dart @@ -44,10 +44,7 @@ class _ContactViewState extends State { children: [ Padding( padding: const EdgeInsets.all(10), - child: InitialsAvatar( - getContactDisplayName(contact), - fontSize: 30, - ), + child: ContactAvatar(contact: contact, fontSize: 30), ), Row( mainAxisAlignment: MainAxisAlignment.center, diff --git a/lib/src/views/settings/avatar_creator.dart b/lib/src/views/settings/avatar_creator.dart index e06e0d8..9a8995f 100644 --- a/lib/src/views/settings/avatar_creator.dart +++ b/lib/src/views/settings/avatar_creator.dart @@ -2,6 +2,9 @@ import 'dart:math'; import 'package:avatar_maker/avatar_maker.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 { const AvatarCreator({super.key}); @@ -28,7 +31,7 @@ class _AvatarCreatorState extends State { height: 25, ), AvatarMakerAvatar( - backgroundColor: Colors.grey[200], + backgroundColor: Colors.transparent, radius: 100, ), SizedBox( @@ -39,13 +42,17 @@ class _AvatarCreatorState extends State { Spacer(flex: 2), Expanded( flex: 3, - child: Container( + child: SizedBox( height: 35, child: ElevatedButton.icon( icon: Icon(Icons.edit), label: Text("Customize"), - onPressed: () => Navigator.push(context, - new MaterialPageRoute(builder: (context) => NewPage())), + onPressed: () => Navigator.push( + context, + MaterialPageRoute( + builder: (context) => NewPage(), + ), + ), ), ), ), @@ -64,6 +71,21 @@ class _AvatarCreatorState extends State { class NewPage extends StatelessWidget { 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 Widget build(BuildContext context) { var _width = MediaQuery.of(context).size.width; @@ -75,9 +97,9 @@ class NewPage extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.center, children: [ Padding( - padding: const EdgeInsets.symmetric(vertical: 30), + padding: const EdgeInsets.symmetric(vertical: 00), child: AvatarMakerAvatar( - radius: 100, + radius: 130, backgroundColor: Colors.transparent, ), ), @@ -85,12 +107,18 @@ class NewPage extends StatelessWidget { width: min(600, _width * 0.85), child: Row( children: [ - Text( - "Customize:", - style: Theme.of(context).textTheme.titleLarge, - ), 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(), AvatarMakerResetWidget(), ], @@ -106,12 +134,20 @@ class NewPage extends StatelessWidget { boxDecoration: BoxDecoration( boxShadow: [BoxShadow()], ), - // primaryBgColor: - // Theme.of(context).colorScheme.surfaceContainerLowest, - // secondaryBgColor: - // const Color.fromARGB(255, 203, 203, 203), - // labelTextStyle: TextStyle( - // color: Theme.of(context).colorScheme.tertiary), + unselectedTileDecoration: BoxDecoration( + color: const Color.fromARGB(255, 83, 83, 83), + borderRadius: BorderRadius.circular(10), + ), + selectedTileDecoration: BoxDecoration( + 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), ), ), ), diff --git a/lib/src/views/settings/privacy_view_block_users.dart b/lib/src/views/settings/privacy_view_block_users.dart index 09d88ca..108116f 100644 --- a/lib/src/views/settings/privacy_view_block_users.dart +++ b/lib/src/views/settings/privacy_view_block_users.dart @@ -124,10 +124,7 @@ class UserList extends StatelessWidget { title: Row(children: [ Text(getContactDisplayName(user)), ]), - leading: InitialsAvatar( - getContactDisplayName(user), - fontSize: 15, - ), + leading: ContactAvatar(contact: user, fontSize: 15), trailing: Checkbox( value: user.blocked, onChanged: (bool? value) { diff --git a/lib/src/views/settings/settings_main_view.dart b/lib/src/views/settings/settings_main_view.dart index 57d5e29..85125c7 100644 --- a/lib/src/views/settings/settings_main_view.dart +++ b/lib/src/views/settings/settings_main_view.dart @@ -47,14 +47,15 @@ class _ProfileViewState extends State { child: Row( children: [ GestureDetector( - onTap: () { - Navigator.push(context, + onTap: () async { + await Navigator.push(context, MaterialPageRoute(builder: (context) { return AvatarCreator(); })); + initAsync(); }, - child: InitialsAvatar( - userData!.username, + child: ContactAvatar( + userData: userData!, fontSize: 30, ), ), diff --git a/pubspec.lock b/pubspec.lock index 914ed36..150abef 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -636,7 +636,7 @@ packages: source: hosted version: "4.0.0" flutter_svg: - dependency: transitive + dependency: "direct main" description: name: flutter_svg sha256: c200fd79c918a40c5cd50ea0877fa13f81bdaf6f0a5d3dbcc2a13e3285d6aa1b diff --git a/pubspec.yaml b/pubspec.yaml index b4771b5..c0bba2a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -53,6 +53,7 @@ dependencies: web_socket_channel: ^3.0.1 camera: ^0.11.1 avatar_maker: ^0.2.0 + flutter_svg: ^2.0.17 # avatar_maker # avatar_maker: # path: ./dependencies/avatar_maker/