✨ Functional key exchange
This commit is contained in:
parent
5a6b841253
commit
d6f3ffc655
assets/translations
drift_schemas/my_database
lib
database
providers
widgets/chat
macos
test/drift/my_database/generated
@ -759,5 +759,7 @@
|
|||||||
"accountKeyPairsDescription": "Manage the key pairs which used to encrypt messages.",
|
"accountKeyPairsDescription": "Manage the key pairs which used to encrypt messages.",
|
||||||
"enrollNewKeyPair": "Enroll New One",
|
"enrollNewKeyPair": "Enroll New One",
|
||||||
"enrollNewKeyPairDescription": "Generate a new key pair.",
|
"enrollNewKeyPairDescription": "Generate a new key pair.",
|
||||||
"keyPairHasPrivateKey": "With private key"
|
"keyPairHasPrivateKey": "With private key",
|
||||||
|
"decrypting": "Decrypting……",
|
||||||
|
"decryptingKeyNotFound": "Key not found or exchange failed, the other party may not be online"
|
||||||
}
|
}
|
||||||
|
@ -757,5 +757,7 @@
|
|||||||
"accountKeyPairsDescription": "管理用于加密信息的密钥对。",
|
"accountKeyPairsDescription": "管理用于加密信息的密钥对。",
|
||||||
"enrollNewKeyPair": "新建密钥对",
|
"enrollNewKeyPair": "新建密钥对",
|
||||||
"enrollNewKeyPairDescription": "生成一对新密钥对。",
|
"enrollNewKeyPairDescription": "生成一对新密钥对。",
|
||||||
"keyPairHasPrivateKey": "有私钥"
|
"keyPairHasPrivateKey": "有私钥",
|
||||||
|
"decrypting": "解密中……",
|
||||||
|
"decryptingKeyNotFound": "未找到密钥对或交换失败,对方可能不在线"
|
||||||
}
|
}
|
||||||
|
@ -756,6 +756,8 @@
|
|||||||
"accountKeyPairs": "密鑰對",
|
"accountKeyPairs": "密鑰對",
|
||||||
"accountKeyPairsDescription": "管理用於加密信息的密鑰對。",
|
"accountKeyPairsDescription": "管理用於加密信息的密鑰對。",
|
||||||
"enrollNewKeyPair": "新建密鑰對",
|
"enrollNewKeyPair": "新建密鑰對",
|
||||||
"enrollNewKeyPairDescription": "生成一對新密鑰對,覆蓋當前的;如果已有一個密鑰將會丟棄舊密鑰的私鑰。",
|
"enrollNewKeyPairDescription": "生成一對新密鑰對。",
|
||||||
"keyPairHasPrivateKey": "有私鑰"
|
"keyPairHasPrivateKey": "有私鑰",
|
||||||
|
"decrypting": "解密中……",
|
||||||
|
"decryptingKeyNotFound": "未找到密鑰對或交換失敗,對方可能不在線"
|
||||||
}
|
}
|
||||||
|
@ -756,6 +756,8 @@
|
|||||||
"accountKeyPairs": "密鑰對",
|
"accountKeyPairs": "密鑰對",
|
||||||
"accountKeyPairsDescription": "管理用於加密信息的密鑰對。",
|
"accountKeyPairsDescription": "管理用於加密信息的密鑰對。",
|
||||||
"enrollNewKeyPair": "新建密鑰對",
|
"enrollNewKeyPair": "新建密鑰對",
|
||||||
"enrollNewKeyPairDescription": "生成一對新密鑰對,覆蓋當前的;如果已有一個密鑰將會丟棄舊密鑰的私鑰。",
|
"enrollNewKeyPairDescription": "生成一對新密鑰對。",
|
||||||
"keyPairHasPrivateKey": "有私鑰"
|
"keyPairHasPrivateKey": "有私鑰",
|
||||||
|
"decrypting": "解密中……",
|
||||||
|
"decryptingKeyNotFound": "未找到密鑰對或交換失敗,對方可能不在線"
|
||||||
}
|
}
|
||||||
|
@ -1 +1 @@
|
|||||||
{"_meta":{"description":"This file contains a serialized version of schema entities for drift.","version":"1.2.0"},"options":{"store_date_time_values_as_text":false},"entities":[{"id":0,"references":[],"type":"table","data":{"name":"sn_local_chat_channel","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"defaultConstraints":"PRIMARY KEY AUTOINCREMENT","dialectAwareDefaultConstraints":{"sqlite":"PRIMARY KEY AUTOINCREMENT"},"default_dart":null,"default_client_dart":null,"dsl_features":["auto-increment"]},{"name":"alias","getter_name":"alias","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"content","getter_name":"content","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const SnChannelConverter()","dart_type_name":"SnChannel"}},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CAST(strftime(\\'%s\\', CURRENT_TIMESTAMP) AS INTEGER)')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[]}},{"id":1,"references":[],"type":"table","data":{"name":"sn_local_chat_message","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"defaultConstraints":"PRIMARY KEY AUTOINCREMENT","dialectAwareDefaultConstraints":{"sqlite":"PRIMARY KEY AUTOINCREMENT"},"default_dart":null,"default_client_dart":null,"dsl_features":["auto-increment"]},{"name":"channel_id","getter_name":"channelId","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"content","getter_name":"content","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const SnMessageConverter()","dart_type_name":"SnChatMessage"}},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CAST(strftime(\\'%s\\', CURRENT_TIMESTAMP) AS INTEGER)')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[]}},{"id":2,"references":[],"type":"table","data":{"name":"sn_local_key_pair","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"account_id","getter_name":"accountId","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"public_key","getter_name":"publicKey","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"private_key","getter_name":"privateKey","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_active","getter_name":"isActive","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_active\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_active\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[]}}]}
|
{"_meta":{"description":"This file contains a serialized version of schema entities for drift.","version":"1.2.0"},"options":{"store_date_time_values_as_text":false},"entities":[{"id":0,"references":[],"type":"table","data":{"name":"sn_local_chat_channel","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"defaultConstraints":"PRIMARY KEY AUTOINCREMENT","dialectAwareDefaultConstraints":{"sqlite":"PRIMARY KEY AUTOINCREMENT"},"default_dart":null,"default_client_dart":null,"dsl_features":["auto-increment"]},{"name":"alias","getter_name":"alias","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"content","getter_name":"content","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const SnChannelConverter()","dart_type_name":"SnChannel"}},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CAST(strftime(\\'%s\\', CURRENT_TIMESTAMP) AS INTEGER)')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[]}},{"id":1,"references":[],"type":"table","data":{"name":"sn_local_chat_message","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"defaultConstraints":"PRIMARY KEY AUTOINCREMENT","dialectAwareDefaultConstraints":{"sqlite":"PRIMARY KEY AUTOINCREMENT"},"default_dart":null,"default_client_dart":null,"dsl_features":["auto-increment"]},{"name":"channel_id","getter_name":"channelId","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"content","getter_name":"content","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const SnMessageConverter()","dart_type_name":"SnChatMessage"}},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CAST(strftime(\\'%s\\', CURRENT_TIMESTAMP) AS INTEGER)')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[]}},{"id":2,"references":[],"type":"table","data":{"name":"sn_local_key_pair","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"account_id","getter_name":"accountId","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"public_key","getter_name":"publicKey","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"private_key","getter_name":"privateKey","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_active","getter_name":"isActive","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_active\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_active\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["id"]}}]}
|
@ -621,7 +621,7 @@ class $SnLocalKeyPairTable extends SnLocalKeyPair
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Set<GeneratedColumn> get $primaryKey => const {};
|
Set<GeneratedColumn> get $primaryKey => {id};
|
||||||
@override
|
@override
|
||||||
SnLocalKeyPairData map(Map<String, dynamic> data, {String? tablePrefix}) {
|
SnLocalKeyPairData map(Map<String, dynamic> data, {String? tablePrefix}) {
|
||||||
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
|
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
|
||||||
|
@ -47,7 +47,9 @@ final class Schema2 extends i0.VersionedSchema {
|
|||||||
entityName: 'sn_local_key_pair',
|
entityName: 'sn_local_key_pair',
|
||||||
withoutRowId: false,
|
withoutRowId: false,
|
||||||
isStrict: false,
|
isStrict: false,
|
||||||
tableConstraints: [],
|
tableConstraints: [
|
||||||
|
'PRIMARY KEY(id)',
|
||||||
|
],
|
||||||
columns: [
|
columns: [
|
||||||
_column_5,
|
_column_5,
|
||||||
_column_6,
|
_column_6,
|
||||||
|
@ -10,4 +10,7 @@ class SnLocalKeyPair extends Table {
|
|||||||
TextColumn get privateKey => text().nullable()();
|
TextColumn get privateKey => text().nullable()();
|
||||||
|
|
||||||
BoolColumn get isActive => boolean().withDefault(Constant(false))();
|
BoolColumn get isActive => boolean().withDefault(Constant(false))();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Set<Column<Object>> get primaryKey => {id};
|
||||||
}
|
}
|
||||||
|
@ -35,24 +35,35 @@ class KeyPairProvider {
|
|||||||
case 'kex.ack':
|
case 'kex.ack':
|
||||||
ackKeyExchange(event);
|
ackKeyExchange(event);
|
||||||
break;
|
break;
|
||||||
case 'key.ask':
|
case 'kex.ask':
|
||||||
replyAskKeyExchange(event);
|
replyAskKeyExchange(event);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String> decryptText(String text, String kpId) async {
|
Future<String> decryptText(String text, String kpId, {int? kpOwner}) async {
|
||||||
|
String? publicKey;
|
||||||
final kp = await (_dt.db.snLocalKeyPair.select()
|
final kp = await (_dt.db.snLocalKeyPair.select()
|
||||||
..where((e) => e.id.equals(kpId)))
|
..where((e) => e.id.equals(kpId)))
|
||||||
.getSingleOrNull();
|
.getSingleOrNull();
|
||||||
if (kp == null) throw Exception('Key pair not found');
|
if (kp == null) {
|
||||||
return await RSA.decryptPKCS1v15(text, kp.privateKey!);
|
if (kpOwner != null) {
|
||||||
|
final out = await askKeyExchange(kpOwner, kpId);
|
||||||
|
publicKey = out.publicKey;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
publicKey = kp.publicKey;
|
||||||
|
}
|
||||||
|
if (publicKey == null) {
|
||||||
|
throw Exception('Key pair not found');
|
||||||
|
}
|
||||||
|
return await RSA.decryptPKCS1v15(text, publicKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String> encryptText(String text) async {
|
Future<String> encryptText(String text) async {
|
||||||
if (activeKp == null) throw Exception('No active key pair');
|
if (activeKp == null) throw Exception('No active key pair');
|
||||||
return await RSA.encryptPKCS1v15(text, activeKp!.publicKey);
|
return await RSA.encryptPKCS1v15(text, activeKp!.privateKey!);
|
||||||
}
|
}
|
||||||
|
|
||||||
final Map<String, Completer<SnKeyPair>> _requests = {};
|
final Map<String, Completer<SnKeyPair>> _requests = {};
|
||||||
@ -65,7 +76,7 @@ class KeyPairProvider {
|
|||||||
|
|
||||||
_ws.conn?.sink.add(
|
_ws.conn?.sink.add(
|
||||||
jsonEncode(WebSocketPackage(
|
jsonEncode(WebSocketPackage(
|
||||||
method: 'key.ask',
|
method: 'kex.ask',
|
||||||
endpoint: 'id',
|
endpoint: 'id',
|
||||||
payload: {
|
payload: {
|
||||||
'keypair_id': kpId,
|
'keypair_id': kpId,
|
||||||
@ -105,12 +116,7 @@ class KeyPairProvider {
|
|||||||
publicKey: kpMeta.publicKey,
|
publicKey: kpMeta.publicKey,
|
||||||
privateKey: Value(kpMeta.privateKey),
|
privateKey: Value(kpMeta.privateKey),
|
||||||
),
|
),
|
||||||
onConflict: DoUpdate(
|
onConflict: DoNothing(),
|
||||||
(_) => SnLocalKeyPairCompanion.custom(
|
|
||||||
publicKey: Constant(kpMeta.publicKey),
|
|
||||||
privateKey: Constant(kpMeta.privateKey),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -208,8 +214,10 @@ class KeyPairProvider {
|
|||||||
final kpMeta = SnKeyPair(
|
final kpMeta = SnKeyPair(
|
||||||
id: id,
|
id: id,
|
||||||
accountId: _ua.user!.id,
|
accountId: _ua.user!.id,
|
||||||
publicKey: kp.publicKey,
|
// This is work as expected
|
||||||
privateKey: kp.privateKey,
|
// We need to share private key to let everyone can decode the message
|
||||||
|
publicKey: kp.privateKey,
|
||||||
|
privateKey: kp.publicKey,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Save the keypair to the local database
|
// Save the keypair to the local database
|
||||||
|
@ -53,12 +53,11 @@ class WebSocketProvider extends ChangeNotifier {
|
|||||||
try {
|
try {
|
||||||
_connectCompleter = Completer<void>();
|
_connectCompleter = Completer<void>();
|
||||||
|
|
||||||
final clientId = await FlutterUdid.consistentUdid;
|
|
||||||
final atk = await _sn.getFreshAtk();
|
final atk = await _sn.getFreshAtk();
|
||||||
final uri = Uri.parse(
|
final uri = Uri.parse(
|
||||||
kIsWeb
|
kIsWeb
|
||||||
? '${_sn.client.options.baseUrl.replaceFirst('http', 'ws')}/ws?tk=$atk'
|
? '${_sn.client.options.baseUrl.replaceFirst('http', 'ws')}/ws?tk=$atk'
|
||||||
: '${_sn.client.options.baseUrl.replaceFirst('http', 'ws')}/ws?clientId=${clientId}tk=$atk',
|
: '${_sn.client.options.baseUrl.replaceFirst('http', 'ws')}/ws?clientId=${await FlutterUdid.consistentUdid}tk=$atk',
|
||||||
);
|
);
|
||||||
|
|
||||||
isBusy = true;
|
isBusy = true;
|
||||||
|
@ -9,6 +9,7 @@ import 'package:popover/popover.dart';
|
|||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
import 'package:surface/providers/config.dart';
|
import 'package:surface/providers/config.dart';
|
||||||
|
import 'package:surface/providers/keypair.dart';
|
||||||
import 'package:surface/providers/user_directory.dart';
|
import 'package:surface/providers/user_directory.dart';
|
||||||
import 'package:surface/providers/userinfo.dart';
|
import 'package:surface/providers/userinfo.dart';
|
||||||
import 'package:surface/screens/account/profile_page.dart';
|
import 'package:surface/screens/account/profile_page.dart';
|
||||||
@ -106,30 +107,34 @@ class ChatMessage extends StatelessWidget {
|
|||||||
GestureDetector(
|
GestureDetector(
|
||||||
child: AccountImage(
|
child: AccountImage(
|
||||||
content: user?.avatar,
|
content: user?.avatar,
|
||||||
badge:
|
badge: (user?.badges.isNotEmpty ?? false)
|
||||||
(user?.badges.isNotEmpty ?? false)
|
? Icon(
|
||||||
? Icon(
|
kBadgesMeta[user!.badges.first.type]?.$2 ??
|
||||||
kBadgesMeta[user!.badges.first.type]?.$2 ?? Symbols.question_mark,
|
Symbols.question_mark,
|
||||||
color: kBadgesMeta[user.badges.first.type]?.$3,
|
color: kBadgesMeta[user.badges.first.type]?.$3,
|
||||||
fill: 1,
|
fill: 1,
|
||||||
size: 18,
|
size: 18,
|
||||||
shadows: [
|
shadows: [
|
||||||
Shadow(offset: Offset(1, 1), blurRadius: 5.0, color: Color.fromARGB(200, 0, 0, 0)),
|
Shadow(
|
||||||
],
|
offset: Offset(1, 1),
|
||||||
)
|
blurRadius: 5.0,
|
||||||
: null,
|
color: Color.fromARGB(200, 0, 0, 0)),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
: null,
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
if (user == null) return;
|
if (user == null) return;
|
||||||
showPopover(
|
showPopover(
|
||||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
backgroundColor:
|
||||||
|
Theme.of(context).colorScheme.surface,
|
||||||
context: context,
|
context: context,
|
||||||
transition: PopoverTransition.other,
|
transition: PopoverTransition.other,
|
||||||
bodyBuilder:
|
bodyBuilder: (context) => SizedBox(
|
||||||
(context) => SizedBox(
|
width: math.min(
|
||||||
width: math.min(400, MediaQuery.of(context).size.width - 10),
|
400, MediaQuery.of(context).size.width - 10),
|
||||||
child: AccountPopoverCard(data: user),
|
child: AccountPopoverCard(data: user),
|
||||||
),
|
),
|
||||||
direction: PopoverDirection.bottom,
|
direction: PopoverDirection.bottom,
|
||||||
arrowHeight: 5,
|
arrowHeight: 5,
|
||||||
arrowWidth: 15,
|
arrowWidth: 15,
|
||||||
@ -150,12 +155,19 @@ class ChatMessage extends StatelessWidget {
|
|||||||
Row(
|
Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
if (isCompact) AccountImage(content: user?.avatar, radius: 12).padding(right: 8),
|
if (isCompact)
|
||||||
|
AccountImage(
|
||||||
|
content: user?.avatar, radius: 12)
|
||||||
|
.padding(right: 8),
|
||||||
Text(
|
Text(
|
||||||
(data.sender.nick?.isNotEmpty ?? false) ? data.sender.nick! : user?.nick ?? 'unknown',
|
(data.sender.nick?.isNotEmpty ?? false)
|
||||||
|
? data.sender.nick!
|
||||||
|
: user?.nick ?? 'unknown',
|
||||||
).bold(),
|
).bold(),
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
Text(dateFormatter.format(data.createdAt.toLocal())).fontSize(13),
|
Text(dateFormatter
|
||||||
|
.format(data.createdAt.toLocal()))
|
||||||
|
.fontSize(13),
|
||||||
],
|
],
|
||||||
).height(21),
|
).height(21),
|
||||||
if (isCompact) const Gap(8),
|
if (isCompact) const Gap(8),
|
||||||
@ -164,10 +176,14 @@ class ChatMessage extends StatelessWidget {
|
|||||||
Container(
|
Container(
|
||||||
constraints: BoxConstraints(maxWidth: 360),
|
constraints: BoxConstraints(maxWidth: 360),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
borderRadius: const BorderRadius.all(
|
||||||
border: Border.all(color: Theme.of(context).dividerColor, width: 1),
|
Radius.circular(8)),
|
||||||
|
border: Border.all(
|
||||||
|
color: Theme.of(context).dividerColor,
|
||||||
|
width: 1),
|
||||||
),
|
),
|
||||||
padding: const EdgeInsets.only(left: 4, right: 4, top: 8, bottom: 6),
|
padding: const EdgeInsets.only(
|
||||||
|
left: 4, right: 4, top: 8, bottom: 6),
|
||||||
child: ChatMessage(
|
child: ChatMessage(
|
||||||
data: data.preload!.quoteEvent!,
|
data: data.preload!.quoteEvent!,
|
||||||
isCompact: true,
|
isCompact: true,
|
||||||
@ -179,11 +195,11 @@ class ChatMessage extends StatelessWidget {
|
|||||||
).padding(bottom: 4, top: 4),
|
).padding(bottom: 4, top: 4),
|
||||||
switch (data.type) {
|
switch (data.type) {
|
||||||
'messages.new' => _ChatMessageText(
|
'messages.new' => _ChatMessageText(
|
||||||
data: data,
|
data: data,
|
||||||
onReply: onReply,
|
onReply: onReply,
|
||||||
onEdit: onEdit,
|
onEdit: onEdit,
|
||||||
onDelete: onDelete,
|
onDelete: onDelete,
|
||||||
),
|
),
|
||||||
_ => _ChatMessageSystemNotify(data: data),
|
_ => _ChatMessageSystemNotify(data: data),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@ -204,9 +220,13 @@ class ChatMessage extends StatelessWidget {
|
|||||||
bordered: true,
|
bordered: true,
|
||||||
maxHeight: 360,
|
maxHeight: 360,
|
||||||
maxWidth: 480 - 48 - padding.left,
|
maxWidth: 480 - 48 - padding.left,
|
||||||
padding: padding.copyWith(top: 8, left: isCompact ? padding.left : 48 + padding.left),
|
padding: padding.copyWith(
|
||||||
|
top: 8, left: isCompact ? padding.left : 48 + padding.left),
|
||||||
),
|
),
|
||||||
if (!hasMerged && !isCompact) const Gap(12) else if (!isCompact) const Gap(8),
|
if (!hasMerged && !isCompact)
|
||||||
|
const Gap(12)
|
||||||
|
else if (!isCompact)
|
||||||
|
const Gap(8),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -220,7 +240,8 @@ class _ChatMessageText extends StatelessWidget {
|
|||||||
final Function(SnChatMessage)? onEdit;
|
final Function(SnChatMessage)? onEdit;
|
||||||
final Function(SnChatMessage)? onDelete;
|
final Function(SnChatMessage)? onDelete;
|
||||||
|
|
||||||
const _ChatMessageText({required this.data, this.onReply, this.onEdit, this.onDelete});
|
const _ChatMessageText(
|
||||||
|
{required this.data, this.onReply, this.onEdit, this.onDelete});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -234,7 +255,8 @@ class _ChatMessageText extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
SelectionArea(
|
SelectionArea(
|
||||||
contextMenuBuilder: (context, editableTextState) {
|
contextMenuBuilder: (context, editableTextState) {
|
||||||
final List<ContextMenuButtonItem> items = editableTextState.contextMenuButtonItems;
|
final List<ContextMenuButtonItem> items =
|
||||||
|
editableTextState.contextMenuButtonItems;
|
||||||
|
|
||||||
if (onReply != null) {
|
if (onReply != null) {
|
||||||
items.insert(
|
items.insert(
|
||||||
@ -278,13 +300,18 @@ class _ChatMessageText extends StatelessWidget {
|
|||||||
buttonItems: items,
|
buttonItems: items,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
child: MarkdownTextContent(
|
child: switch (data.body['algorithm']) {
|
||||||
content: data.body['text'],
|
'rsa' => _ChatDecryptMessage(message: data),
|
||||||
isAutoWarp: true,
|
_ => MarkdownTextContent(
|
||||||
isEnlargeSticker: RegExp(r"^:([-\w]+):$").hasMatch(data.body['text'] ?? ''),
|
content: data.body['text'],
|
||||||
),
|
isAutoWarp: true,
|
||||||
|
isEnlargeSticker:
|
||||||
|
RegExp(r"^:([-\w]+):$").hasMatch(data.body['text'] ?? ''),
|
||||||
|
),
|
||||||
|
},
|
||||||
),
|
),
|
||||||
if (data.updatedAt != data.createdAt) Text('messageEditedHint'.tr()).fontSize(13).opacity(0.75),
|
if (data.updatedAt != data.createdAt)
|
||||||
|
Text('messageEditedHint'.tr()).fontSize(13).opacity(0.75),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
} else if (data.body['attachments']?.isNotEmpty) {
|
} else if (data.body['attachments']?.isNotEmpty) {
|
||||||
@ -335,14 +362,19 @@ class _ChatMessageSystemNotify extends StatelessWidget {
|
|||||||
).opacity(0.75);
|
).opacity(0.75);
|
||||||
case 'calls.start':
|
case 'calls.start':
|
||||||
return Row(
|
return Row(
|
||||||
children: [const Icon(Symbols.call, size: 20), const Gap(4), Text('callMessageStarted'.tr())],
|
children: [
|
||||||
|
const Icon(Symbols.call, size: 20),
|
||||||
|
const Gap(4),
|
||||||
|
Text('callMessageStarted'.tr())
|
||||||
|
],
|
||||||
).opacity(0.75);
|
).opacity(0.75);
|
||||||
case 'calls.end':
|
case 'calls.end':
|
||||||
return Row(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
const Icon(Symbols.call_end, size: 20),
|
const Icon(Symbols.call_end, size: 20),
|
||||||
const Gap(4),
|
const Gap(4),
|
||||||
Text('callMessageEnded'.tr(args: [_formatDuration(Duration(seconds: data.body['last']))])),
|
Text('callMessageEnded'.tr(
|
||||||
|
args: [_formatDuration(Duration(seconds: data.body['last']))])),
|
||||||
],
|
],
|
||||||
).opacity(0.75);
|
).opacity(0.75);
|
||||||
default:
|
default:
|
||||||
@ -356,3 +388,33 @@ class _ChatMessageSystemNotify extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _ChatDecryptMessage extends StatelessWidget {
|
||||||
|
final SnChatMessage message;
|
||||||
|
const _ChatDecryptMessage({required this.message});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final kp = context.read<KeyPairProvider>();
|
||||||
|
return FutureBuilder(
|
||||||
|
future: kp.decryptText(
|
||||||
|
message.body['text'],
|
||||||
|
message.body['keypair_id'],
|
||||||
|
kpOwner: message.sender.accountId,
|
||||||
|
),
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (snapshot.hasError) {
|
||||||
|
return Text('decryptingKeyNotFound'.tr());
|
||||||
|
}
|
||||||
|
if (!snapshot.hasData) {
|
||||||
|
return Text('decrypting'.tr());
|
||||||
|
}
|
||||||
|
return MarkdownTextContent(
|
||||||
|
content: snapshot.data!,
|
||||||
|
isAutoWarp: true,
|
||||||
|
isEnlargeSticker: RegExp(r"^:([-\w]+):$").hasMatch(snapshot.data!),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -7,6 +7,8 @@ PODS:
|
|||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- device_info_plus (0.0.1):
|
- device_info_plus (0.0.1):
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
|
- fast_rsa (0.6.0):
|
||||||
|
- FlutterMacOS
|
||||||
- file_picker (0.0.1):
|
- file_picker (0.0.1):
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- file_saver (0.0.1):
|
- file_saver (0.0.1):
|
||||||
@ -23,14 +25,14 @@ PODS:
|
|||||||
- Firebase/Messaging (11.8.0):
|
- Firebase/Messaging (11.8.0):
|
||||||
- Firebase/CoreOnly
|
- Firebase/CoreOnly
|
||||||
- FirebaseMessaging (~> 11.8.0)
|
- FirebaseMessaging (~> 11.8.0)
|
||||||
- firebase_analytics (11.4.3):
|
- firebase_analytics (11.4.4):
|
||||||
- Firebase/Analytics (= 11.8.0)
|
- Firebase/Analytics (= 11.8.0)
|
||||||
- firebase_core
|
- firebase_core
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- firebase_core (3.12.0):
|
- firebase_core (3.12.1):
|
||||||
- Firebase/CoreOnly (~> 11.8.0)
|
- Firebase/CoreOnly (~> 11.8.0)
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- firebase_messaging (15.2.3):
|
- firebase_messaging (15.2.4):
|
||||||
- Firebase/CoreOnly (~> 11.8.0)
|
- Firebase/CoreOnly (~> 11.8.0)
|
||||||
- Firebase/Messaging (~> 11.8.0)
|
- Firebase/Messaging (~> 11.8.0)
|
||||||
- firebase_core
|
- firebase_core
|
||||||
@ -76,6 +78,8 @@ PODS:
|
|||||||
- flutter_inappwebview_macos (0.0.1):
|
- flutter_inappwebview_macos (0.0.1):
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- OrderedSet (~> 6.0.3)
|
- OrderedSet (~> 6.0.3)
|
||||||
|
- flutter_timezone (0.1.0):
|
||||||
|
- FlutterMacOS
|
||||||
- flutter_udid (0.0.1):
|
- flutter_udid (0.0.1):
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- SAMKeychain
|
- SAMKeychain
|
||||||
@ -86,6 +90,8 @@ PODS:
|
|||||||
- gal (1.0.0):
|
- gal (1.0.0):
|
||||||
- Flutter
|
- Flutter
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
|
- geolocator_apple (1.2.0):
|
||||||
|
- FlutterMacOS
|
||||||
- GoogleAppMeasurement (11.8.0):
|
- GoogleAppMeasurement (11.8.0):
|
||||||
- GoogleAppMeasurement/AdIdSupport (= 11.8.0)
|
- GoogleAppMeasurement/AdIdSupport (= 11.8.0)
|
||||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
||||||
@ -213,6 +219,7 @@ DEPENDENCIES:
|
|||||||
- connectivity_plus (from `Flutter/ephemeral/.symlinks/plugins/connectivity_plus/macos`)
|
- connectivity_plus (from `Flutter/ephemeral/.symlinks/plugins/connectivity_plus/macos`)
|
||||||
- croppy (from `Flutter/ephemeral/.symlinks/plugins/croppy/macos`)
|
- croppy (from `Flutter/ephemeral/.symlinks/plugins/croppy/macos`)
|
||||||
- device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`)
|
- device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`)
|
||||||
|
- fast_rsa (from `Flutter/ephemeral/.symlinks/plugins/fast_rsa/macos`)
|
||||||
- file_picker (from `Flutter/ephemeral/.symlinks/plugins/file_picker/macos`)
|
- file_picker (from `Flutter/ephemeral/.symlinks/plugins/file_picker/macos`)
|
||||||
- file_saver (from `Flutter/ephemeral/.symlinks/plugins/file_saver/macos`)
|
- file_saver (from `Flutter/ephemeral/.symlinks/plugins/file_saver/macos`)
|
||||||
- file_selector_macos (from `Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos`)
|
- file_selector_macos (from `Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos`)
|
||||||
@ -220,10 +227,12 @@ DEPENDENCIES:
|
|||||||
- firebase_core (from `Flutter/ephemeral/.symlinks/plugins/firebase_core/macos`)
|
- firebase_core (from `Flutter/ephemeral/.symlinks/plugins/firebase_core/macos`)
|
||||||
- firebase_messaging (from `Flutter/ephemeral/.symlinks/plugins/firebase_messaging/macos`)
|
- firebase_messaging (from `Flutter/ephemeral/.symlinks/plugins/firebase_messaging/macos`)
|
||||||
- flutter_inappwebview_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_inappwebview_macos/macos`)
|
- flutter_inappwebview_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_inappwebview_macos/macos`)
|
||||||
|
- flutter_timezone (from `Flutter/ephemeral/.symlinks/plugins/flutter_timezone/macos`)
|
||||||
- flutter_udid (from `Flutter/ephemeral/.symlinks/plugins/flutter_udid/macos`)
|
- flutter_udid (from `Flutter/ephemeral/.symlinks/plugins/flutter_udid/macos`)
|
||||||
- flutter_webrtc (from `Flutter/ephemeral/.symlinks/plugins/flutter_webrtc/macos`)
|
- flutter_webrtc (from `Flutter/ephemeral/.symlinks/plugins/flutter_webrtc/macos`)
|
||||||
- FlutterMacOS (from `Flutter/ephemeral`)
|
- FlutterMacOS (from `Flutter/ephemeral`)
|
||||||
- gal (from `Flutter/ephemeral/.symlinks/plugins/gal/darwin`)
|
- gal (from `Flutter/ephemeral/.symlinks/plugins/gal/darwin`)
|
||||||
|
- geolocator_apple (from `Flutter/ephemeral/.symlinks/plugins/geolocator_apple/macos`)
|
||||||
- hotkey_manager_macos (from `Flutter/ephemeral/.symlinks/plugins/hotkey_manager_macos/macos`)
|
- hotkey_manager_macos (from `Flutter/ephemeral/.symlinks/plugins/hotkey_manager_macos/macos`)
|
||||||
- in_app_review (from `Flutter/ephemeral/.symlinks/plugins/in_app_review/macos`)
|
- in_app_review (from `Flutter/ephemeral/.symlinks/plugins/in_app_review/macos`)
|
||||||
- livekit_client (from `Flutter/ephemeral/.symlinks/plugins/livekit_client/macos`)
|
- livekit_client (from `Flutter/ephemeral/.symlinks/plugins/livekit_client/macos`)
|
||||||
@ -272,6 +281,8 @@ EXTERNAL SOURCES:
|
|||||||
:path: Flutter/ephemeral/.symlinks/plugins/croppy/macos
|
:path: Flutter/ephemeral/.symlinks/plugins/croppy/macos
|
||||||
device_info_plus:
|
device_info_plus:
|
||||||
:path: Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos
|
:path: Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos
|
||||||
|
fast_rsa:
|
||||||
|
:path: Flutter/ephemeral/.symlinks/plugins/fast_rsa/macos
|
||||||
file_picker:
|
file_picker:
|
||||||
:path: Flutter/ephemeral/.symlinks/plugins/file_picker/macos
|
:path: Flutter/ephemeral/.symlinks/plugins/file_picker/macos
|
||||||
file_saver:
|
file_saver:
|
||||||
@ -286,6 +297,8 @@ EXTERNAL SOURCES:
|
|||||||
:path: Flutter/ephemeral/.symlinks/plugins/firebase_messaging/macos
|
:path: Flutter/ephemeral/.symlinks/plugins/firebase_messaging/macos
|
||||||
flutter_inappwebview_macos:
|
flutter_inappwebview_macos:
|
||||||
:path: Flutter/ephemeral/.symlinks/plugins/flutter_inappwebview_macos/macos
|
:path: Flutter/ephemeral/.symlinks/plugins/flutter_inappwebview_macos/macos
|
||||||
|
flutter_timezone:
|
||||||
|
:path: Flutter/ephemeral/.symlinks/plugins/flutter_timezone/macos
|
||||||
flutter_udid:
|
flutter_udid:
|
||||||
:path: Flutter/ephemeral/.symlinks/plugins/flutter_udid/macos
|
:path: Flutter/ephemeral/.symlinks/plugins/flutter_udid/macos
|
||||||
flutter_webrtc:
|
flutter_webrtc:
|
||||||
@ -294,6 +307,8 @@ EXTERNAL SOURCES:
|
|||||||
:path: Flutter/ephemeral
|
:path: Flutter/ephemeral
|
||||||
gal:
|
gal:
|
||||||
:path: Flutter/ephemeral/.symlinks/plugins/gal/darwin
|
:path: Flutter/ephemeral/.symlinks/plugins/gal/darwin
|
||||||
|
geolocator_apple:
|
||||||
|
:path: Flutter/ephemeral/.symlinks/plugins/geolocator_apple/macos
|
||||||
hotkey_manager_macos:
|
hotkey_manager_macos:
|
||||||
:path: Flutter/ephemeral/.symlinks/plugins/hotkey_manager_macos/macos
|
:path: Flutter/ephemeral/.symlinks/plugins/hotkey_manager_macos/macos
|
||||||
in_app_review:
|
in_app_review:
|
||||||
@ -338,23 +353,26 @@ SPEC CHECKSUMS:
|
|||||||
connectivity_plus: 0a976dfd033b59192912fa3c6c7b54aab5093802
|
connectivity_plus: 0a976dfd033b59192912fa3c6c7b54aab5093802
|
||||||
croppy: 25a638bd7d05411d8c697f481568f261037694fc
|
croppy: 25a638bd7d05411d8c697f481568f261037694fc
|
||||||
device_info_plus: 1b14eed9bf95428983aed283a8d51cce3d8c4215
|
device_info_plus: 1b14eed9bf95428983aed283a8d51cce3d8c4215
|
||||||
|
fast_rsa: 47a50bec1042c8c01726007dc0590a078418f997
|
||||||
file_picker: e716a70a9fe5fd9e09ebc922d7541464289443af
|
file_picker: e716a70a9fe5fd9e09ebc922d7541464289443af
|
||||||
file_saver: 44e6fbf666677faf097302460e214e977fdd977b
|
file_saver: 44e6fbf666677faf097302460e214e977fdd977b
|
||||||
file_selector_macos: cc3858c981fe6889f364731200d6232dac1d812d
|
file_selector_macos: cc3858c981fe6889f364731200d6232dac1d812d
|
||||||
Firebase: d80354ed7f6df5f9aca55e9eb47cc4b634735eaf
|
Firebase: d80354ed7f6df5f9aca55e9eb47cc4b634735eaf
|
||||||
firebase_analytics: 1a71372a9735d7046d2c69db848a8d178f9fb587
|
firebase_analytics: 75b9d9ea8b21ce77898a3a46910e2051e93db8e1
|
||||||
firebase_core: 68e1d27035b096239f147a041643e14e156f1481
|
firebase_core: 1b573eac37729348cdc472516991dd7e269ae37e
|
||||||
firebase_messaging: 89b5e0e28413dd878a58d2f286cdc03887b5d467
|
firebase_messaging: 0620038ea399ceae2218c9087fca00a28f576209
|
||||||
FirebaseAnalytics: 4fd42def128146e24e480e89f310e3d8534ea42b
|
FirebaseAnalytics: 4fd42def128146e24e480e89f310e3d8534ea42b
|
||||||
FirebaseCore: 99fe0c4b44a39f37d99e6404e02009d2db5d718d
|
FirebaseCore: 99fe0c4b44a39f37d99e6404e02009d2db5d718d
|
||||||
FirebaseCoreInternal: df24ce5af28864660ecbd13596fc8dd3a8c34629
|
FirebaseCoreInternal: df24ce5af28864660ecbd13596fc8dd3a8c34629
|
||||||
FirebaseInstallations: 6c963bd2a86aca0481eef4f48f5a4df783ae5917
|
FirebaseInstallations: 6c963bd2a86aca0481eef4f48f5a4df783ae5917
|
||||||
FirebaseMessaging: 487b634ccdf6f7b7ff180fdcb2a9935490f764e8
|
FirebaseMessaging: 487b634ccdf6f7b7ff180fdcb2a9935490f764e8
|
||||||
flutter_inappwebview_macos: bdf207b8f4ebd58e86ae06cd96b147de99a67c9b
|
flutter_inappwebview_macos: bdf207b8f4ebd58e86ae06cd96b147de99a67c9b
|
||||||
|
flutter_timezone: 62400baa441155f2a4144188648f2ff861649747
|
||||||
flutter_udid: 2e7b3da4b5fdfba86a396b97898f5fe8f4ec1a52
|
flutter_udid: 2e7b3da4b5fdfba86a396b97898f5fe8f4ec1a52
|
||||||
flutter_webrtc: d55fd3f5c75b42940b6b4b2cf376a5797398d1f8
|
flutter_webrtc: d55fd3f5c75b42940b6b4b2cf376a5797398d1f8
|
||||||
FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
|
FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
|
||||||
gal: 6a522c75909f1244732d4596d11d6a2f86ff37a5
|
gal: 6a522c75909f1244732d4596d11d6a2f86ff37a5
|
||||||
|
geolocator_apple: 72a78ae3f3e4ec0db62117bd93e34523f5011d58
|
||||||
GoogleAppMeasurement: fc0817122bd4d4189164f85374e06773b9561896
|
GoogleAppMeasurement: fc0817122bd4d4189164f85374e06773b9561896
|
||||||
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
|
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
|
||||||
GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d
|
GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d
|
||||||
|
@ -481,7 +481,7 @@ class SnLocalKeyPair extends Table
|
|||||||
String get actualTableName => $name;
|
String get actualTableName => $name;
|
||||||
static const String $name = 'sn_local_key_pair';
|
static const String $name = 'sn_local_key_pair';
|
||||||
@override
|
@override
|
||||||
Set<GeneratedColumn> get $primaryKey => const {};
|
Set<GeneratedColumn> get $primaryKey => {id};
|
||||||
@override
|
@override
|
||||||
SnLocalKeyPairData map(Map<String, dynamic> data, {String? tablePrefix}) {
|
SnLocalKeyPairData map(Map<String, dynamic> data, {String? tablePrefix}) {
|
||||||
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
|
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
|
||||||
|
Loading…
x
Reference in New Issue
Block a user