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|
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

View file

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

View file

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

View file

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

View file

@ -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<String> 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) {

View file

@ -122,6 +122,13 @@ class ContactsDao extends DatabaseAccessor<TwonlyDatabase>
.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() {
final count = contacts.blocked.count(distinct: 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 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))();

View file

@ -33,6 +33,20 @@ class $ContactsTable extends Contacts with TableInfo<$ContactsTable, Contact> {
late final GeneratedColumn<String> nickName = GeneratedColumn<String>(
'nick_name', aliasedName, true,
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 =
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<Contact> {
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<Contact> {
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<Contact> {
if (!nullToAbsent || nickName != null) {
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['requested'] = Variable<bool>(requested);
map['blocked'] = Variable<bool>(blocked);
@ -352,6 +390,10 @@ class Contact extends DataClass implements Insertable<Contact> {
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<Contact> {
username: serializer.fromJson<String>(json['username']),
displayName: serializer.fromJson<String?>(json['displayName']),
nickName: serializer.fromJson<String?>(json['nickName']),
avatarSvg: serializer.fromJson<String?>(json['avatarSvg']),
myAvatarCounter: serializer.fromJson<int>(json['myAvatarCounter']),
accepted: serializer.fromJson<bool>(json['accepted']),
requested: serializer.fromJson<bool>(json['requested']),
blocked: serializer.fromJson<bool>(json['blocked']),
@ -404,6 +448,8 @@ class Contact extends DataClass implements Insertable<Contact> {
'username': serializer.toJson<String>(username),
'displayName': serializer.toJson<String?>(displayName),
'nickName': serializer.toJson<String?>(nickName),
'avatarSvg': serializer.toJson<String?>(avatarSvg),
'myAvatarCounter': serializer.toJson<int>(myAvatarCounter),
'accepted': serializer.toJson<bool>(accepted),
'requested': serializer.toJson<bool>(requested),
'blocked': serializer.toJson<bool>(blocked),
@ -424,6 +470,8 @@ class Contact extends DataClass implements Insertable<Contact> {
String? username,
Value<String?> displayName = const Value.absent(),
Value<String?> nickName = const Value.absent(),
Value<String?> avatarSvg = const Value.absent(),
int? myAvatarCounter,
bool? accepted,
bool? requested,
bool? blocked,
@ -440,6 +488,8 @@ class Contact extends DataClass implements Insertable<Contact> {
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<Contact> {
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<Contact> {
..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<Contact> {
username,
displayName,
nickName,
avatarSvg,
myAvatarCounter,
accepted,
requested,
blocked,
@ -538,6 +596,8 @@ class Contact extends DataClass implements Insertable<Contact> {
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<Contact> {
final Value<String> username;
final Value<String?> displayName;
final Value<String?> nickName;
final Value<String?> avatarSvg;
final Value<int> myAvatarCounter;
final Value<bool> accepted;
final Value<bool> requested;
final Value<bool> blocked;
@ -572,6 +634,8 @@ class ContactsCompanion extends UpdateCompanion<Contact> {
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<Contact> {
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<Contact> {
Expression<String>? username,
Expression<String>? displayName,
Expression<String>? nickName,
Expression<String>? avatarSvg,
Expression<int>? myAvatarCounter,
Expression<bool>? accepted,
Expression<bool>? requested,
Expression<bool>? blocked,
@ -623,6 +691,8 @@ class ContactsCompanion extends UpdateCompanion<Contact> {
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<Contact> {
Value<String>? username,
Value<String?>? displayName,
Value<String?>? nickName,
Value<String?>? avatarSvg,
Value<int>? myAvatarCounter,
Value<bool>? accepted,
Value<bool>? requested,
Value<bool>? blocked,
@ -661,6 +733,8 @@ class ContactsCompanion extends UpdateCompanion<Contact> {
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<Contact> {
if (nickName.present) {
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) {
map['accepted'] = Variable<bool>(accepted.value);
}
@ -737,6 +817,8 @@ class ContactsCompanion extends UpdateCompanion<Contact> {
..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, int>
downloadState = GeneratedColumn<int>('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<MessageKind, String> kind =
GeneratedColumn<String>('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<String?> displayName,
Value<String?> nickName,
Value<String?> avatarSvg,
Value<int> myAvatarCounter,
Value<bool> accepted,
Value<bool> requested,
Value<bool> blocked,
@ -2465,6 +2544,8 @@ typedef $$ContactsTableUpdateCompanionBuilder = ContactsCompanion Function({
Value<String> username,
Value<String?> displayName,
Value<String?> nickName,
Value<String?> avatarSvg,
Value<int> myAvatarCounter,
Value<bool> accepted,
Value<bool> requested,
Value<bool> blocked,
@ -2519,6 +2600,13 @@ class $$ContactsTableFilterComposer
ColumnFilters<String> get nickName => $composableBuilder(
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(
column: $table.accepted, builder: (column) => ColumnFilters(column));
@ -2600,6 +2688,13 @@ class $$ContactsTableOrderingComposer
ColumnOrderings<String> get nickName => $composableBuilder(
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(
column: $table.accepted, builder: (column) => ColumnOrderings(column));
@ -2661,6 +2756,12 @@ class $$ContactsTableAnnotationComposer
GeneratedColumn<String> get nickName =>
$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 =>
$composableBuilder(column: $table.accepted, builder: (column) => column);
@ -2743,6 +2844,8 @@ class $$ContactsTableTableManager extends RootTableManager<
Value<String> username = const Value.absent(),
Value<String?> displayName = 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> requested = const Value.absent(),
Value<bool> 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<String?> displayName = 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> requested = const Value.absent(),
Value<bool> 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,

View file

@ -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};
}
}

View file

@ -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,
UserData({
required this.userId,
required this.username,
required this.displayName});
final String username;
final String displayName;
required this.displayName,
});
String username;
String displayName;
String? avatarSvg;
String? avatarJson;
int? avatarCounter;
final int userId;

View file

@ -10,10 +10,16 @@ UserData _$UserDataFromJson(Map<String, dynamic> 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<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{
'username': instance.username,
'displayName': instance.displayName,
'avatarSvg': instance.avatarSvg,
'avatarJson': instance.avatarJson,
'avatarCounter': instance.avatarCounter,
'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/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<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));
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));

View file

@ -75,6 +75,7 @@ class ApiProvider {
if (!globalIsAppInBackground) {
tryTransmitMessages();
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 {
final appDir = await getApplicationSupportDirectory();
if (appDir.existsSync()) {

View file

@ -66,7 +66,14 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
}
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<CameraPreviewView> {
@override
void 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: [

View file

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

View file

@ -214,8 +214,8 @@ class _ChatItemDetailsViewState extends State<ChatItemDetailsView> {
? Container()
: Row(
children: [
InitialsAvatar(
getContactDisplayName(user!),
ContactAvatar(
contact: user!,
fontSize: 19,
),
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: () {
if (currentMessage == null) {
context

View file

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

View file

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

View file

@ -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<AvatarCreator> {
height: 25,
),
AvatarMakerAvatar(
backgroundColor: Colors.grey[200],
backgroundColor: Colors.transparent,
radius: 100,
),
SizedBox(
@ -39,13 +42,17 @@ class _AvatarCreatorState extends State<AvatarCreator> {
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<AvatarCreator> {
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),
),
),
),

View file

@ -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) {

View file

@ -47,14 +47,15 @@ class _ProfileViewState extends State<ProfileView> {
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,
),
),

View file

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

View file

@ -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/