mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-01-15 14:48:41 +00:00
creation of groups works
This commit is contained in:
parent
98a19b174b
commit
72dca4d4b4
24 changed files with 615 additions and 167 deletions
|
|
@ -37,6 +37,11 @@ class ContactsDao extends DatabaseAccessor<TwonlyDB> with _$ContactsDaoMixin {
|
|||
return select(contacts)..where((t) => t.userId.equals(userId));
|
||||
}
|
||||
|
||||
Future<Contact?> getContactById(int userId) async {
|
||||
return (select(contacts)..where((t) => t.userId.equals(userId)))
|
||||
.getSingleOrNull();
|
||||
}
|
||||
|
||||
Future<List<Contact>> getContactsByUsername(String username) async {
|
||||
return (select(contacts)..where((t) => t.username.equals(username))).get();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,13 @@ import 'package:twonly/src/utils/misc.dart';
|
|||
|
||||
part 'groups.dao.g.dart';
|
||||
|
||||
@DriftAccessor(tables: [Groups, GroupMembers, GroupHistories])
|
||||
@DriftAccessor(
|
||||
tables: [
|
||||
Groups,
|
||||
GroupMembers,
|
||||
GroupHistories,
|
||||
],
|
||||
)
|
||||
class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
|
||||
// this constructor is required so that the main database can create an instance
|
||||
// of this object.
|
||||
|
|
@ -54,6 +60,24 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
|
|||
await into(groupHistories).insert(insertAction);
|
||||
}
|
||||
|
||||
Future<void> updateMember(
|
||||
String groupId,
|
||||
int contactId,
|
||||
GroupMembersCompanion updates,
|
||||
) async {
|
||||
await (update(groupMembers)
|
||||
..where(
|
||||
(c) => c.groupId.equals(groupId) & c.contactId.equals(contactId)))
|
||||
.write(updates);
|
||||
}
|
||||
|
||||
Future<void> removeMember(String groupId, int contactId) async {
|
||||
await (delete(groupMembers)
|
||||
..where(
|
||||
(c) => c.groupId.equals(groupId) & c.contactId.equals(contactId)))
|
||||
.go();
|
||||
}
|
||||
|
||||
Future<Group?> createNewDirectChat(
|
||||
int contactId,
|
||||
GroupsCompanion group,
|
||||
|
|
|
|||
|
|
@ -399,6 +399,38 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
|
|||
.getSingleOrNull();
|
||||
}
|
||||
|
||||
Stream<Future<List<(Message, Contact)>>> watchLastOpenedMessagePerContact(
|
||||
String groupId,
|
||||
) {
|
||||
const sql = '''
|
||||
SELECT m.*, c.*
|
||||
FROM (
|
||||
SELECT ma.contact_id, ma.message_id,
|
||||
ROW_NUMBER() OVER (PARTITION BY ma.contact_id
|
||||
ORDER BY ma.action_at DESC, ma.message_id DESC) AS rn
|
||||
FROM message_actions ma
|
||||
WHERE ma.type = 'openedAt'
|
||||
) last_open
|
||||
JOIN messages m ON m.message_id = last_open.message_id
|
||||
JOIN contacts c ON c.user_id = last_open.contact_id
|
||||
WHERE last_open.rn = 1 AND m.group_id = ?;
|
||||
''';
|
||||
|
||||
return customSelect(
|
||||
sql,
|
||||
variables: [Variable.withString(groupId)],
|
||||
readsFrom: {messages, messageActions, contacts},
|
||||
).watch().map((rows) async {
|
||||
final res = <(Message, Contact)>[];
|
||||
for (final row in rows) {
|
||||
final message = await messages.mapFromRow(row);
|
||||
final contact = await contacts.mapFromRow(row);
|
||||
res.add((message, contact));
|
||||
}
|
||||
return res;
|
||||
});
|
||||
}
|
||||
|
||||
// Future<void> deleteMessagesByContactId(int contactId) {
|
||||
// return (delete(messages)
|
||||
// ..where(
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ class Groups extends Table {
|
|||
Set<Column> get primaryKey => {groupId};
|
||||
}
|
||||
|
||||
enum MemberState { normal, admin }
|
||||
enum MemberState { normal, admin, leftGroup }
|
||||
|
||||
@DataClassName('GroupMember')
|
||||
class GroupMembers extends Table {
|
||||
|
|
|
|||
|
|
@ -112,6 +112,38 @@ class Message extends $pb.GeneratedMessage {
|
|||
PlaintextContent ensurePlaintextContent() => $_ensure(3);
|
||||
}
|
||||
|
||||
class PlaintextContent_RetryErrorMessage extends $pb.GeneratedMessage {
|
||||
factory PlaintextContent_RetryErrorMessage() => create();
|
||||
PlaintextContent_RetryErrorMessage._() : super();
|
||||
factory PlaintextContent_RetryErrorMessage.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
|
||||
factory PlaintextContent_RetryErrorMessage.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
|
||||
|
||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'PlaintextContent.RetryErrorMessage', createEmptyInstance: create)
|
||||
..hasRequiredFields = false
|
||||
;
|
||||
|
||||
@$core.Deprecated(
|
||||
'Using this can add significant overhead to your binary. '
|
||||
'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
|
||||
'Will be removed in next major version')
|
||||
PlaintextContent_RetryErrorMessage clone() => PlaintextContent_RetryErrorMessage()..mergeFromMessage(this);
|
||||
@$core.Deprecated(
|
||||
'Using this can add significant overhead to your binary. '
|
||||
'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
|
||||
'Will be removed in next major version')
|
||||
PlaintextContent_RetryErrorMessage copyWith(void Function(PlaintextContent_RetryErrorMessage) updates) => super.copyWith((message) => updates(message as PlaintextContent_RetryErrorMessage)) as PlaintextContent_RetryErrorMessage;
|
||||
|
||||
$pb.BuilderInfo get info_ => _i;
|
||||
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static PlaintextContent_RetryErrorMessage create() => PlaintextContent_RetryErrorMessage._();
|
||||
PlaintextContent_RetryErrorMessage createEmptyInstance() => create();
|
||||
static $pb.PbList<PlaintextContent_RetryErrorMessage> createRepeated() => $pb.PbList<PlaintextContent_RetryErrorMessage>();
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static PlaintextContent_RetryErrorMessage getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<PlaintextContent_RetryErrorMessage>(create);
|
||||
static PlaintextContent_RetryErrorMessage? _defaultInstance;
|
||||
}
|
||||
|
||||
class PlaintextContent_DecryptionErrorMessage extends $pb.GeneratedMessage {
|
||||
factory PlaintextContent_DecryptionErrorMessage({
|
||||
PlaintextContent_DecryptionErrorMessage_Type? type,
|
||||
|
|
@ -165,11 +197,15 @@ class PlaintextContent_DecryptionErrorMessage extends $pb.GeneratedMessage {
|
|||
class PlaintextContent extends $pb.GeneratedMessage {
|
||||
factory PlaintextContent({
|
||||
PlaintextContent_DecryptionErrorMessage? decryptionErrorMessage,
|
||||
PlaintextContent_RetryErrorMessage? retryControlError,
|
||||
}) {
|
||||
final $result = create();
|
||||
if (decryptionErrorMessage != null) {
|
||||
$result.decryptionErrorMessage = decryptionErrorMessage;
|
||||
}
|
||||
if (retryControlError != null) {
|
||||
$result.retryControlError = retryControlError;
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
PlaintextContent._() : super();
|
||||
|
|
@ -178,6 +214,7 @@ class PlaintextContent extends $pb.GeneratedMessage {
|
|||
|
||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'PlaintextContent', createEmptyInstance: create)
|
||||
..aOM<PlaintextContent_DecryptionErrorMessage>(1, _omitFieldNames ? '' : 'decryptionErrorMessage', protoName: 'decryptionErrorMessage', subBuilder: PlaintextContent_DecryptionErrorMessage.create)
|
||||
..aOM<PlaintextContent_RetryErrorMessage>(2, _omitFieldNames ? '' : 'retryControlError', protoName: 'retryControlError', subBuilder: PlaintextContent_RetryErrorMessage.create)
|
||||
..hasRequiredFields = false
|
||||
;
|
||||
|
||||
|
|
@ -212,6 +249,17 @@ class PlaintextContent extends $pb.GeneratedMessage {
|
|||
void clearDecryptionErrorMessage() => clearField(1);
|
||||
@$pb.TagNumber(1)
|
||||
PlaintextContent_DecryptionErrorMessage ensureDecryptionErrorMessage() => $_ensure(0);
|
||||
|
||||
@$pb.TagNumber(2)
|
||||
PlaintextContent_RetryErrorMessage get retryControlError => $_getN(1);
|
||||
@$pb.TagNumber(2)
|
||||
set retryControlError(PlaintextContent_RetryErrorMessage v) { setField(2, v); }
|
||||
@$pb.TagNumber(2)
|
||||
$core.bool hasRetryControlError() => $_has(1);
|
||||
@$pb.TagNumber(2)
|
||||
void clearRetryControlError() => clearField(2);
|
||||
@$pb.TagNumber(2)
|
||||
PlaintextContent_RetryErrorMessage ensureRetryControlError() => $_ensure(1);
|
||||
}
|
||||
|
||||
class EncryptedContent_GroupCreate extends $pb.GeneratedMessage {
|
||||
|
|
|
|||
|
|
@ -56,13 +56,20 @@ const PlaintextContent$json = {
|
|||
'1': 'PlaintextContent',
|
||||
'2': [
|
||||
{'1': 'decryptionErrorMessage', '3': 1, '4': 1, '5': 11, '6': '.PlaintextContent.DecryptionErrorMessage', '9': 0, '10': 'decryptionErrorMessage', '17': true},
|
||||
{'1': 'retryControlError', '3': 2, '4': 1, '5': 11, '6': '.PlaintextContent.RetryErrorMessage', '9': 1, '10': 'retryControlError', '17': true},
|
||||
],
|
||||
'3': [PlaintextContent_DecryptionErrorMessage$json],
|
||||
'3': [PlaintextContent_RetryErrorMessage$json, PlaintextContent_DecryptionErrorMessage$json],
|
||||
'8': [
|
||||
{'1': '_decryptionErrorMessage'},
|
||||
{'1': '_retryControlError'},
|
||||
],
|
||||
};
|
||||
|
||||
@$core.Deprecated('Use plaintextContentDescriptor instead')
|
||||
const PlaintextContent_RetryErrorMessage$json = {
|
||||
'1': 'RetryErrorMessage',
|
||||
};
|
||||
|
||||
@$core.Deprecated('Use plaintextContentDescriptor instead')
|
||||
const PlaintextContent_DecryptionErrorMessage$json = {
|
||||
'1': 'DecryptionErrorMessage',
|
||||
|
|
@ -85,10 +92,12 @@ const PlaintextContent_DecryptionErrorMessage_Type$json = {
|
|||
final $typed_data.Uint8List plaintextContentDescriptor = $convert.base64Decode(
|
||||
'ChBQbGFpbnRleHRDb250ZW50EmUKFmRlY3J5cHRpb25FcnJvck1lc3NhZ2UYASABKAsyKC5QbG'
|
||||
'FpbnRleHRDb250ZW50LkRlY3J5cHRpb25FcnJvck1lc3NhZ2VIAFIWZGVjcnlwdGlvbkVycm9y'
|
||||
'TWVzc2FnZYgBARqEAQoWRGVjcnlwdGlvbkVycm9yTWVzc2FnZRJBCgR0eXBlGAEgASgOMi0uUG'
|
||||
'xhaW50ZXh0Q29udGVudC5EZWNyeXB0aW9uRXJyb3JNZXNzYWdlLlR5cGVSBHR5cGUiJwoEVHlw'
|
||||
'ZRILCgdVTktOT1dOEAASEgoOUFJFS0VZX1VOS05PV04QAUIZChdfZGVjcnlwdGlvbkVycm9yTW'
|
||||
'Vzc2FnZQ==');
|
||||
'TWVzc2FnZYgBARJWChFyZXRyeUNvbnRyb2xFcnJvchgCIAEoCzIjLlBsYWludGV4dENvbnRlbn'
|
||||
'QuUmV0cnlFcnJvck1lc3NhZ2VIAVIRcmV0cnlDb250cm9sRXJyb3KIAQEaEwoRUmV0cnlFcnJv'
|
||||
'ck1lc3NhZ2UahAEKFkRlY3J5cHRpb25FcnJvck1lc3NhZ2USQQoEdHlwZRgBIAEoDjItLlBsYW'
|
||||
'ludGV4dENvbnRlbnQuRGVjcnlwdGlvbkVycm9yTWVzc2FnZS5UeXBlUgR0eXBlIicKBFR5cGUS'
|
||||
'CwoHVU5LTk9XThAAEhIKDlBSRUtFWV9VTktOT1dOEAFCGQoXX2RlY3J5cHRpb25FcnJvck1lc3'
|
||||
'NhZ2VCFAoSX3JldHJ5Q29udHJvbEVycm9y');
|
||||
|
||||
@$core.Deprecated('Use encryptedContentDescriptor instead')
|
||||
const EncryptedContent$json = {
|
||||
|
|
|
|||
|
|
@ -16,6 +16,9 @@ message Message {
|
|||
|
||||
message PlaintextContent {
|
||||
optional DecryptionErrorMessage decryptionErrorMessage = 1;
|
||||
optional RetryErrorMessage retryControlError = 2;
|
||||
|
||||
message RetryErrorMessage { }
|
||||
|
||||
message DecryptionErrorMessage {
|
||||
enum Type {
|
||||
|
|
|
|||
|
|
@ -485,11 +485,18 @@ class ApiService {
|
|||
return sendRequestSync(req);
|
||||
}
|
||||
|
||||
Future<Result> getUsername(int userId) async {
|
||||
Future<Response_UserData?> getUserById(int userId) async {
|
||||
final get = ApplicationData_GetUserById()..userId = Int64(userId);
|
||||
final appData = ApplicationData()..getuserbyid = get;
|
||||
final req = createClientToServerFromApplicationData(appData);
|
||||
return sendRequestSync(req, contactId: userId);
|
||||
final res = await sendRequestSync(req);
|
||||
if (res.isSuccess) {
|
||||
final ok = res.value as server.Response_Ok;
|
||||
if (ok.hasUserdata()) {
|
||||
return ok.userdata;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<Result> downloadDone(List<int> token) async {
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import 'package:twonly/src/services/notifications/setup.notifications.dart';
|
|||
import 'package:twonly/src/utils/log.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
|
||||
Future<void> handleContactRequest(
|
||||
Future<bool> handleContactRequest(
|
||||
int fromUserId,
|
||||
EncryptedContent_ContactRequest contactRequest,
|
||||
) async {
|
||||
|
|
@ -34,24 +34,23 @@ Future<void> handleContactRequest(
|
|||
),
|
||||
),
|
||||
);
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// Request the username by the server so an attacker can not
|
||||
// forge the displayed username in the contact request
|
||||
final username = await apiService.getUsername(fromUserId);
|
||||
if (username.isSuccess) {
|
||||
// ignore: avoid_dynamic_calls
|
||||
final name = username.value.userdata.username as Uint8List;
|
||||
final user = await apiService.getUserById(fromUserId);
|
||||
if (user == null) {
|
||||
return false;
|
||||
}
|
||||
await twonlyDB.contactsDao.insertOnConflictUpdate(
|
||||
ContactsCompanion(
|
||||
username: Value(utf8.decode(name)),
|
||||
username: Value(utf8.decode(user.username)),
|
||||
userId: Value(fromUserId),
|
||||
requested: const Value(true),
|
||||
deletedByUser: const Value(false),
|
||||
),
|
||||
);
|
||||
}
|
||||
await setupNotificationWithUsers();
|
||||
case EncryptedContent_ContactRequest_Type.ACCEPT:
|
||||
Log.info('Got a contact accept from $fromUserId');
|
||||
|
|
@ -82,6 +81,7 @@ Future<void> handleContactRequest(
|
|||
),
|
||||
);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
Future<void> handleContactUpdate(
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import 'package:twonly/globals.dart';
|
|||
import 'package:twonly/src/database/tables/groups.table.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart';
|
||||
import 'package:twonly/src/services/api/messages.dart';
|
||||
import 'package:twonly/src/services/group.services.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
|
||||
|
|
@ -26,9 +27,11 @@ Future<void> handleGroupCreate(
|
|||
final group = await twonlyDB.groupsDao.createNewGroup(
|
||||
GroupsCompanion(
|
||||
groupId: Value(groupId),
|
||||
stateVersionId: const Value(1),
|
||||
stateVersionId: const Value(0),
|
||||
stateEncryptionKey: Value(Uint8List.fromList(newGroup.stateKey)),
|
||||
myGroupPrivateKey: Value(myGroupKey.getPrivateKey().serialize()),
|
||||
groupName: const Value(''),
|
||||
joinedGroup: const Value(false),
|
||||
),
|
||||
);
|
||||
|
||||
|
|
@ -63,6 +66,15 @@ Future<void> handleGroupCreate(
|
|||
|
||||
// can be done in the background -> websocket message can be ACK
|
||||
unawaited(fetchGroupStatesForUnjoinedGroups());
|
||||
|
||||
await sendCipherTextToGroup(
|
||||
groupId,
|
||||
EncryptedContent(
|
||||
groupJoin: EncryptedContent_GroupJoin(
|
||||
groupPublicKey: myGroupKey.getPublicKey().serialize(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> handleGroupUpdate(
|
||||
|
|
@ -71,8 +83,23 @@ Future<void> handleGroupUpdate(
|
|||
EncryptedContent_GroupUpdate update,
|
||||
) async {}
|
||||
|
||||
Future<void> handleGroupJoin(
|
||||
Future<bool> handleGroupJoin(
|
||||
int fromUserId,
|
||||
String groupId,
|
||||
EncryptedContent_GroupJoin join,
|
||||
) async {}
|
||||
) async {
|
||||
if (await twonlyDB.contactsDao.getContactById(fromUserId) == null) {
|
||||
if (!await addNewHiddenContact(fromUserId)) {
|
||||
Log.error('Got group join, but could not load contact.');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
await twonlyDB.groupsDao.updateMember(
|
||||
groupId,
|
||||
fromUserId,
|
||||
GroupMembersCompanion(
|
||||
groupPublicKey: Value(Uint8List.fromList(join.groupPublicKey)),
|
||||
),
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -187,14 +187,18 @@ Future<void> insertAndSendTextMessage(
|
|||
encryptedContent.textMessage.quoteMessageId = quotesMessageId;
|
||||
}
|
||||
|
||||
await sendCipherTextToGroup(groupId, encryptedContent, message.messageId);
|
||||
await sendCipherTextToGroup(
|
||||
groupId,
|
||||
encryptedContent,
|
||||
messageId: message.messageId,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> sendCipherTextToGroup(
|
||||
String groupId,
|
||||
pb.EncryptedContent encryptedContent,
|
||||
pb.EncryptedContent encryptedContent, {
|
||||
String? messageId,
|
||||
) async {
|
||||
}) async {
|
||||
final groupMembers = await twonlyDB.groupsDao.getGroupMembers(groupId);
|
||||
|
||||
await twonlyDB.groupsDao.increaseLastMessageExchange(groupId, DateTime.now());
|
||||
|
|
|
|||
|
|
@ -74,11 +74,23 @@ Future<void> handleClient2ClientMessage(int fromUserId, Uint8List body) async {
|
|||
await twonlyDB.receiptsDao.confirmReceipt(receiptId, fromUserId);
|
||||
|
||||
case Message_Type.PLAINTEXT_CONTENT:
|
||||
if (message.hasPlaintextContent() &&
|
||||
message.plaintextContent.hasDecryptionErrorMessage()) {
|
||||
var retry = false;
|
||||
if (message.hasPlaintextContent()) {
|
||||
if (message.plaintextContent.hasDecryptionErrorMessage()) {
|
||||
Log.info(
|
||||
'Got decryption error: ${message.plaintextContent.decryptionErrorMessage.type} for $receiptId',
|
||||
);
|
||||
retry = true;
|
||||
}
|
||||
if (message.plaintextContent.hasRetryControlError()) {
|
||||
Log.info(
|
||||
'Got access control error for $receiptId. Resending message.',
|
||||
);
|
||||
retry = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (retry) {
|
||||
final newReceiptId = uuid.v4();
|
||||
await twonlyDB.receiptsDao.updateReceipt(
|
||||
receiptId,
|
||||
|
|
@ -162,7 +174,10 @@ Future<PlaintextContent?> handleEncryptedMessage(
|
|||
final senderProfileCounter = await checkForProfileUpdate(fromUserId, content);
|
||||
|
||||
if (content.hasContactRequest()) {
|
||||
await handleContactRequest(fromUserId, content.contactRequest);
|
||||
if (!await handleContactRequest(fromUserId, content.contactRequest)) {
|
||||
return PlaintextContent()
|
||||
..retryControlError = PlaintextContent_RetryErrorMessage();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -206,15 +221,6 @@ Future<PlaintextContent?> handleEncryptedMessage(
|
|||
return null;
|
||||
}
|
||||
|
||||
if (content.hasGroupUpdate()) {
|
||||
await handleGroupUpdate(
|
||||
fromUserId,
|
||||
content.groupId,
|
||||
content.groupUpdate,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (content.hasGroupCreate()) {
|
||||
await handleGroupCreate(
|
||||
fromUserId,
|
||||
|
|
@ -224,15 +230,6 @@ Future<PlaintextContent?> handleEncryptedMessage(
|
|||
return null;
|
||||
}
|
||||
|
||||
if (content.hasGroupJoin()) {
|
||||
await handleGroupJoin(
|
||||
fromUserId,
|
||||
content.groupId,
|
||||
content.groupJoin,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Verify that the user is (still) in that group...
|
||||
if (!await twonlyDB.groupsDao.isContactInGroup(fromUserId, content.groupId)) {
|
||||
if (getUUIDforDirectChat(gUser.userId, fromUserId) == content.groupId) {
|
||||
|
|
@ -255,11 +252,41 @@ Future<PlaintextContent?> handleEncryptedMessage(
|
|||
),
|
||||
);
|
||||
} else {
|
||||
if (content.hasGroupJoin()) {
|
||||
Log.error(
|
||||
'Got group join message, but group does not exists yet, retry later. As probably the GroupCreate was not yet received.',
|
||||
);
|
||||
// In case the group join was received before the GroupCreate the sender should send it later again.
|
||||
return PlaintextContent()
|
||||
..retryControlError = PlaintextContent_RetryErrorMessage();
|
||||
}
|
||||
|
||||
Log.error('User $fromUserId tried to access group ${content.groupId}.');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (content.hasGroupUpdate()) {
|
||||
await handleGroupUpdate(
|
||||
fromUserId,
|
||||
content.groupId,
|
||||
content.groupUpdate,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (content.hasGroupJoin()) {
|
||||
if (!await handleGroupJoin(
|
||||
fromUserId,
|
||||
content.groupId,
|
||||
content.groupJoin,
|
||||
)) {
|
||||
return PlaintextContent()
|
||||
..retryControlError = PlaintextContent_RetryErrorMessage();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
if (content.hasTextMessage()) {
|
||||
await handleTextMessage(
|
||||
fromUserId,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:cryptography_flutter_plus/cryptography_flutter_plus.dart';
|
||||
import 'package:cryptography_plus/cryptography_plus.dart';
|
||||
import 'package:drift/drift.dart' show Value;
|
||||
|
|
@ -13,6 +15,7 @@ import 'package:twonly/src/model/protobuf/api/http/http_requests.pb.dart';
|
|||
import 'package:twonly/src/model/protobuf/client/generated/groups.pb.dart';
|
||||
import 'package:twonly/src/model/protobuf/client/generated/messages.pbserver.dart';
|
||||
import 'package:twonly/src/services/api/messages.dart';
|
||||
import 'package:twonly/src/services/signal/session.signal.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
|
||||
|
|
@ -29,7 +32,7 @@ Future<bool> createNewGroup(String groupName, List<Contact> members) async {
|
|||
final memberIds = members.map((x) => Int64(x.userId)).toList();
|
||||
|
||||
final groupState = EncryptedGroupState(
|
||||
memberIds: memberIds,
|
||||
memberIds: [Int64(gUser.userId)] + memberIds,
|
||||
adminIds: [Int64(gUser.userId)],
|
||||
groupName: groupName,
|
||||
deleteMessagesAfterMilliseconds:
|
||||
|
|
@ -88,6 +91,7 @@ Future<bool> createNewGroup(String groupName, List<Contact> members) async {
|
|||
stateEncryptionKey: Value(stateEncryptionKey),
|
||||
stateVersionId: const Value(1),
|
||||
myGroupPrivateKey: Value(myGroupKey.getPrivateKey().serialize()),
|
||||
joinedGroup: const Value(true),
|
||||
),
|
||||
);
|
||||
|
||||
|
|
@ -125,7 +129,6 @@ Future<bool> createNewGroup(String groupName, List<Contact> members) async {
|
|||
groupPublicKey: myGroupKey.getPublicKey().serialize(),
|
||||
),
|
||||
),
|
||||
null,
|
||||
);
|
||||
|
||||
return true;
|
||||
|
|
@ -134,11 +137,15 @@ Future<bool> createNewGroup(String groupName, List<Contact> members) async {
|
|||
Future<void> fetchGroupStatesForUnjoinedGroups() async {
|
||||
final groups = await twonlyDB.groupsDao.getAllNotJoinedGroups();
|
||||
|
||||
for (final group in groups) {}
|
||||
for (final group in groups) {
|
||||
await fetchGroupState(group);
|
||||
}
|
||||
}
|
||||
|
||||
Future<GroupState?> fetchGroupState(Group group) async {
|
||||
Future<bool> fetchGroupState(Group group) async {
|
||||
try {
|
||||
var isSuccess = true;
|
||||
|
||||
final response = await http
|
||||
.get(
|
||||
Uri.parse('${getGroupStateUrl()}/${group.groupId}'),
|
||||
|
|
@ -149,7 +156,7 @@ Future<GroupState?> fetchGroupState(Group group) async {
|
|||
Log.error(
|
||||
'Could not load group state. Got status code ${response.statusCode} from server.',
|
||||
);
|
||||
return null;
|
||||
return false;
|
||||
}
|
||||
|
||||
final groupStateServer = GroupState.fromBuffer(response.bodyBytes);
|
||||
|
|
@ -166,14 +173,128 @@ Future<GroupState?> fetchGroupState(Group group) async {
|
|||
final encryptedGroupState =
|
||||
EncryptedGroupState.fromBuffer(encryptedGroupStateRaw);
|
||||
|
||||
encryptedGroupState.adminIds;
|
||||
encryptedGroupState.memberIds;
|
||||
encryptedGroupState.groupName;
|
||||
encryptedGroupState.deleteMessagesAfterMilliseconds;
|
||||
encryptedGroupState.deleteMessagesAfterMilliseconds;
|
||||
groupStateServer.versionId;
|
||||
if (group.stateVersionId >= groupStateServer.versionId.toInt()) {
|
||||
Log.error('Group ${group.groupId} has newest group state');
|
||||
return false;
|
||||
}
|
||||
|
||||
final isGroupAdmin = encryptedGroupState.adminIds
|
||||
.firstWhereOrNull((t) => t.toInt() == gUser.userId) !=
|
||||
null;
|
||||
|
||||
await twonlyDB.groupsDao.updateGroup(
|
||||
group.groupId,
|
||||
GroupsCompanion(
|
||||
groupName: Value(encryptedGroupState.groupName),
|
||||
deleteMessagesAfterMilliseconds: Value(
|
||||
encryptedGroupState.deleteMessagesAfterMilliseconds.toInt(),
|
||||
),
|
||||
isGroupAdmin: Value(isGroupAdmin),
|
||||
),
|
||||
);
|
||||
|
||||
var currentGroupMembers =
|
||||
await twonlyDB.groupsDao.getGroupMembers(group.groupId);
|
||||
|
||||
// First find and insert NEW members
|
||||
for (final memberId in encryptedGroupState.memberIds) {
|
||||
if (currentGroupMembers.any((t) => t.contactId == memberId.toInt())) {
|
||||
// User is already in the database
|
||||
continue;
|
||||
}
|
||||
Log.info('New member in the GROUP state: $memberId');
|
||||
|
||||
var inContacts = true;
|
||||
|
||||
if (await twonlyDB.contactsDao.getContactById(memberId.toInt()) == null) {
|
||||
// User is not yet in the contacts, add him in the hidden. So he is not in the contact list / needs to be
|
||||
// requested separately.
|
||||
if (!await addNewHiddenContact(memberId.toInt())) {
|
||||
Log.error('Could not request member ID will retry later.');
|
||||
isSuccess = false;
|
||||
inContacts = false;
|
||||
}
|
||||
}
|
||||
if (inContacts) {
|
||||
// User is already a contact, so just add him to the group members list
|
||||
await twonlyDB.groupsDao.insertGroupMember(
|
||||
GroupMembersCompanion(
|
||||
groupId: Value(group.groupId),
|
||||
contactId: Value(memberId.toInt()),
|
||||
memberState: const Value(MemberState.normal),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// check if there is a member which is not in the server list...
|
||||
|
||||
// update the current members list
|
||||
currentGroupMembers =
|
||||
await twonlyDB.groupsDao.getGroupMembers(group.groupId);
|
||||
|
||||
for (final member in currentGroupMembers) {
|
||||
// Member is not any more in the members list
|
||||
if (!encryptedGroupState.memberIds.contains(Int64(member.contactId))) {
|
||||
await twonlyDB.groupsDao.removeMember(group.groupId, member.contactId);
|
||||
continue;
|
||||
}
|
||||
|
||||
MemberState? newMemberState;
|
||||
|
||||
if (encryptedGroupState.adminIds.contains(Int64(member.contactId))) {
|
||||
if (member.memberState == MemberState.normal) {
|
||||
// user was promoted
|
||||
newMemberState = MemberState.admin;
|
||||
}
|
||||
} else if (member.memberState == MemberState.admin) {
|
||||
// user was demoted
|
||||
newMemberState = MemberState.normal;
|
||||
}
|
||||
|
||||
if (newMemberState != null) {
|
||||
await twonlyDB.groupsDao.updateMember(
|
||||
group.groupId,
|
||||
member.contactId,
|
||||
GroupMembersCompanion(
|
||||
memberState: Value(newMemberState),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (isSuccess) {
|
||||
// in case not all members could be loaded from the server,
|
||||
// this will ensure it will be tried again later
|
||||
await twonlyDB.groupsDao.updateGroup(
|
||||
group.groupId,
|
||||
GroupsCompanion(
|
||||
stateVersionId: Value(groupStateServer.versionId.toInt()),
|
||||
joinedGroup: const Value(true),
|
||||
),
|
||||
);
|
||||
}
|
||||
return true;
|
||||
} catch (e) {
|
||||
Log.error(e);
|
||||
return null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> addNewHiddenContact(int contactId) async {
|
||||
final userData = await apiService.getUserById(contactId);
|
||||
if (userData == null) {
|
||||
Log.error('Could not load contact informations');
|
||||
return false;
|
||||
}
|
||||
await twonlyDB.contactsDao.insertOnConflictUpdate(
|
||||
ContactsCompanion(
|
||||
username: Value(utf8.decode(userData.username)),
|
||||
userId: Value(contactId),
|
||||
deletedByUser:
|
||||
const Value(true), // this will hide the contact in the contact list
|
||||
),
|
||||
);
|
||||
await createNewSignalSession(userData);
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,17 +30,22 @@ Color getMessageColor(Message message) {
|
|||
}
|
||||
|
||||
class ChatItem {
|
||||
const ChatItem._({this.message, this.date});
|
||||
const ChatItem._({this.message, this.date, this.lastOpenedPosition});
|
||||
factory ChatItem.date(DateTime date) {
|
||||
return ChatItem._(date: date);
|
||||
}
|
||||
factory ChatItem.message(Message message) {
|
||||
return ChatItem._(message: message);
|
||||
}
|
||||
factory ChatItem.lastOpenedPosition(List<Contact> contacts) {
|
||||
return ChatItem._(lastOpenedPosition: contacts);
|
||||
}
|
||||
final Message? message;
|
||||
final DateTime? date;
|
||||
final List<Contact>? lastOpenedPosition;
|
||||
bool get isMessage => message != null;
|
||||
bool get isDate => date != null;
|
||||
bool get isLastOpenedPosition => lastOpenedPosition != null;
|
||||
}
|
||||
|
||||
/// Displays detailed information about a SampleItem.
|
||||
|
|
@ -60,7 +65,12 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
|
|||
String currentInputText = '';
|
||||
late StreamSubscription<Group?> userSub;
|
||||
late StreamSubscription<List<Message>> messageSub;
|
||||
late StreamSubscription<Future<List<(Message, Contact)>>>?
|
||||
lastOpenedMessageByContactSub;
|
||||
|
||||
List<ChatItem> messages = [];
|
||||
List<Message> allMessages = [];
|
||||
List<(Message, Contact)> lastOpenedMessageByContact = [];
|
||||
List<MemoryItem> galleryItems = [];
|
||||
Message? quotesMessage;
|
||||
GlobalKey verifyShieldKey = GlobalKey();
|
||||
|
|
@ -87,6 +97,7 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
|
|||
void dispose() {
|
||||
userSub.cancel();
|
||||
messageSub.cancel();
|
||||
lastOpenedMessageByContactSub?.cancel();
|
||||
tutorial?.cancel();
|
||||
textFieldFocus.dispose();
|
||||
super.dispose();
|
||||
|
|
@ -103,14 +114,36 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
|
|||
});
|
||||
});
|
||||
|
||||
if (!widget.group.isDirectChat) {
|
||||
final lastOpenedStream =
|
||||
twonlyDB.messagesDao.watchLastOpenedMessagePerContact(group.groupId);
|
||||
lastOpenedMessageByContactSub =
|
||||
lastOpenedStream.listen((lastActionsFuture) async {
|
||||
final update = await lastActionsFuture;
|
||||
lastOpenedMessageByContact = update;
|
||||
await setMessages(allMessages, update);
|
||||
});
|
||||
}
|
||||
|
||||
final msgStream = twonlyDB.messagesDao.watchByGroupId(group.groupId);
|
||||
messageSub = msgStream.listen((newMessages) async {
|
||||
messageSub = msgStream.listen((update) async {
|
||||
allMessages = update;
|
||||
|
||||
/// In case a message is not open yet the message is updated, which will trigger this watch to be called again.
|
||||
/// So as long as the Mutex is locked just return...
|
||||
if (protectMessageUpdating.isLocked) {
|
||||
return;
|
||||
// return;
|
||||
}
|
||||
await protectMessageUpdating.protect(() async {
|
||||
await setMessages(update, lastOpenedMessageByContact);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> setMessages(
|
||||
List<Message> newMessages,
|
||||
List<(Message, Contact)> lastOpenedMessageByContact,
|
||||
) async {
|
||||
await flutterLocalNotificationsPlugin.cancelAll();
|
||||
|
||||
final chatItems = <ChatItem>[];
|
||||
|
|
@ -119,8 +152,22 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
|
|||
DateTime? lastDate;
|
||||
|
||||
final openedMessages = <int, List<String>>{};
|
||||
final lastOpenedMessageToContact = <String, List<Contact>>{};
|
||||
|
||||
final myLastMessageIndex =
|
||||
newMessages.lastIndexWhere((t) => t.senderId == null);
|
||||
|
||||
for (final opened in lastOpenedMessageByContact) {
|
||||
if (!lastOpenedMessageToContact.containsKey(opened.$1.messageId)) {
|
||||
lastOpenedMessageToContact[opened.$1.messageId] = [opened.$2];
|
||||
} else {
|
||||
lastOpenedMessageToContact[opened.$1.messageId]!.add(opened.$2);
|
||||
}
|
||||
}
|
||||
var index = 0;
|
||||
|
||||
for (final msg in newMessages) {
|
||||
index += 1;
|
||||
if (msg.type == MessageType.text &&
|
||||
msg.senderId != null &&
|
||||
msg.openedAt == null) {
|
||||
|
|
@ -142,6 +189,16 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
|
|||
lastDate = msg.createdAt;
|
||||
}
|
||||
chatItems.add(ChatItem.message(msg));
|
||||
|
||||
if (index <= myLastMessageIndex || index == newMessages.length) {
|
||||
if (lastOpenedMessageToContact.containsKey(msg.messageId)) {
|
||||
chatItems.add(
|
||||
ChatItem.lastOpenedPosition(
|
||||
lastOpenedMessageToContact[msg.messageId]!,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (final contactId in openedMessages.keys) {
|
||||
|
|
@ -159,8 +216,6 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
|
|||
final items = await MemoryItem.convertFromMessages(storedMediaFiles);
|
||||
galleryItems = items.values.toList();
|
||||
setState(() {});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _sendMessage() async {
|
||||
|
|
@ -275,6 +330,18 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
|
|||
return ChatDateChip(
|
||||
item: messages[i],
|
||||
);
|
||||
} else if (messages[i].isLastOpenedPosition) {
|
||||
return Wrap(
|
||||
spacing: 8,
|
||||
alignment: WrapAlignment.center,
|
||||
children: messages[i].lastOpenedPosition!.map((w) {
|
||||
return AvatarIcon(
|
||||
key: GlobalKey(),
|
||||
contact: w,
|
||||
fontSize: 12,
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
} else {
|
||||
final chatMessage = messages[i].message!;
|
||||
return Transform.translate(
|
||||
|
|
|
|||
|
|
@ -63,7 +63,6 @@ class _AllReactionsViewState extends State<AllReactionsView> {
|
|||
remove: true,
|
||||
),
|
||||
),
|
||||
null,
|
||||
);
|
||||
if (mounted) Navigator.pop(context);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import 'package:twonly/src/views/chats/chat_messages_components/chat_text_entry.
|
|||
import 'package:twonly/src/views/chats/chat_messages_components/message_actions.dart';
|
||||
import 'package:twonly/src/views/chats/chat_messages_components/message_context_menu.dart';
|
||||
import 'package:twonly/src/views/chats/chat_messages_components/response_container.dart';
|
||||
import 'package:twonly/src/views/components/avatar_icon.component.dart';
|
||||
|
||||
class ChatListEntry extends StatefulWidget {
|
||||
const ChatListEntry({
|
||||
|
|
@ -86,7 +87,7 @@ class _ChatListEntryState extends State<ChatListEntry> {
|
|||
Widget build(BuildContext context) {
|
||||
final right = widget.message.senderId == null;
|
||||
|
||||
final (padding, borderRadius) = getMessageLayout(
|
||||
final (padding, borderRadius, hideContactAvatar) = getMessageLayout(
|
||||
widget.message,
|
||||
widget.prevMessage,
|
||||
widget.nextMessage,
|
||||
|
|
@ -172,19 +173,36 @@ class _ChatListEntryState extends State<ChatListEntry> {
|
|||
|
||||
return Align(
|
||||
alignment: right ? Alignment.centerRight : Alignment.centerLeft,
|
||||
child: Padding(padding: padding, child: child),
|
||||
child: Padding(
|
||||
padding: padding,
|
||||
child: Row(
|
||||
mainAxisAlignment:
|
||||
right ? MainAxisAlignment.end : MainAxisAlignment.start,
|
||||
children: [
|
||||
if (!right && !widget.group.isDirectChat)
|
||||
hideContactAvatar
|
||||
? const SizedBox(width: 24)
|
||||
: AvatarIcon(
|
||||
contactId: widget.message.senderId,
|
||||
fontSize: 12,
|
||||
),
|
||||
child,
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
(EdgeInsetsGeometry, BorderRadius) getMessageLayout(
|
||||
(EdgeInsetsGeometry, BorderRadius, bool) getMessageLayout(
|
||||
Message message,
|
||||
Message? prevMessage,
|
||||
Message? nextMessage,
|
||||
bool hasReactions,
|
||||
) {
|
||||
var bottom = 20.0;
|
||||
var top = 0.0;
|
||||
var bottom = 10.0;
|
||||
var top = 10.0;
|
||||
var hideContactAvatar = false;
|
||||
|
||||
var topLeft = 12.0;
|
||||
var topRight = 12.0;
|
||||
|
|
@ -209,6 +227,7 @@ class _ChatListEntryState extends State<ChatListEntry> {
|
|||
if (combinesWidthNext) {
|
||||
bottom = 0;
|
||||
bottomLeft = 5.0;
|
||||
hideContactAvatar = true;
|
||||
}
|
||||
|
||||
if (message.senderId == null) {
|
||||
|
|
@ -219,6 +238,7 @@ class _ChatListEntryState extends State<ChatListEntry> {
|
|||
final tmp2 = bottomLeft;
|
||||
bottomLeft = bottomRight;
|
||||
bottomRight = tmp2;
|
||||
hideContactAvatar = true;
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -228,6 +248,7 @@ class _ChatListEntryState extends State<ChatListEntry> {
|
|||
topRight: Radius.circular(topRight),
|
||||
bottomRight: Radius.circular(bottomRight),
|
||||
bottomLeft: Radius.circular(bottomLeft),
|
||||
)
|
||||
),
|
||||
hideContactAvatar
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -76,7 +76,6 @@ class _ChatMediaEntryState extends State<ChatMediaEntry> {
|
|||
targetMessageId: widget.message.messageId,
|
||||
),
|
||||
),
|
||||
null,
|
||||
);
|
||||
await twonlyDB.messagesDao.updateMessageId(
|
||||
widget.message.messageId,
|
||||
|
|
|
|||
|
|
@ -65,7 +65,6 @@ class MessageContextMenu extends StatelessWidget {
|
|||
remove: false,
|
||||
),
|
||||
),
|
||||
null,
|
||||
);
|
||||
},
|
||||
icon: FontAwesomeIcons.faceLaugh,
|
||||
|
|
@ -124,7 +123,6 @@ class MessageContextMenu extends StatelessWidget {
|
|||
senderMessageId: message.messageId,
|
||||
),
|
||||
),
|
||||
null,
|
||||
);
|
||||
} else {
|
||||
await twonlyDB.messagesDao
|
||||
|
|
@ -225,7 +223,6 @@ Future<void> editTextMessage(BuildContext context, Message message) async {
|
|||
),
|
||||
),
|
||||
),
|
||||
null,
|
||||
);
|
||||
}
|
||||
if (!context.mounted) return;
|
||||
|
|
|
|||
|
|
@ -290,7 +290,6 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
|||
targetMessageId: currentMessage!.messageId,
|
||||
),
|
||||
),
|
||||
null,
|
||||
);
|
||||
setState(() {
|
||||
imageSaved = true;
|
||||
|
|
|
|||
|
|
@ -47,7 +47,6 @@ class _EmojiReactionWidgetState extends State<EmojiReactionWidget> {
|
|||
remove: false,
|
||||
),
|
||||
),
|
||||
null,
|
||||
);
|
||||
|
||||
setState(() {
|
||||
|
|
|
|||
|
|
@ -122,7 +122,9 @@ class _MessageInfoViewState extends State<MessageInfoView> {
|
|||
}
|
||||
|
||||
columns.add(
|
||||
Row(
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Row(
|
||||
children: [
|
||||
AvatarIcon(
|
||||
contact: groupMember.$2,
|
||||
|
|
@ -151,6 +153,7 @@ class _MessageInfoViewState extends State<MessageInfoView> {
|
|||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return columns;
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import 'package:flutter_svg/svg.dart';
|
|||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/model/json/userdata.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
|
||||
class AvatarIcon extends StatefulWidget {
|
||||
|
|
@ -11,12 +10,14 @@ class AvatarIcon extends StatefulWidget {
|
|||
super.key,
|
||||
this.group,
|
||||
this.contact,
|
||||
this.contactId,
|
||||
this.userData,
|
||||
this.fontSize = 20,
|
||||
this.color,
|
||||
});
|
||||
final Group? group;
|
||||
final Contact? contact;
|
||||
final int? contactId;
|
||||
final UserData? userData;
|
||||
final double? fontSize;
|
||||
final Color? color;
|
||||
|
|
@ -54,6 +55,12 @@ class _AvatarIconState extends State<AvatarIcon> {
|
|||
_avatarSVGs.add(widget.userData!.avatarSvg!);
|
||||
} else if (widget.contact?.avatarSvgCompressed != null) {
|
||||
_avatarSVGs.add(getAvatarSvg(widget.contact!.avatarSvgCompressed!));
|
||||
} else if (widget.contactId != null) {
|
||||
final contact =
|
||||
await twonlyDB.contactsDao.getContactById(widget.contactId!);
|
||||
if (contact != null && contact.avatarSvgCompressed != null) {
|
||||
_avatarSVGs.add(getAvatarSvg(contact.avatarSvgCompressed!));
|
||||
}
|
||||
}
|
||||
if (mounted) setState(() {});
|
||||
}
|
||||
|
|
@ -62,6 +69,62 @@ class _AvatarIconState extends State<AvatarIcon> {
|
|||
Widget build(BuildContext context) {
|
||||
final proSize = (widget.fontSize == null) ? 40 : (widget.fontSize! * 2);
|
||||
|
||||
Widget avatars = SvgPicture.asset('assets/images/default_avatar.svg');
|
||||
|
||||
if (_avatarSVGs.length == 1) {
|
||||
avatars = SvgPicture.string(
|
||||
_avatarSVGs.first,
|
||||
errorBuilder: (a, b, c) => avatars,
|
||||
);
|
||||
} else if (_avatarSVGs.length >= 2) {
|
||||
final a = SvgPicture.string(
|
||||
_avatarSVGs.first,
|
||||
errorBuilder: (a, b, c) => avatars,
|
||||
);
|
||||
final b = SvgPicture.string(
|
||||
_avatarSVGs[1],
|
||||
errorBuilder: (a, b, c) => avatars,
|
||||
);
|
||||
if (_avatarSVGs.length >= 3) {
|
||||
final c = SvgPicture.string(
|
||||
_avatarSVGs[2],
|
||||
errorBuilder: (a, b, c) => avatars,
|
||||
);
|
||||
avatars = Stack(
|
||||
children: [
|
||||
Transform.translate(
|
||||
offset: const Offset(-15, 5),
|
||||
child: Transform.scale(
|
||||
scale: 0.8,
|
||||
child: c,
|
||||
),
|
||||
),
|
||||
Transform.translate(
|
||||
offset: const Offset(15, 5),
|
||||
child: Transform.scale(
|
||||
scale: 0.8,
|
||||
child: b,
|
||||
),
|
||||
),
|
||||
a,
|
||||
],
|
||||
);
|
||||
} else {
|
||||
avatars = Stack(
|
||||
children: [
|
||||
Transform.translate(
|
||||
offset: const Offset(-10, 5),
|
||||
child: Transform.scale(
|
||||
scale: 0.8,
|
||||
child: b,
|
||||
),
|
||||
),
|
||||
Transform.translate(offset: const Offset(10, 0), child: a),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return Container(
|
||||
constraints: BoxConstraints(
|
||||
minHeight: 2 * (widget.fontSize ?? 20),
|
||||
|
|
@ -76,17 +139,7 @@ class _AvatarIconState extends State<AvatarIcon> {
|
|||
height: proSize as double,
|
||||
width: proSize,
|
||||
color: widget.color,
|
||||
child: Center(
|
||||
child: _avatarSVGs.isEmpty
|
||||
? SvgPicture.asset('assets/images/default_avatar.svg')
|
||||
: SvgPicture.string(
|
||||
_avatarSVGs.first,
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
Log.error('$error');
|
||||
return Container();
|
||||
},
|
||||
),
|
||||
),
|
||||
child: Center(child: avatars),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -46,9 +46,11 @@ class _FlameCounterWidgetState extends State<FlameCounterWidget> {
|
|||
isBestFriend = gUser.myBestFriendGroupId == groupId;
|
||||
final stream = twonlyDB.groupsDao.watchFlameCounter(groupId);
|
||||
flameCounterSub = stream.listen((counter) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
flameCounter = counter;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,6 +36,8 @@ class _GroupCreateSelectGroupNameViewState
|
|||
await createNewGroup(textFieldGroupName.text, widget.selectedUsers);
|
||||
if (wasSuccess) {
|
||||
// POP
|
||||
Navigator.popUntil(context, (route) => route.isFirst);
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
|
|
|
|||
Loading…
Reference in a new issue