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/