mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-01-15 16:08:40 +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));
|
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 {
|
Future<List<Contact>> getContactsByUsername(String username) async {
|
||||||
return (select(contacts)..where((t) => t.username.equals(username))).get();
|
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';
|
part 'groups.dao.g.dart';
|
||||||
|
|
||||||
@DriftAccessor(tables: [Groups, GroupMembers, GroupHistories])
|
@DriftAccessor(
|
||||||
|
tables: [
|
||||||
|
Groups,
|
||||||
|
GroupMembers,
|
||||||
|
GroupHistories,
|
||||||
|
],
|
||||||
|
)
|
||||||
class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
|
class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
|
||||||
// this constructor is required so that the main database can create an instance
|
// this constructor is required so that the main database can create an instance
|
||||||
// of this object.
|
// of this object.
|
||||||
|
|
@ -54,6 +60,24 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
|
||||||
await into(groupHistories).insert(insertAction);
|
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(
|
Future<Group?> createNewDirectChat(
|
||||||
int contactId,
|
int contactId,
|
||||||
GroupsCompanion group,
|
GroupsCompanion group,
|
||||||
|
|
|
||||||
|
|
@ -399,6 +399,38 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
|
||||||
.getSingleOrNull();
|
.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) {
|
// Future<void> deleteMessagesByContactId(int contactId) {
|
||||||
// return (delete(messages)
|
// return (delete(messages)
|
||||||
// ..where(
|
// ..where(
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,7 @@ class Groups extends Table {
|
||||||
Set<Column> get primaryKey => {groupId};
|
Set<Column> get primaryKey => {groupId};
|
||||||
}
|
}
|
||||||
|
|
||||||
enum MemberState { normal, admin }
|
enum MemberState { normal, admin, leftGroup }
|
||||||
|
|
||||||
@DataClassName('GroupMember')
|
@DataClassName('GroupMember')
|
||||||
class GroupMembers extends Table {
|
class GroupMembers extends Table {
|
||||||
|
|
|
||||||
|
|
@ -112,6 +112,38 @@ class Message extends $pb.GeneratedMessage {
|
||||||
PlaintextContent ensurePlaintextContent() => $_ensure(3);
|
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 {
|
class PlaintextContent_DecryptionErrorMessage extends $pb.GeneratedMessage {
|
||||||
factory PlaintextContent_DecryptionErrorMessage({
|
factory PlaintextContent_DecryptionErrorMessage({
|
||||||
PlaintextContent_DecryptionErrorMessage_Type? type,
|
PlaintextContent_DecryptionErrorMessage_Type? type,
|
||||||
|
|
@ -165,11 +197,15 @@ class PlaintextContent_DecryptionErrorMessage extends $pb.GeneratedMessage {
|
||||||
class PlaintextContent extends $pb.GeneratedMessage {
|
class PlaintextContent extends $pb.GeneratedMessage {
|
||||||
factory PlaintextContent({
|
factory PlaintextContent({
|
||||||
PlaintextContent_DecryptionErrorMessage? decryptionErrorMessage,
|
PlaintextContent_DecryptionErrorMessage? decryptionErrorMessage,
|
||||||
|
PlaintextContent_RetryErrorMessage? retryControlError,
|
||||||
}) {
|
}) {
|
||||||
final $result = create();
|
final $result = create();
|
||||||
if (decryptionErrorMessage != null) {
|
if (decryptionErrorMessage != null) {
|
||||||
$result.decryptionErrorMessage = decryptionErrorMessage;
|
$result.decryptionErrorMessage = decryptionErrorMessage;
|
||||||
}
|
}
|
||||||
|
if (retryControlError != null) {
|
||||||
|
$result.retryControlError = retryControlError;
|
||||||
|
}
|
||||||
return $result;
|
return $result;
|
||||||
}
|
}
|
||||||
PlaintextContent._() : super();
|
PlaintextContent._() : super();
|
||||||
|
|
@ -178,6 +214,7 @@ class PlaintextContent extends $pb.GeneratedMessage {
|
||||||
|
|
||||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'PlaintextContent', createEmptyInstance: create)
|
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_DecryptionErrorMessage>(1, _omitFieldNames ? '' : 'decryptionErrorMessage', protoName: 'decryptionErrorMessage', subBuilder: PlaintextContent_DecryptionErrorMessage.create)
|
||||||
|
..aOM<PlaintextContent_RetryErrorMessage>(2, _omitFieldNames ? '' : 'retryControlError', protoName: 'retryControlError', subBuilder: PlaintextContent_RetryErrorMessage.create)
|
||||||
..hasRequiredFields = false
|
..hasRequiredFields = false
|
||||||
;
|
;
|
||||||
|
|
||||||
|
|
@ -212,6 +249,17 @@ class PlaintextContent extends $pb.GeneratedMessage {
|
||||||
void clearDecryptionErrorMessage() => clearField(1);
|
void clearDecryptionErrorMessage() => clearField(1);
|
||||||
@$pb.TagNumber(1)
|
@$pb.TagNumber(1)
|
||||||
PlaintextContent_DecryptionErrorMessage ensureDecryptionErrorMessage() => $_ensure(0);
|
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 {
|
class EncryptedContent_GroupCreate extends $pb.GeneratedMessage {
|
||||||
|
|
|
||||||
|
|
@ -56,13 +56,20 @@ const PlaintextContent$json = {
|
||||||
'1': 'PlaintextContent',
|
'1': 'PlaintextContent',
|
||||||
'2': [
|
'2': [
|
||||||
{'1': 'decryptionErrorMessage', '3': 1, '4': 1, '5': 11, '6': '.PlaintextContent.DecryptionErrorMessage', '9': 0, '10': 'decryptionErrorMessage', '17': true},
|
{'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': [
|
'8': [
|
||||||
{'1': '_decryptionErrorMessage'},
|
{'1': '_decryptionErrorMessage'},
|
||||||
|
{'1': '_retryControlError'},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@$core.Deprecated('Use plaintextContentDescriptor instead')
|
||||||
|
const PlaintextContent_RetryErrorMessage$json = {
|
||||||
|
'1': 'RetryErrorMessage',
|
||||||
|
};
|
||||||
|
|
||||||
@$core.Deprecated('Use plaintextContentDescriptor instead')
|
@$core.Deprecated('Use plaintextContentDescriptor instead')
|
||||||
const PlaintextContent_DecryptionErrorMessage$json = {
|
const PlaintextContent_DecryptionErrorMessage$json = {
|
||||||
'1': 'DecryptionErrorMessage',
|
'1': 'DecryptionErrorMessage',
|
||||||
|
|
@ -85,10 +92,12 @@ const PlaintextContent_DecryptionErrorMessage_Type$json = {
|
||||||
final $typed_data.Uint8List plaintextContentDescriptor = $convert.base64Decode(
|
final $typed_data.Uint8List plaintextContentDescriptor = $convert.base64Decode(
|
||||||
'ChBQbGFpbnRleHRDb250ZW50EmUKFmRlY3J5cHRpb25FcnJvck1lc3NhZ2UYASABKAsyKC5QbG'
|
'ChBQbGFpbnRleHRDb250ZW50EmUKFmRlY3J5cHRpb25FcnJvck1lc3NhZ2UYASABKAsyKC5QbG'
|
||||||
'FpbnRleHRDb250ZW50LkRlY3J5cHRpb25FcnJvck1lc3NhZ2VIAFIWZGVjcnlwdGlvbkVycm9y'
|
'FpbnRleHRDb250ZW50LkRlY3J5cHRpb25FcnJvck1lc3NhZ2VIAFIWZGVjcnlwdGlvbkVycm9y'
|
||||||
'TWVzc2FnZYgBARqEAQoWRGVjcnlwdGlvbkVycm9yTWVzc2FnZRJBCgR0eXBlGAEgASgOMi0uUG'
|
'TWVzc2FnZYgBARJWChFyZXRyeUNvbnRyb2xFcnJvchgCIAEoCzIjLlBsYWludGV4dENvbnRlbn'
|
||||||
'xhaW50ZXh0Q29udGVudC5EZWNyeXB0aW9uRXJyb3JNZXNzYWdlLlR5cGVSBHR5cGUiJwoEVHlw'
|
'QuUmV0cnlFcnJvck1lc3NhZ2VIAVIRcmV0cnlDb250cm9sRXJyb3KIAQEaEwoRUmV0cnlFcnJv'
|
||||||
'ZRILCgdVTktOT1dOEAASEgoOUFJFS0VZX1VOS05PV04QAUIZChdfZGVjcnlwdGlvbkVycm9yTW'
|
'ck1lc3NhZ2UahAEKFkRlY3J5cHRpb25FcnJvck1lc3NhZ2USQQoEdHlwZRgBIAEoDjItLlBsYW'
|
||||||
'Vzc2FnZQ==');
|
'ludGV4dENvbnRlbnQuRGVjcnlwdGlvbkVycm9yTWVzc2FnZS5UeXBlUgR0eXBlIicKBFR5cGUS'
|
||||||
|
'CwoHVU5LTk9XThAAEhIKDlBSRUtFWV9VTktOT1dOEAFCGQoXX2RlY3J5cHRpb25FcnJvck1lc3'
|
||||||
|
'NhZ2VCFAoSX3JldHJ5Q29udHJvbEVycm9y');
|
||||||
|
|
||||||
@$core.Deprecated('Use encryptedContentDescriptor instead')
|
@$core.Deprecated('Use encryptedContentDescriptor instead')
|
||||||
const EncryptedContent$json = {
|
const EncryptedContent$json = {
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,9 @@ message Message {
|
||||||
|
|
||||||
message PlaintextContent {
|
message PlaintextContent {
|
||||||
optional DecryptionErrorMessage decryptionErrorMessage = 1;
|
optional DecryptionErrorMessage decryptionErrorMessage = 1;
|
||||||
|
optional RetryErrorMessage retryControlError = 2;
|
||||||
|
|
||||||
|
message RetryErrorMessage { }
|
||||||
|
|
||||||
message DecryptionErrorMessage {
|
message DecryptionErrorMessage {
|
||||||
enum Type {
|
enum Type {
|
||||||
|
|
|
||||||
|
|
@ -485,11 +485,18 @@ class ApiService {
|
||||||
return sendRequestSync(req);
|
return sendRequestSync(req);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Result> getUsername(int userId) async {
|
Future<Response_UserData?> getUserById(int userId) async {
|
||||||
final get = ApplicationData_GetUserById()..userId = Int64(userId);
|
final get = ApplicationData_GetUserById()..userId = Int64(userId);
|
||||||
final appData = ApplicationData()..getuserbyid = get;
|
final appData = ApplicationData()..getuserbyid = get;
|
||||||
final req = createClientToServerFromApplicationData(appData);
|
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 {
|
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/log.dart';
|
||||||
import 'package:twonly/src/utils/misc.dart';
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
|
|
||||||
Future<void> handleContactRequest(
|
Future<bool> handleContactRequest(
|
||||||
int fromUserId,
|
int fromUserId,
|
||||||
EncryptedContent_ContactRequest contactRequest,
|
EncryptedContent_ContactRequest contactRequest,
|
||||||
) async {
|
) async {
|
||||||
|
|
@ -34,24 +34,23 @@ Future<void> handleContactRequest(
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
return;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Request the username by the server so an attacker can not
|
// Request the username by the server so an attacker can not
|
||||||
// forge the displayed username in the contact request
|
// forge the displayed username in the contact request
|
||||||
final username = await apiService.getUsername(fromUserId);
|
final user = await apiService.getUserById(fromUserId);
|
||||||
if (username.isSuccess) {
|
if (user == null) {
|
||||||
// ignore: avoid_dynamic_calls
|
return false;
|
||||||
final name = username.value.userdata.username as Uint8List;
|
|
||||||
await twonlyDB.contactsDao.insertOnConflictUpdate(
|
|
||||||
ContactsCompanion(
|
|
||||||
username: Value(utf8.decode(name)),
|
|
||||||
userId: Value(fromUserId),
|
|
||||||
requested: const Value(true),
|
|
||||||
deletedByUser: const Value(false),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
await twonlyDB.contactsDao.insertOnConflictUpdate(
|
||||||
|
ContactsCompanion(
|
||||||
|
username: Value(utf8.decode(user.username)),
|
||||||
|
userId: Value(fromUserId),
|
||||||
|
requested: const Value(true),
|
||||||
|
deletedByUser: const Value(false),
|
||||||
|
),
|
||||||
|
);
|
||||||
await setupNotificationWithUsers();
|
await setupNotificationWithUsers();
|
||||||
case EncryptedContent_ContactRequest_Type.ACCEPT:
|
case EncryptedContent_ContactRequest_Type.ACCEPT:
|
||||||
Log.info('Got a contact accept from $fromUserId');
|
Log.info('Got a contact accept from $fromUserId');
|
||||||
|
|
@ -82,6 +81,7 @@ Future<void> handleContactRequest(
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> handleContactUpdate(
|
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/tables/groups.table.dart';
|
||||||
import 'package:twonly/src/database/twonly.db.dart';
|
import 'package:twonly/src/database/twonly.db.dart';
|
||||||
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.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/services/group.services.dart';
|
||||||
import 'package:twonly/src/utils/log.dart';
|
import 'package:twonly/src/utils/log.dart';
|
||||||
|
|
||||||
|
|
@ -26,9 +27,11 @@ Future<void> handleGroupCreate(
|
||||||
final group = await twonlyDB.groupsDao.createNewGroup(
|
final group = await twonlyDB.groupsDao.createNewGroup(
|
||||||
GroupsCompanion(
|
GroupsCompanion(
|
||||||
groupId: Value(groupId),
|
groupId: Value(groupId),
|
||||||
stateVersionId: const Value(1),
|
stateVersionId: const Value(0),
|
||||||
stateEncryptionKey: Value(Uint8List.fromList(newGroup.stateKey)),
|
stateEncryptionKey: Value(Uint8List.fromList(newGroup.stateKey)),
|
||||||
myGroupPrivateKey: Value(myGroupKey.getPrivateKey().serialize()),
|
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
|
// can be done in the background -> websocket message can be ACK
|
||||||
unawaited(fetchGroupStatesForUnjoinedGroups());
|
unawaited(fetchGroupStatesForUnjoinedGroups());
|
||||||
|
|
||||||
|
await sendCipherTextToGroup(
|
||||||
|
groupId,
|
||||||
|
EncryptedContent(
|
||||||
|
groupJoin: EncryptedContent_GroupJoin(
|
||||||
|
groupPublicKey: myGroupKey.getPublicKey().serialize(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> handleGroupUpdate(
|
Future<void> handleGroupUpdate(
|
||||||
|
|
@ -71,8 +83,23 @@ Future<void> handleGroupUpdate(
|
||||||
EncryptedContent_GroupUpdate update,
|
EncryptedContent_GroupUpdate update,
|
||||||
) async {}
|
) async {}
|
||||||
|
|
||||||
Future<void> handleGroupJoin(
|
Future<bool> handleGroupJoin(
|
||||||
int fromUserId,
|
int fromUserId,
|
||||||
String groupId,
|
String groupId,
|
||||||
EncryptedContent_GroupJoin join,
|
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;
|
encryptedContent.textMessage.quoteMessageId = quotesMessageId;
|
||||||
}
|
}
|
||||||
|
|
||||||
await sendCipherTextToGroup(groupId, encryptedContent, message.messageId);
|
await sendCipherTextToGroup(
|
||||||
|
groupId,
|
||||||
|
encryptedContent,
|
||||||
|
messageId: message.messageId,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> sendCipherTextToGroup(
|
Future<void> sendCipherTextToGroup(
|
||||||
String groupId,
|
String groupId,
|
||||||
pb.EncryptedContent encryptedContent,
|
pb.EncryptedContent encryptedContent, {
|
||||||
String? messageId,
|
String? messageId,
|
||||||
) async {
|
}) async {
|
||||||
final groupMembers = await twonlyDB.groupsDao.getGroupMembers(groupId);
|
final groupMembers = await twonlyDB.groupsDao.getGroupMembers(groupId);
|
||||||
|
|
||||||
await twonlyDB.groupsDao.increaseLastMessageExchange(groupId, DateTime.now());
|
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);
|
await twonlyDB.receiptsDao.confirmReceipt(receiptId, fromUserId);
|
||||||
|
|
||||||
case Message_Type.PLAINTEXT_CONTENT:
|
case Message_Type.PLAINTEXT_CONTENT:
|
||||||
if (message.hasPlaintextContent() &&
|
var retry = false;
|
||||||
message.plaintextContent.hasDecryptionErrorMessage()) {
|
if (message.hasPlaintextContent()) {
|
||||||
Log.info(
|
if (message.plaintextContent.hasDecryptionErrorMessage()) {
|
||||||
'Got decryption error: ${message.plaintextContent.decryptionErrorMessage.type} for $receiptId',
|
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();
|
final newReceiptId = uuid.v4();
|
||||||
await twonlyDB.receiptsDao.updateReceipt(
|
await twonlyDB.receiptsDao.updateReceipt(
|
||||||
receiptId,
|
receiptId,
|
||||||
|
|
@ -162,7 +174,10 @@ Future<PlaintextContent?> handleEncryptedMessage(
|
||||||
final senderProfileCounter = await checkForProfileUpdate(fromUserId, content);
|
final senderProfileCounter = await checkForProfileUpdate(fromUserId, content);
|
||||||
|
|
||||||
if (content.hasContactRequest()) {
|
if (content.hasContactRequest()) {
|
||||||
await handleContactRequest(fromUserId, content.contactRequest);
|
if (!await handleContactRequest(fromUserId, content.contactRequest)) {
|
||||||
|
return PlaintextContent()
|
||||||
|
..retryControlError = PlaintextContent_RetryErrorMessage();
|
||||||
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -206,15 +221,6 @@ Future<PlaintextContent?> handleEncryptedMessage(
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (content.hasGroupUpdate()) {
|
|
||||||
await handleGroupUpdate(
|
|
||||||
fromUserId,
|
|
||||||
content.groupId,
|
|
||||||
content.groupUpdate,
|
|
||||||
);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (content.hasGroupCreate()) {
|
if (content.hasGroupCreate()) {
|
||||||
await handleGroupCreate(
|
await handleGroupCreate(
|
||||||
fromUserId,
|
fromUserId,
|
||||||
|
|
@ -224,15 +230,6 @@ Future<PlaintextContent?> handleEncryptedMessage(
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (content.hasGroupJoin()) {
|
|
||||||
await handleGroupJoin(
|
|
||||||
fromUserId,
|
|
||||||
content.groupId,
|
|
||||||
content.groupJoin,
|
|
||||||
);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Verify that the user is (still) in that group...
|
/// Verify that the user is (still) in that group...
|
||||||
if (!await twonlyDB.groupsDao.isContactInGroup(fromUserId, content.groupId)) {
|
if (!await twonlyDB.groupsDao.isContactInGroup(fromUserId, content.groupId)) {
|
||||||
if (getUUIDforDirectChat(gUser.userId, fromUserId) == content.groupId) {
|
if (getUUIDforDirectChat(gUser.userId, fromUserId) == content.groupId) {
|
||||||
|
|
@ -255,11 +252,41 @@ Future<PlaintextContent?> handleEncryptedMessage(
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} 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}.');
|
Log.error('User $fromUserId tried to access group ${content.groupId}.');
|
||||||
return null;
|
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()) {
|
if (content.hasTextMessage()) {
|
||||||
await handleTextMessage(
|
await handleTextMessage(
|
||||||
fromUserId,
|
fromUserId,
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
|
import 'dart:convert';
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
import 'package:cryptography_flutter_plus/cryptography_flutter_plus.dart';
|
import 'package:cryptography_flutter_plus/cryptography_flutter_plus.dart';
|
||||||
import 'package:cryptography_plus/cryptography_plus.dart';
|
import 'package:cryptography_plus/cryptography_plus.dart';
|
||||||
import 'package:drift/drift.dart' show Value;
|
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/groups.pb.dart';
|
||||||
import 'package:twonly/src/model/protobuf/client/generated/messages.pbserver.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/api/messages.dart';
|
||||||
|
import 'package:twonly/src/services/signal/session.signal.dart';
|
||||||
import 'package:twonly/src/utils/log.dart';
|
import 'package:twonly/src/utils/log.dart';
|
||||||
import 'package:twonly/src/utils/misc.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 memberIds = members.map((x) => Int64(x.userId)).toList();
|
||||||
|
|
||||||
final groupState = EncryptedGroupState(
|
final groupState = EncryptedGroupState(
|
||||||
memberIds: memberIds,
|
memberIds: [Int64(gUser.userId)] + memberIds,
|
||||||
adminIds: [Int64(gUser.userId)],
|
adminIds: [Int64(gUser.userId)],
|
||||||
groupName: groupName,
|
groupName: groupName,
|
||||||
deleteMessagesAfterMilliseconds:
|
deleteMessagesAfterMilliseconds:
|
||||||
|
|
@ -88,6 +91,7 @@ Future<bool> createNewGroup(String groupName, List<Contact> members) async {
|
||||||
stateEncryptionKey: Value(stateEncryptionKey),
|
stateEncryptionKey: Value(stateEncryptionKey),
|
||||||
stateVersionId: const Value(1),
|
stateVersionId: const Value(1),
|
||||||
myGroupPrivateKey: Value(myGroupKey.getPrivateKey().serialize()),
|
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(),
|
groupPublicKey: myGroupKey.getPublicKey().serialize(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
null,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -134,11 +137,15 @@ Future<bool> createNewGroup(String groupName, List<Contact> members) async {
|
||||||
Future<void> fetchGroupStatesForUnjoinedGroups() async {
|
Future<void> fetchGroupStatesForUnjoinedGroups() async {
|
||||||
final groups = await twonlyDB.groupsDao.getAllNotJoinedGroups();
|
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 {
|
try {
|
||||||
|
var isSuccess = true;
|
||||||
|
|
||||||
final response = await http
|
final response = await http
|
||||||
.get(
|
.get(
|
||||||
Uri.parse('${getGroupStateUrl()}/${group.groupId}'),
|
Uri.parse('${getGroupStateUrl()}/${group.groupId}'),
|
||||||
|
|
@ -149,7 +156,7 @@ Future<GroupState?> fetchGroupState(Group group) async {
|
||||||
Log.error(
|
Log.error(
|
||||||
'Could not load group state. Got status code ${response.statusCode} from server.',
|
'Could not load group state. Got status code ${response.statusCode} from server.',
|
||||||
);
|
);
|
||||||
return null;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
final groupStateServer = GroupState.fromBuffer(response.bodyBytes);
|
final groupStateServer = GroupState.fromBuffer(response.bodyBytes);
|
||||||
|
|
@ -166,14 +173,128 @@ Future<GroupState?> fetchGroupState(Group group) async {
|
||||||
final encryptedGroupState =
|
final encryptedGroupState =
|
||||||
EncryptedGroupState.fromBuffer(encryptedGroupStateRaw);
|
EncryptedGroupState.fromBuffer(encryptedGroupStateRaw);
|
||||||
|
|
||||||
encryptedGroupState.adminIds;
|
if (group.stateVersionId >= groupStateServer.versionId.toInt()) {
|
||||||
encryptedGroupState.memberIds;
|
Log.error('Group ${group.groupId} has newest group state');
|
||||||
encryptedGroupState.groupName;
|
return false;
|
||||||
encryptedGroupState.deleteMessagesAfterMilliseconds;
|
}
|
||||||
encryptedGroupState.deleteMessagesAfterMilliseconds;
|
|
||||||
groupStateServer.versionId;
|
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) {
|
} catch (e) {
|
||||||
Log.error(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 {
|
class ChatItem {
|
||||||
const ChatItem._({this.message, this.date});
|
const ChatItem._({this.message, this.date, this.lastOpenedPosition});
|
||||||
factory ChatItem.date(DateTime date) {
|
factory ChatItem.date(DateTime date) {
|
||||||
return ChatItem._(date: date);
|
return ChatItem._(date: date);
|
||||||
}
|
}
|
||||||
factory ChatItem.message(Message message) {
|
factory ChatItem.message(Message message) {
|
||||||
return ChatItem._(message: message);
|
return ChatItem._(message: message);
|
||||||
}
|
}
|
||||||
|
factory ChatItem.lastOpenedPosition(List<Contact> contacts) {
|
||||||
|
return ChatItem._(lastOpenedPosition: contacts);
|
||||||
|
}
|
||||||
final Message? message;
|
final Message? message;
|
||||||
final DateTime? date;
|
final DateTime? date;
|
||||||
|
final List<Contact>? lastOpenedPosition;
|
||||||
bool get isMessage => message != null;
|
bool get isMessage => message != null;
|
||||||
bool get isDate => date != null;
|
bool get isDate => date != null;
|
||||||
|
bool get isLastOpenedPosition => lastOpenedPosition != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Displays detailed information about a SampleItem.
|
/// Displays detailed information about a SampleItem.
|
||||||
|
|
@ -60,7 +65,12 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
|
||||||
String currentInputText = '';
|
String currentInputText = '';
|
||||||
late StreamSubscription<Group?> userSub;
|
late StreamSubscription<Group?> userSub;
|
||||||
late StreamSubscription<List<Message>> messageSub;
|
late StreamSubscription<List<Message>> messageSub;
|
||||||
|
late StreamSubscription<Future<List<(Message, Contact)>>>?
|
||||||
|
lastOpenedMessageByContactSub;
|
||||||
|
|
||||||
List<ChatItem> messages = [];
|
List<ChatItem> messages = [];
|
||||||
|
List<Message> allMessages = [];
|
||||||
|
List<(Message, Contact)> lastOpenedMessageByContact = [];
|
||||||
List<MemoryItem> galleryItems = [];
|
List<MemoryItem> galleryItems = [];
|
||||||
Message? quotesMessage;
|
Message? quotesMessage;
|
||||||
GlobalKey verifyShieldKey = GlobalKey();
|
GlobalKey verifyShieldKey = GlobalKey();
|
||||||
|
|
@ -87,6 +97,7 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
|
||||||
void dispose() {
|
void dispose() {
|
||||||
userSub.cancel();
|
userSub.cancel();
|
||||||
messageSub.cancel();
|
messageSub.cancel();
|
||||||
|
lastOpenedMessageByContactSub?.cancel();
|
||||||
tutorial?.cancel();
|
tutorial?.cancel();
|
||||||
textFieldFocus.dispose();
|
textFieldFocus.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
|
|
@ -103,66 +114,110 @@ 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);
|
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.
|
/// 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...
|
/// So as long as the Mutex is locked just return...
|
||||||
if (protectMessageUpdating.isLocked) {
|
if (protectMessageUpdating.isLocked) {
|
||||||
return;
|
// return;
|
||||||
}
|
}
|
||||||
await protectMessageUpdating.protect(() async {
|
await protectMessageUpdating.protect(() async {
|
||||||
await flutterLocalNotificationsPlugin.cancelAll();
|
await setMessages(update, lastOpenedMessageByContact);
|
||||||
|
|
||||||
final chatItems = <ChatItem>[];
|
|
||||||
final storedMediaFiles = <Message>[];
|
|
||||||
|
|
||||||
DateTime? lastDate;
|
|
||||||
|
|
||||||
final openedMessages = <int, List<String>>{};
|
|
||||||
|
|
||||||
for (final msg in newMessages) {
|
|
||||||
if (msg.type == MessageType.text &&
|
|
||||||
msg.senderId != null &&
|
|
||||||
msg.openedAt == null) {
|
|
||||||
if (openedMessages[msg.senderId!] == null) {
|
|
||||||
openedMessages[msg.senderId!] = [];
|
|
||||||
}
|
|
||||||
openedMessages[msg.senderId!]!.add(msg.messageId);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (msg.type == MessageType.media && msg.mediaStored) {
|
|
||||||
storedMediaFiles.add(msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lastDate == null ||
|
|
||||||
msg.createdAt.day != lastDate.day ||
|
|
||||||
msg.createdAt.month != lastDate.month ||
|
|
||||||
msg.createdAt.year != lastDate.year) {
|
|
||||||
chatItems.add(ChatItem.date(msg.createdAt));
|
|
||||||
lastDate = msg.createdAt;
|
|
||||||
}
|
|
||||||
chatItems.add(ChatItem.message(msg));
|
|
||||||
}
|
|
||||||
|
|
||||||
for (final contactId in openedMessages.keys) {
|
|
||||||
await notifyContactAboutOpeningMessage(
|
|
||||||
contactId,
|
|
||||||
openedMessages[contactId]!,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!mounted) return;
|
|
||||||
setState(() {
|
|
||||||
messages = chatItems.reversed.toList();
|
|
||||||
});
|
|
||||||
|
|
||||||
final items = await MemoryItem.convertFromMessages(storedMediaFiles);
|
|
||||||
galleryItems = items.values.toList();
|
|
||||||
setState(() {});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> setMessages(
|
||||||
|
List<Message> newMessages,
|
||||||
|
List<(Message, Contact)> lastOpenedMessageByContact,
|
||||||
|
) async {
|
||||||
|
await flutterLocalNotificationsPlugin.cancelAll();
|
||||||
|
|
||||||
|
final chatItems = <ChatItem>[];
|
||||||
|
final storedMediaFiles = <Message>[];
|
||||||
|
|
||||||
|
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) {
|
||||||
|
if (openedMessages[msg.senderId!] == null) {
|
||||||
|
openedMessages[msg.senderId!] = [];
|
||||||
|
}
|
||||||
|
openedMessages[msg.senderId!]!.add(msg.messageId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg.type == MessageType.media && msg.mediaStored) {
|
||||||
|
storedMediaFiles.add(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastDate == null ||
|
||||||
|
msg.createdAt.day != lastDate.day ||
|
||||||
|
msg.createdAt.month != lastDate.month ||
|
||||||
|
msg.createdAt.year != lastDate.year) {
|
||||||
|
chatItems.add(ChatItem.date(msg.createdAt));
|
||||||
|
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) {
|
||||||
|
await notifyContactAboutOpeningMessage(
|
||||||
|
contactId,
|
||||||
|
openedMessages[contactId]!,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() {
|
||||||
|
messages = chatItems.reversed.toList();
|
||||||
|
});
|
||||||
|
|
||||||
|
final items = await MemoryItem.convertFromMessages(storedMediaFiles);
|
||||||
|
galleryItems = items.values.toList();
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _sendMessage() async {
|
Future<void> _sendMessage() async {
|
||||||
if (newMessageController.text == '') return;
|
if (newMessageController.text == '') return;
|
||||||
|
|
||||||
|
|
@ -275,6 +330,18 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
|
||||||
return ChatDateChip(
|
return ChatDateChip(
|
||||||
item: messages[i],
|
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 {
|
} else {
|
||||||
final chatMessage = messages[i].message!;
|
final chatMessage = messages[i].message!;
|
||||||
return Transform.translate(
|
return Transform.translate(
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,6 @@ class _AllReactionsViewState extends State<AllReactionsView> {
|
||||||
remove: true,
|
remove: true,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
null,
|
|
||||||
);
|
);
|
||||||
if (mounted) Navigator.pop(context);
|
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_actions.dart';
|
||||||
import 'package:twonly/src/views/chats/chat_messages_components/message_context_menu.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/chats/chat_messages_components/response_container.dart';
|
||||||
|
import 'package:twonly/src/views/components/avatar_icon.component.dart';
|
||||||
|
|
||||||
class ChatListEntry extends StatefulWidget {
|
class ChatListEntry extends StatefulWidget {
|
||||||
const ChatListEntry({
|
const ChatListEntry({
|
||||||
|
|
@ -86,7 +87,7 @@ class _ChatListEntryState extends State<ChatListEntry> {
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final right = widget.message.senderId == null;
|
final right = widget.message.senderId == null;
|
||||||
|
|
||||||
final (padding, borderRadius) = getMessageLayout(
|
final (padding, borderRadius, hideContactAvatar) = getMessageLayout(
|
||||||
widget.message,
|
widget.message,
|
||||||
widget.prevMessage,
|
widget.prevMessage,
|
||||||
widget.nextMessage,
|
widget.nextMessage,
|
||||||
|
|
@ -172,19 +173,36 @@ class _ChatListEntryState extends State<ChatListEntry> {
|
||||||
|
|
||||||
return Align(
|
return Align(
|
||||||
alignment: right ? Alignment.centerRight : Alignment.centerLeft,
|
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 message,
|
||||||
Message? prevMessage,
|
Message? prevMessage,
|
||||||
Message? nextMessage,
|
Message? nextMessage,
|
||||||
bool hasReactions,
|
bool hasReactions,
|
||||||
) {
|
) {
|
||||||
var bottom = 20.0;
|
var bottom = 10.0;
|
||||||
var top = 0.0;
|
var top = 10.0;
|
||||||
|
var hideContactAvatar = false;
|
||||||
|
|
||||||
var topLeft = 12.0;
|
var topLeft = 12.0;
|
||||||
var topRight = 12.0;
|
var topRight = 12.0;
|
||||||
|
|
@ -209,6 +227,7 @@ class _ChatListEntryState extends State<ChatListEntry> {
|
||||||
if (combinesWidthNext) {
|
if (combinesWidthNext) {
|
||||||
bottom = 0;
|
bottom = 0;
|
||||||
bottomLeft = 5.0;
|
bottomLeft = 5.0;
|
||||||
|
hideContactAvatar = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (message.senderId == null) {
|
if (message.senderId == null) {
|
||||||
|
|
@ -219,6 +238,7 @@ class _ChatListEntryState extends State<ChatListEntry> {
|
||||||
final tmp2 = bottomLeft;
|
final tmp2 = bottomLeft;
|
||||||
bottomLeft = bottomRight;
|
bottomLeft = bottomRight;
|
||||||
bottomRight = tmp2;
|
bottomRight = tmp2;
|
||||||
|
hideContactAvatar = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -228,6 +248,7 @@ class _ChatListEntryState extends State<ChatListEntry> {
|
||||||
topRight: Radius.circular(topRight),
|
topRight: Radius.circular(topRight),
|
||||||
bottomRight: Radius.circular(bottomRight),
|
bottomRight: Radius.circular(bottomRight),
|
||||||
bottomLeft: Radius.circular(bottomLeft),
|
bottomLeft: Radius.circular(bottomLeft),
|
||||||
)
|
),
|
||||||
|
hideContactAvatar
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -76,7 +76,6 @@ class _ChatMediaEntryState extends State<ChatMediaEntry> {
|
||||||
targetMessageId: widget.message.messageId,
|
targetMessageId: widget.message.messageId,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
null,
|
|
||||||
);
|
);
|
||||||
await twonlyDB.messagesDao.updateMessageId(
|
await twonlyDB.messagesDao.updateMessageId(
|
||||||
widget.message.messageId,
|
widget.message.messageId,
|
||||||
|
|
|
||||||
|
|
@ -65,7 +65,6 @@ class MessageContextMenu extends StatelessWidget {
|
||||||
remove: false,
|
remove: false,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
null,
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
icon: FontAwesomeIcons.faceLaugh,
|
icon: FontAwesomeIcons.faceLaugh,
|
||||||
|
|
@ -124,7 +123,6 @@ class MessageContextMenu extends StatelessWidget {
|
||||||
senderMessageId: message.messageId,
|
senderMessageId: message.messageId,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
null,
|
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
await twonlyDB.messagesDao
|
await twonlyDB.messagesDao
|
||||||
|
|
@ -225,7 +223,6 @@ Future<void> editTextMessage(BuildContext context, Message message) async {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
null,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (!context.mounted) return;
|
if (!context.mounted) return;
|
||||||
|
|
|
||||||
|
|
@ -290,7 +290,6 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
||||||
targetMessageId: currentMessage!.messageId,
|
targetMessageId: currentMessage!.messageId,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
null,
|
|
||||||
);
|
);
|
||||||
setState(() {
|
setState(() {
|
||||||
imageSaved = true;
|
imageSaved = true;
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,6 @@ class _EmojiReactionWidgetState extends State<EmojiReactionWidget> {
|
||||||
remove: false,
|
remove: false,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
null,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
|
|
|
||||||
|
|
@ -122,34 +122,37 @@ class _MessageInfoViewState extends State<MessageInfoView> {
|
||||||
}
|
}
|
||||||
|
|
||||||
columns.add(
|
columns.add(
|
||||||
Row(
|
Padding(
|
||||||
children: [
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||||
AvatarIcon(
|
child: Row(
|
||||||
contact: groupMember.$2,
|
children: [
|
||||||
fontSize: 15,
|
AvatarIcon(
|
||||||
),
|
contact: groupMember.$2,
|
||||||
const SizedBox(width: 6),
|
fontSize: 15,
|
||||||
Expanded(
|
),
|
||||||
child: Column(
|
const SizedBox(width: 6),
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
getContactDisplayName(groupMember.$2),
|
||||||
|
style: const TextStyle(fontSize: 17),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Column(
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
getContactDisplayName(groupMember.$2),
|
friendlyDateTime(context, actionAt),
|
||||||
style: const TextStyle(fontSize: 17),
|
style: const TextStyle(fontSize: 12),
|
||||||
),
|
),
|
||||||
|
Text(actionTypeText),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
Column(
|
),
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
friendlyDateTime(context, actionAt),
|
|
||||||
style: const TextStyle(fontSize: 12),
|
|
||||||
),
|
|
||||||
Text(actionTypeText),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ import 'package:flutter_svg/svg.dart';
|
||||||
import 'package:twonly/globals.dart';
|
import 'package:twonly/globals.dart';
|
||||||
import 'package:twonly/src/database/twonly.db.dart';
|
import 'package:twonly/src/database/twonly.db.dart';
|
||||||
import 'package:twonly/src/model/json/userdata.dart';
|
import 'package:twonly/src/model/json/userdata.dart';
|
||||||
import 'package:twonly/src/utils/log.dart';
|
|
||||||
import 'package:twonly/src/utils/misc.dart';
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
|
|
||||||
class AvatarIcon extends StatefulWidget {
|
class AvatarIcon extends StatefulWidget {
|
||||||
|
|
@ -11,12 +10,14 @@ class AvatarIcon extends StatefulWidget {
|
||||||
super.key,
|
super.key,
|
||||||
this.group,
|
this.group,
|
||||||
this.contact,
|
this.contact,
|
||||||
|
this.contactId,
|
||||||
this.userData,
|
this.userData,
|
||||||
this.fontSize = 20,
|
this.fontSize = 20,
|
||||||
this.color,
|
this.color,
|
||||||
});
|
});
|
||||||
final Group? group;
|
final Group? group;
|
||||||
final Contact? contact;
|
final Contact? contact;
|
||||||
|
final int? contactId;
|
||||||
final UserData? userData;
|
final UserData? userData;
|
||||||
final double? fontSize;
|
final double? fontSize;
|
||||||
final Color? color;
|
final Color? color;
|
||||||
|
|
@ -54,6 +55,12 @@ class _AvatarIconState extends State<AvatarIcon> {
|
||||||
_avatarSVGs.add(widget.userData!.avatarSvg!);
|
_avatarSVGs.add(widget.userData!.avatarSvg!);
|
||||||
} else if (widget.contact?.avatarSvgCompressed != null) {
|
} else if (widget.contact?.avatarSvgCompressed != null) {
|
||||||
_avatarSVGs.add(getAvatarSvg(widget.contact!.avatarSvgCompressed!));
|
_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(() {});
|
if (mounted) setState(() {});
|
||||||
}
|
}
|
||||||
|
|
@ -62,6 +69,62 @@ class _AvatarIconState extends State<AvatarIcon> {
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final proSize = (widget.fontSize == null) ? 40 : (widget.fontSize! * 2);
|
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(
|
return Container(
|
||||||
constraints: BoxConstraints(
|
constraints: BoxConstraints(
|
||||||
minHeight: 2 * (widget.fontSize ?? 20),
|
minHeight: 2 * (widget.fontSize ?? 20),
|
||||||
|
|
@ -76,17 +139,7 @@ class _AvatarIconState extends State<AvatarIcon> {
|
||||||
height: proSize as double,
|
height: proSize as double,
|
||||||
width: proSize,
|
width: proSize,
|
||||||
color: widget.color,
|
color: widget.color,
|
||||||
child: Center(
|
child: Center(child: avatars),
|
||||||
child: _avatarSVGs.isEmpty
|
|
||||||
? SvgPicture.asset('assets/images/default_avatar.svg')
|
|
||||||
: SvgPicture.string(
|
|
||||||
_avatarSVGs.first,
|
|
||||||
errorBuilder: (context, error, stackTrace) {
|
|
||||||
Log.error('$error');
|
|
||||||
return Container();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -46,9 +46,11 @@ class _FlameCounterWidgetState extends State<FlameCounterWidget> {
|
||||||
isBestFriend = gUser.myBestFriendGroupId == groupId;
|
isBestFriend = gUser.myBestFriendGroupId == groupId;
|
||||||
final stream = twonlyDB.groupsDao.watchFlameCounter(groupId);
|
final stream = twonlyDB.groupsDao.watchFlameCounter(groupId);
|
||||||
flameCounterSub = stream.listen((counter) {
|
flameCounterSub = stream.listen((counter) {
|
||||||
setState(() {
|
if (mounted) {
|
||||||
flameCounter = counter;
|
setState(() {
|
||||||
});
|
flameCounter = counter;
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,8 @@ class _GroupCreateSelectGroupNameViewState
|
||||||
await createNewGroup(textFieldGroupName.text, widget.selectedUsers);
|
await createNewGroup(textFieldGroupName.text, widget.selectedUsers);
|
||||||
if (wasSuccess) {
|
if (wasSuccess) {
|
||||||
// POP
|
// POP
|
||||||
|
Navigator.popUntil(context, (route) => route.isFirst);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue