Compare commits
No commits in common. "76d8cd943dc2e4d0c790427930bb9c024ab323ba" and "cb2de52bee42b8333621379049951de0a942be86" have entirely different histories.
76d8cd943d
...
cb2de52bee
@ -759,7 +759,5 @@
|
||||
"accountKeyPairsDescription": "Manage the key pairs which used to encrypt messages.",
|
||||
"enrollNewKeyPair": "Enroll New One",
|
||||
"enrollNewKeyPairDescription": "Generate a new key pair.",
|
||||
"keyPairHasPrivateKey": "With private key",
|
||||
"decrypting": "Decrypting……",
|
||||
"decryptingKeyNotFound": "Key not found or exchange failed, the other party may not be online"
|
||||
"keyPairHasPrivateKey": "With private key"
|
||||
}
|
||||
|
@ -757,7 +757,5 @@
|
||||
"accountKeyPairsDescription": "管理用于加密信息的密钥对。",
|
||||
"enrollNewKeyPair": "新建密钥对",
|
||||
"enrollNewKeyPairDescription": "生成一对新密钥对。",
|
||||
"keyPairHasPrivateKey": "有私钥",
|
||||
"decrypting": "解密中……",
|
||||
"decryptingKeyNotFound": "未找到密钥对或交换失败,对方可能不在线"
|
||||
"keyPairHasPrivateKey": "有私钥"
|
||||
}
|
||||
|
@ -756,8 +756,6 @@
|
||||
"accountKeyPairs": "密鑰對",
|
||||
"accountKeyPairsDescription": "管理用於加密信息的密鑰對。",
|
||||
"enrollNewKeyPair": "新建密鑰對",
|
||||
"enrollNewKeyPairDescription": "生成一對新密鑰對。",
|
||||
"keyPairHasPrivateKey": "有私鑰",
|
||||
"decrypting": "解密中……",
|
||||
"decryptingKeyNotFound": "未找到密鑰對或交換失敗,對方可能不在線"
|
||||
"enrollNewKeyPairDescription": "生成一對新密鑰對,覆蓋當前的;如果已有一個密鑰將會丟棄舊密鑰的私鑰。",
|
||||
"keyPairHasPrivateKey": "有私鑰"
|
||||
}
|
||||
|
@ -756,8 +756,6 @@
|
||||
"accountKeyPairs": "密鑰對",
|
||||
"accountKeyPairsDescription": "管理用於加密信息的密鑰對。",
|
||||
"enrollNewKeyPair": "新建密鑰對",
|
||||
"enrollNewKeyPairDescription": "生成一對新密鑰對。",
|
||||
"keyPairHasPrivateKey": "有私鑰",
|
||||
"decrypting": "解密中……",
|
||||
"decryptingKeyNotFound": "未找到密鑰對或交換失敗,對方可能不在線"
|
||||
"enrollNewKeyPairDescription": "生成一對新密鑰對,覆蓋當前的;如果已有一個密鑰將會丟棄舊密鑰的私鑰。",
|
||||
"keyPairHasPrivateKey": "有私鑰"
|
||||
}
|
||||
|
@ -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":[],"explicit_pk":["id"]}}]}
|
||||
{"_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":[]}}]}
|
@ -9,7 +9,6 @@ import 'package:provider/provider.dart';
|
||||
import 'package:surface/database/database.dart';
|
||||
import 'package:surface/logger.dart';
|
||||
import 'package:surface/providers/database.dart';
|
||||
import 'package:surface/providers/keypair.dart';
|
||||
import 'package:surface/providers/sn_attachment.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/providers/user_directory.dart';
|
||||
@ -26,7 +25,6 @@ class ChatMessageController extends ChangeNotifier {
|
||||
late final WebSocketProvider _ws;
|
||||
late final SnAttachmentProvider _attach;
|
||||
late final DatabaseProvider _dt;
|
||||
late final KeyPairProvider _kp;
|
||||
|
||||
StreamSubscription? _wsSubscription;
|
||||
|
||||
@ -36,7 +34,6 @@ class ChatMessageController extends ChangeNotifier {
|
||||
_ws = context.read<WebSocketProvider>();
|
||||
_attach = context.read<SnAttachmentProvider>();
|
||||
_dt = context.read<DatabaseProvider>();
|
||||
_kp = context.read<KeyPairProvider>();
|
||||
}
|
||||
|
||||
bool isPending = true;
|
||||
@ -248,24 +245,6 @@ class ChatMessageController extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> _encodeMessageBody(
|
||||
String text,
|
||||
bool isEncrypted,
|
||||
) async {
|
||||
if (!isEncrypted || _kp.activeKp == null) {
|
||||
return {
|
||||
'text': text,
|
||||
'algorithm': 'plain',
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
'text': await _kp.encryptText(text),
|
||||
'algorithm': 'rsa',
|
||||
'keypair_id': _kp.activeKp!.id,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> sendMessage(
|
||||
String type,
|
||||
String content, {
|
||||
@ -273,13 +252,13 @@ class ChatMessageController extends ChangeNotifier {
|
||||
int? relatedId,
|
||||
List<String>? attachments,
|
||||
SnChatMessage? editingMessage,
|
||||
bool isEncrypted = false,
|
||||
}) async {
|
||||
if (channel == null) return;
|
||||
const uuid = Uuid();
|
||||
final nonce = uuid.v4();
|
||||
final body = {
|
||||
...(await _encodeMessageBody(content, isEncrypted)),
|
||||
'text': content,
|
||||
'algorithm': 'plain',
|
||||
if (quoteId != null) 'quote_event': quoteId,
|
||||
if (relatedId != null) 'related_event': relatedId,
|
||||
if (attachments != null && attachments.isNotEmpty)
|
||||
|
@ -621,7 +621,7 @@ class $SnLocalKeyPairTable extends SnLocalKeyPair
|
||||
}
|
||||
|
||||
@override
|
||||
Set<GeneratedColumn> get $primaryKey => {id};
|
||||
Set<GeneratedColumn> get $primaryKey => const {};
|
||||
@override
|
||||
SnLocalKeyPairData map(Map<String, dynamic> data, {String? tablePrefix}) {
|
||||
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
|
||||
|
@ -47,9 +47,7 @@ final class Schema2 extends i0.VersionedSchema {
|
||||
entityName: 'sn_local_key_pair',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: [
|
||||
'PRIMARY KEY(id)',
|
||||
],
|
||||
tableConstraints: [],
|
||||
columns: [
|
||||
_column_5,
|
||||
_column_6,
|
||||
|
@ -10,7 +10,4 @@ class SnLocalKeyPair extends Table {
|
||||
TextColumn get privateKey => text().nullable()();
|
||||
|
||||
BoolColumn get isActive => boolean().withDefault(Constant(false))();
|
||||
|
||||
@override
|
||||
Set<Column<Object>> get primaryKey => {id};
|
||||
}
|
||||
|
@ -35,35 +35,24 @@ class KeyPairProvider {
|
||||
case 'kex.ack':
|
||||
ackKeyExchange(event);
|
||||
break;
|
||||
case 'kex.ask':
|
||||
case 'key.ask':
|
||||
replyAskKeyExchange(event);
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<String> decryptText(String text, String kpId, {int? kpOwner}) async {
|
||||
String? publicKey;
|
||||
Future<String> decryptText(String text, String kpId) async {
|
||||
final kp = await (_dt.db.snLocalKeyPair.select()
|
||||
..where((e) => e.id.equals(kpId)))
|
||||
.getSingleOrNull();
|
||||
if (kp == null) {
|
||||
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);
|
||||
if (kp == null) throw Exception('Key pair not found');
|
||||
return await RSA.decryptPKCS1v15(text, kp.privateKey!);
|
||||
}
|
||||
|
||||
Future<String> encryptText(String text) async {
|
||||
if (activeKp == null) throw Exception('No active key pair');
|
||||
return await RSA.encryptPKCS1v15(text, activeKp!.privateKey!);
|
||||
return await RSA.encryptPKCS1v15(text, activeKp!.publicKey);
|
||||
}
|
||||
|
||||
final Map<String, Completer<SnKeyPair>> _requests = {};
|
||||
@ -76,7 +65,7 @@ class KeyPairProvider {
|
||||
|
||||
_ws.conn?.sink.add(
|
||||
jsonEncode(WebSocketPackage(
|
||||
method: 'kex.ask',
|
||||
method: 'key.ask',
|
||||
endpoint: 'id',
|
||||
payload: {
|
||||
'keypair_id': kpId,
|
||||
@ -116,7 +105,12 @@ class KeyPairProvider {
|
||||
publicKey: kpMeta.publicKey,
|
||||
privateKey: Value(kpMeta.privateKey),
|
||||
),
|
||||
onConflict: DoNothing(),
|
||||
onConflict: DoUpdate(
|
||||
(_) => SnLocalKeyPairCompanion.custom(
|
||||
publicKey: Constant(kpMeta.publicKey),
|
||||
privateKey: Constant(kpMeta.privateKey),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -214,10 +208,8 @@ class KeyPairProvider {
|
||||
final kpMeta = SnKeyPair(
|
||||
id: id,
|
||||
accountId: _ua.user!.id,
|
||||
// This is work as expected
|
||||
// We need to share private key to let everyone can decode the message
|
||||
publicKey: kp.privateKey,
|
||||
privateKey: kp.publicKey,
|
||||
publicKey: kp.publicKey,
|
||||
privateKey: kp.privateKey,
|
||||
);
|
||||
|
||||
// Save the keypair to the local database
|
||||
|
@ -53,11 +53,12 @@ class WebSocketProvider extends ChangeNotifier {
|
||||
try {
|
||||
_connectCompleter = Completer<void>();
|
||||
|
||||
final clientId = await FlutterUdid.consistentUdid;
|
||||
final atk = await _sn.getFreshAtk();
|
||||
final uri = Uri.parse(
|
||||
kIsWeb
|
||||
? '${_sn.client.options.baseUrl.replaceFirst('http', 'ws')}/ws?tk=$atk'
|
||||
: '${_sn.client.options.baseUrl.replaceFirst('http', 'ws')}/ws?clientId=${await FlutterUdid.consistentUdid}tk=$atk',
|
||||
: '${_sn.client.options.baseUrl.replaceFirst('http', 'ws')}/ws?clientId=${clientId}tk=$atk',
|
||||
);
|
||||
|
||||
isBusy = true;
|
||||
|
@ -1,5 +1,4 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
@ -20,7 +19,6 @@ import 'package:surface/providers/user_directory.dart';
|
||||
import 'package:surface/providers/userinfo.dart';
|
||||
import 'package:surface/providers/websocket.dart';
|
||||
import 'package:surface/types/chat.dart';
|
||||
import 'package:surface/types/websocket.dart';
|
||||
import 'package:surface/widgets/chat/call/call_prejoin.dart';
|
||||
import 'package:surface/widgets/chat/chat_message.dart';
|
||||
import 'package:surface/widgets/chat/chat_message_input.dart';
|
||||
@ -60,8 +58,6 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
|
||||
final GlobalKey<ChatMessageInputState> _inputGlobalKey = GlobalKey();
|
||||
late final ChatMessageController _messageController;
|
||||
|
||||
bool _isEncrypted = false;
|
||||
|
||||
StreamSubscription? _wsSubscription;
|
||||
|
||||
// TODO fetch user identity and ask them to join the channel or not
|
||||
@ -95,14 +91,9 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
|
||||
nty.skippableNotifyChannel = _channel!.id;
|
||||
final ws = context.read<WebSocketProvider>();
|
||||
if (_channel != null) {
|
||||
ws.conn?.sink.add(
|
||||
jsonEncode(WebSocketPackage(
|
||||
method: 'events.subscribe',
|
||||
endpoint: 'im',
|
||||
payload: {
|
||||
'channel_id': _channel!.id,
|
||||
})),
|
||||
);
|
||||
ws.conn?.sink.add({
|
||||
'channel_id': _channel?.id,
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
@ -256,15 +247,9 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
|
||||
nty.skippableNotifyChannel = null;
|
||||
final ws = context.read<WebSocketProvider>();
|
||||
if (_channel != null) {
|
||||
ws.conn?.sink.add(
|
||||
jsonEncode(WebSocketPackage(
|
||||
method: 'events.unsubscribe',
|
||||
endpoint: 'im',
|
||||
payload: {
|
||||
'channel_id': _channel!.id,
|
||||
},
|
||||
)),
|
||||
);
|
||||
ws.conn?.sink.add({
|
||||
'channel_id': _channel?.id,
|
||||
});
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
@ -283,15 +268,6 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
|
||||
: _channel?.name ?? 'loading'.tr(),
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
setState(() => _isEncrypted = !_isEncrypted);
|
||||
_inputGlobalKey.currentState?.setEncrypted(_isEncrypted);
|
||||
},
|
||||
icon: _isEncrypted
|
||||
? const Icon(Symbols.lock)
|
||||
: const Icon(Symbols.no_encryption),
|
||||
),
|
||||
IconButton(
|
||||
icon: _ongoingCall == null
|
||||
? const Icon(Symbols.call)
|
||||
|
@ -9,7 +9,6 @@ import 'package:popover/popover.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/providers/config.dart';
|
||||
import 'package:surface/providers/keypair.dart';
|
||||
import 'package:surface/providers/user_directory.dart';
|
||||
import 'package:surface/providers/userinfo.dart';
|
||||
import 'package:surface/screens/account/profile_page.dart';
|
||||
@ -20,7 +19,6 @@ import 'package:surface/widgets/attachment/attachment_list.dart';
|
||||
import 'package:surface/widgets/context_menu.dart';
|
||||
import 'package:surface/widgets/link_preview.dart';
|
||||
import 'package:surface/widgets/markdown_content.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
import 'package:swipe_to/swipe_to.dart';
|
||||
|
||||
class ChatMessage extends StatelessWidget {
|
||||
@ -108,34 +106,30 @@ class ChatMessage extends StatelessWidget {
|
||||
GestureDetector(
|
||||
child: AccountImage(
|
||||
content: user?.avatar,
|
||||
badge: (user?.badges.isNotEmpty ?? false)
|
||||
? Icon(
|
||||
kBadgesMeta[user!.badges.first.type]?.$2 ??
|
||||
Symbols.question_mark,
|
||||
color: kBadgesMeta[user.badges.first.type]?.$3,
|
||||
fill: 1,
|
||||
size: 18,
|
||||
shadows: [
|
||||
Shadow(
|
||||
offset: Offset(1, 1),
|
||||
blurRadius: 5.0,
|
||||
color: Color.fromARGB(200, 0, 0, 0)),
|
||||
],
|
||||
)
|
||||
: null,
|
||||
badge:
|
||||
(user?.badges.isNotEmpty ?? false)
|
||||
? Icon(
|
||||
kBadgesMeta[user!.badges.first.type]?.$2 ?? Symbols.question_mark,
|
||||
color: kBadgesMeta[user.badges.first.type]?.$3,
|
||||
fill: 1,
|
||||
size: 18,
|
||||
shadows: [
|
||||
Shadow(offset: Offset(1, 1), blurRadius: 5.0, color: Color.fromARGB(200, 0, 0, 0)),
|
||||
],
|
||||
)
|
||||
: null,
|
||||
),
|
||||
onTap: () {
|
||||
if (user == null) return;
|
||||
showPopover(
|
||||
backgroundColor:
|
||||
Theme.of(context).colorScheme.surface,
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
context: context,
|
||||
transition: PopoverTransition.other,
|
||||
bodyBuilder: (context) => SizedBox(
|
||||
width: math.min(
|
||||
400, MediaQuery.of(context).size.width - 10),
|
||||
child: AccountPopoverCard(data: user),
|
||||
),
|
||||
bodyBuilder:
|
||||
(context) => SizedBox(
|
||||
width: math.min(400, MediaQuery.of(context).size.width - 10),
|
||||
child: AccountPopoverCard(data: user),
|
||||
),
|
||||
direction: PopoverDirection.bottom,
|
||||
arrowHeight: 5,
|
||||
arrowWidth: 15,
|
||||
@ -156,19 +150,12 @@ class ChatMessage extends StatelessWidget {
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
if (isCompact)
|
||||
AccountImage(
|
||||
content: user?.avatar, radius: 12)
|
||||
.padding(right: 8),
|
||||
if (isCompact) AccountImage(content: user?.avatar, radius: 12).padding(right: 8),
|
||||
Text(
|
||||
(data.sender.nick?.isNotEmpty ?? false)
|
||||
? data.sender.nick!
|
||||
: user?.nick ?? 'unknown',
|
||||
(data.sender.nick?.isNotEmpty ?? false) ? data.sender.nick! : user?.nick ?? 'unknown',
|
||||
).bold(),
|
||||
const Gap(8),
|
||||
Text(dateFormatter
|
||||
.format(data.createdAt.toLocal()))
|
||||
.fontSize(13),
|
||||
Text(dateFormatter.format(data.createdAt.toLocal())).fontSize(13),
|
||||
],
|
||||
).height(21),
|
||||
if (isCompact) const Gap(8),
|
||||
@ -177,14 +164,10 @@ class ChatMessage extends StatelessWidget {
|
||||
Container(
|
||||
constraints: BoxConstraints(maxWidth: 360),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(8)),
|
||||
border: Border.all(
|
||||
color: Theme.of(context).dividerColor,
|
||||
width: 1),
|
||||
borderRadius: const BorderRadius.all(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(
|
||||
data: data.preload!.quoteEvent!,
|
||||
isCompact: true,
|
||||
@ -196,11 +179,11 @@ class ChatMessage extends StatelessWidget {
|
||||
).padding(bottom: 4, top: 4),
|
||||
switch (data.type) {
|
||||
'messages.new' => _ChatMessageText(
|
||||
data: data,
|
||||
onReply: onReply,
|
||||
onEdit: onEdit,
|
||||
onDelete: onDelete,
|
||||
),
|
||||
data: data,
|
||||
onReply: onReply,
|
||||
onEdit: onEdit,
|
||||
onDelete: onDelete,
|
||||
),
|
||||
_ => _ChatMessageSystemNotify(data: data),
|
||||
},
|
||||
],
|
||||
@ -221,13 +204,9 @@ class ChatMessage extends StatelessWidget {
|
||||
bordered: true,
|
||||
maxHeight: 360,
|
||||
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),
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -241,8 +220,7 @@ class _ChatMessageText extends StatelessWidget {
|
||||
final Function(SnChatMessage)? onEdit;
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
@ -256,8 +234,7 @@ class _ChatMessageText extends StatelessWidget {
|
||||
children: [
|
||||
SelectionArea(
|
||||
contextMenuBuilder: (context, editableTextState) {
|
||||
final List<ContextMenuButtonItem> items =
|
||||
editableTextState.contextMenuButtonItems;
|
||||
final List<ContextMenuButtonItem> items = editableTextState.contextMenuButtonItems;
|
||||
|
||||
if (onReply != null) {
|
||||
items.insert(
|
||||
@ -301,18 +278,13 @@ class _ChatMessageText extends StatelessWidget {
|
||||
buttonItems: items,
|
||||
);
|
||||
},
|
||||
child: switch (data.body['algorithm']) {
|
||||
'rsa' => _ChatDecryptMessage(message: data),
|
||||
_ => MarkdownTextContent(
|
||||
content: data.body['text'],
|
||||
isAutoWarp: true,
|
||||
isEnlargeSticker:
|
||||
RegExp(r"^:([-\w]+):$").hasMatch(data.body['text'] ?? ''),
|
||||
),
|
||||
},
|
||||
child: MarkdownTextContent(
|
||||
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) {
|
||||
@ -322,7 +294,7 @@ class _ChatMessageText extends StatelessWidget {
|
||||
const Gap(4),
|
||||
Text('messageFileHint'.plural(data.body['attachments']!.length)),
|
||||
],
|
||||
).opacity(0.8);
|
||||
).opacity(0.75);
|
||||
}
|
||||
|
||||
return const SizedBox.shrink();
|
||||
@ -348,102 +320,39 @@ class _ChatMessageSystemNotify extends StatelessWidget {
|
||||
case 'messages.edit':
|
||||
return Row(
|
||||
children: [
|
||||
const Icon(Symbols.edit, size: 16),
|
||||
const Gap(8),
|
||||
const Icon(Symbols.edit, size: 20),
|
||||
const Gap(4),
|
||||
Text('messageEdited'.tr(args: ['#${data.relatedEventId}'])),
|
||||
],
|
||||
).opacity(0.8);
|
||||
).opacity(0.75);
|
||||
case 'messages.delete':
|
||||
return Row(
|
||||
children: [
|
||||
const Icon(Symbols.delete, size: 16),
|
||||
const Gap(8),
|
||||
const Icon(Symbols.delete, size: 20),
|
||||
const Gap(4),
|
||||
Text('messageDeleted'.tr(args: ['#${data.relatedEventId}'])),
|
||||
],
|
||||
).opacity(0.8);
|
||||
).opacity(0.75);
|
||||
case 'calls.start':
|
||||
return Row(
|
||||
children: [
|
||||
const Icon(Symbols.call, size: 16),
|
||||
const Gap(8),
|
||||
Text('callMessageStarted'.tr())
|
||||
],
|
||||
).opacity(0.8);
|
||||
children: [const Icon(Symbols.call, size: 20), const Gap(4), Text('callMessageStarted'.tr())],
|
||||
).opacity(0.75);
|
||||
case 'calls.end':
|
||||
return Row(
|
||||
children: [
|
||||
const Icon(Symbols.call_end, size: 16),
|
||||
const Gap(8),
|
||||
Text('callMessageEnded'.tr(
|
||||
args: [_formatDuration(Duration(seconds: data.body['last']))])),
|
||||
const Icon(Symbols.call_end, size: 20),
|
||||
const Gap(4),
|
||||
Text('callMessageEnded'.tr(args: [_formatDuration(Duration(seconds: data.body['last']))])),
|
||||
],
|
||||
).opacity(0.8);
|
||||
).opacity(0.75);
|
||||
default:
|
||||
return Row(
|
||||
children: [
|
||||
const Icon(Symbols.info, size: 16),
|
||||
const Gap(8),
|
||||
const Icon(Symbols.info, size: 20),
|
||||
const Gap(4),
|
||||
Text('messageUnsupported'.tr(args: [data.type])),
|
||||
],
|
||||
).opacity(0.8);
|
||||
).opacity(0.75);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _ChatDecryptMessage extends StatelessWidget {
|
||||
final SnChatMessage message;
|
||||
const _ChatDecryptMessage({required this.message});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final kp = context.read<KeyPairProvider>();
|
||||
return FutureBuilder(
|
||||
key: Key('chat-message-encrypted-${message.id}'),
|
||||
future: kp.decryptText(
|
||||
message.body['text'],
|
||||
message.body['keypair_id'],
|
||||
kpOwner: message.sender.accountId,
|
||||
),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasError) {
|
||||
return Row(
|
||||
children: [
|
||||
Icon(Symbols.key_off, size: 16),
|
||||
const Gap(8),
|
||||
Expanded(
|
||||
child: Text('decryptingKeyNotFound'.tr()),
|
||||
),
|
||||
],
|
||||
).opacity(0.8);
|
||||
}
|
||||
if (!snapshot.hasData) {
|
||||
return Row(
|
||||
children: [
|
||||
Animate(
|
||||
autoPlay: true,
|
||||
onPlay: (e) => e.repeat(),
|
||||
effects: [
|
||||
RotateEffect(
|
||||
duration: const Duration(seconds: 3),
|
||||
begin: 0,
|
||||
end: -1,
|
||||
)
|
||||
],
|
||||
child: Icon(Symbols.sync, size: 16),
|
||||
),
|
||||
const Gap(8),
|
||||
Expanded(
|
||||
child: Text('decrypting'.tr()),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
return MarkdownTextContent(
|
||||
content: snapshot.data!,
|
||||
isAutoWarp: true,
|
||||
isEnlargeSticker: RegExp(r"^:([-\w]+):$").hasMatch(snapshot.data!),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -28,8 +28,7 @@ class ChatMessageInput extends StatefulWidget {
|
||||
final ChatMessageController controller;
|
||||
final SnChannelMember? otherMember;
|
||||
|
||||
const ChatMessageInput(
|
||||
{super.key, required this.controller, this.otherMember});
|
||||
const ChatMessageInput({super.key, required this.controller, this.otherMember});
|
||||
|
||||
@override
|
||||
State<ChatMessageInput> createState() => ChatMessageInputState();
|
||||
@ -39,8 +38,6 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
||||
bool _isBusy = false;
|
||||
double? _progress;
|
||||
|
||||
bool _isEncrypted = false;
|
||||
|
||||
SnChatMessage? _replyingMessage, _editingMessage;
|
||||
|
||||
final TextEditingController _contentController = TextEditingController();
|
||||
@ -48,20 +45,12 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
||||
|
||||
final HotKey _pasteHotKey = HotKey(
|
||||
key: PhysicalKeyboardKey.keyV,
|
||||
modifiers: [
|
||||
(!kIsWeb && Platform.isMacOS)
|
||||
? HotKeyModifier.meta
|
||||
: HotKeyModifier.control
|
||||
],
|
||||
modifiers: [(!kIsWeb && Platform.isMacOS) ? HotKeyModifier.meta : HotKeyModifier.control],
|
||||
scope: HotKeyScope.inapp,
|
||||
);
|
||||
final HotKey _newLineHotKey = HotKey(
|
||||
key: PhysicalKeyboardKey.enter,
|
||||
modifiers: [
|
||||
(!kIsWeb && Platform.isMacOS)
|
||||
? HotKeyModifier.meta
|
||||
: HotKeyModifier.control
|
||||
],
|
||||
modifiers: [(!kIsWeb && Platform.isMacOS) ? HotKeyModifier.meta : HotKeyModifier.control],
|
||||
scope: HotKeyScope.inapp,
|
||||
);
|
||||
|
||||
@ -94,10 +83,6 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
||||
});
|
||||
}
|
||||
|
||||
void setEncrypted(bool value) {
|
||||
setState(() => _isEncrypted = value);
|
||||
}
|
||||
|
||||
void setReply(SnChatMessage? value) {
|
||||
setState(() => _replyingMessage = value);
|
||||
}
|
||||
@ -115,8 +100,7 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
||||
void setEdit(SnChatMessage? value) {
|
||||
_contentController.text = value?.body['text'] ?? '';
|
||||
_attachments.clear();
|
||||
_attachments.addAll(
|
||||
value?.preload?.attachments?.map((e) => PostWriteMedia(e)) ?? []);
|
||||
_attachments.addAll(value?.preload?.attachments?.map((e) => PostWriteMedia(e)) ?? []);
|
||||
setState(() => _editingMessage = value);
|
||||
}
|
||||
|
||||
@ -155,9 +139,7 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
||||
media.name,
|
||||
'messaging',
|
||||
null,
|
||||
mimetype: media.raw != null && media.type == SnMediaType.image
|
||||
? 'image/png'
|
||||
: null,
|
||||
mimetype: media.raw != null && media.type == SnMediaType.image ? 'image/png' : null,
|
||||
);
|
||||
|
||||
final item = await attach.chunkedUploadParts(
|
||||
@ -189,14 +171,10 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
||||
widget.controller.sendMessage(
|
||||
_editingMessage != null ? 'messages.edit' : 'messages.new',
|
||||
_contentController.text,
|
||||
attachments: _attachments
|
||||
.where((e) => e.attachment != null)
|
||||
.map((e) => e.attachment!.rid)
|
||||
.toList(),
|
||||
attachments: _attachments.where((e) => e.attachment != null).map((e) => e.attachment!.rid).toList(),
|
||||
relatedId: _editingMessage?.id,
|
||||
quoteId: _replyingMessage?.id,
|
||||
editingMessage: _editingMessage,
|
||||
isEncrypted: _isEncrypted,
|
||||
);
|
||||
_contentController.clear();
|
||||
_attachments.clear();
|
||||
@ -254,15 +232,12 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
||||
TweenAnimationBuilder<double>(
|
||||
tween: Tween(begin: 0, end: _progress),
|
||||
duration: Duration(milliseconds: 300),
|
||||
builder: (context, value, _) =>
|
||||
LinearProgressIndicator(value: value, minHeight: 2),
|
||||
builder: (context, value, _) => LinearProgressIndicator(value: value, minHeight: 2),
|
||||
)
|
||||
else if (_isBusy)
|
||||
const LinearProgressIndicator(value: null, minHeight: 2),
|
||||
Padding(
|
||||
padding: _attachments.isNotEmpty
|
||||
? const EdgeInsets.only(top: 8)
|
||||
: EdgeInsets.zero,
|
||||
padding: _attachments.isNotEmpty ? const EdgeInsets.only(top: 8) : EdgeInsets.zero,
|
||||
child: PostMediaPendingList(
|
||||
attachments: _attachments,
|
||||
isBusy: _isBusy,
|
||||
@ -274,8 +249,9 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
||||
},
|
||||
onUpdateBusy: (state) => setState(() => _isBusy = state),
|
||||
),
|
||||
).height(_attachments.isNotEmpty ? 80 + 8 : 0, animate: true).animate(
|
||||
const Duration(milliseconds: 300), Curves.fastEaseInToSlowEaseOut),
|
||||
)
|
||||
.height(_attachments.isNotEmpty ? 80 + 8 : 0, animate: true)
|
||||
.animate(const Duration(milliseconds: 300), Curves.fastEaseInToSlowEaseOut),
|
||||
SingleChildScrollView(
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
child: _replyingMessage != null
|
||||
@ -296,8 +272,7 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
||||
const Gap(8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
_replyingMessage?.body['text'] ??
|
||||
'${_replyingMessage?.sender.nick}',
|
||||
_replyingMessage?.body['text'] ?? '${_replyingMessage?.sender.nick}',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
@ -314,8 +289,9 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
||||
).padding(vertical: 8),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
).height(_replyingMessage != null ? 38 : 0, animate: true).animate(
|
||||
const Duration(milliseconds: 300), Curves.fastEaseInToSlowEaseOut),
|
||||
)
|
||||
.height(_replyingMessage != null ? 38 : 0, animate: true)
|
||||
.animate(const Duration(milliseconds: 300), Curves.fastEaseInToSlowEaseOut),
|
||||
SingleChildScrollView(
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
child: _editingMessage != null
|
||||
@ -336,8 +312,7 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
||||
const Gap(8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
_editingMessage?.body['text'] ??
|
||||
'${_editingMessage?.sender.nick}',
|
||||
_editingMessage?.body['text'] ?? '${_editingMessage?.sender.nick}',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
@ -355,41 +330,30 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
||||
).padding(vertical: 8),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
).height(_editingMessage != null ? 38 : 0, animate: true).animate(
|
||||
const Duration(milliseconds: 300), Curves.fastEaseInToSlowEaseOut),
|
||||
)
|
||||
.height(_editingMessage != null ? 38 : 0, animate: true)
|
||||
.animate(const Duration(milliseconds: 300), Curves.fastEaseInToSlowEaseOut),
|
||||
Container(
|
||||
padding: EdgeInsets.only(
|
||||
left: 16,
|
||||
right: 16,
|
||||
top: 6,
|
||||
),
|
||||
constraints: BoxConstraints(maxHeight: 240),
|
||||
padding: EdgeInsets.symmetric(horizontal: 16),
|
||||
constraints: BoxConstraints(minHeight: 56, maxHeight: 240),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
focusNode: _focusNode,
|
||||
controller: _contentController,
|
||||
decoration: InputDecoration(
|
||||
prefixIconConstraints: const BoxConstraints(
|
||||
minWidth: 24,
|
||||
maxWidth: 24,
|
||||
),
|
||||
prefixIcon:
|
||||
_isEncrypted ? Icon(Symbols.lock, size: 18) : null,
|
||||
isCollapsed: true,
|
||||
hintText: widget.otherMember != null
|
||||
? 'fieldChatMessageDirect'.tr(args: [
|
||||
'@${ud.getAccountFromCache(widget.otherMember?.accountId)?.name}',
|
||||
])
|
||||
: 'fieldChatMessage'.tr(args: [
|
||||
widget.controller.channel?.name ?? 'loading'.tr()
|
||||
]),
|
||||
: 'fieldChatMessage'.tr(args: [widget.controller.channel?.name ?? 'loading'.tr()]),
|
||||
border: InputBorder.none,
|
||||
),
|
||||
textInputAction: TextInputAction.send,
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
onSubmitted: (_) {
|
||||
if (_isBusy) return;
|
||||
_sendMessage();
|
||||
@ -398,18 +362,20 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
||||
maxLines: null,
|
||||
),
|
||||
),
|
||||
const Gap(8),
|
||||
IconButton(
|
||||
visualDensity: VisualDensity.compact,
|
||||
icon: Icon(
|
||||
Symbols.mood,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
visualDensity: const VisualDensity(horizontal: -4, vertical: -4),
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(),
|
||||
onPressed: () {
|
||||
_showEmojiPicker(context);
|
||||
},
|
||||
),
|
||||
AddPostMediaButton(
|
||||
visualDensity: VisualDensity.compact,
|
||||
onAdd: (items) {
|
||||
setState(() {
|
||||
_attachments.addAll(items);
|
||||
@ -417,18 +383,18 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
visualDensity: VisualDensity.compact,
|
||||
onPressed: _isBusy ? null : _sendMessage,
|
||||
icon: Icon(
|
||||
Symbols.send,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
visualDensity: const VisualDensity(horizontal: -4, vertical: -4),
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Gap(MediaQuery.of(context).padding.bottom),
|
||||
],
|
||||
);
|
||||
}
|
||||
@ -439,8 +405,7 @@ class _StickerPicker extends StatelessWidget {
|
||||
final Function? onDismiss;
|
||||
final Function(String)? onInsert;
|
||||
|
||||
const _StickerPicker(
|
||||
{this.onDismiss, required this.originalText, this.onInsert});
|
||||
const _StickerPicker({this.onDismiss, required this.originalText, this.onInsert});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -466,10 +431,8 @@ class _StickerPicker extends StatelessWidget {
|
||||
return <Widget>[
|
||||
Container(
|
||||
margin: EdgeInsets.only(bottom: 8),
|
||||
padding:
|
||||
EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
color:
|
||||
Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@ -481,8 +444,7 @@ class _StickerPicker extends StatelessWidget {
|
||||
),
|
||||
GridView.builder(
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
padding:
|
||||
const EdgeInsets.only(left: 8, right: 8, bottom: 8),
|
||||
padding: const EdgeInsets.only(left: 8, right: 8, bottom: 8),
|
||||
shrinkWrap: true,
|
||||
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
|
||||
maxCrossAxisExtent: 48,
|
||||
@ -505,8 +467,7 @@ class _StickerPicker extends StatelessWidget {
|
||||
richMessage: TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text:
|
||||
':${element.pack.prefix}${element.alias}:\n',
|
||||
text: ':${element.pack.prefix}${element.alias}:\n',
|
||||
style: GoogleFonts.robotoMono()),
|
||||
TextSpan(text: element.name).bold(),
|
||||
],
|
||||
@ -515,15 +476,11 @@ class _StickerPicker extends StatelessWidget {
|
||||
width: 48,
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(8)),
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.surfaceContainerHigh,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(8)),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
child: UniversalImage(
|
||||
sn.getAttachmentUrl(element.attachment.rid),
|
||||
width: 48,
|
||||
|
@ -464,14 +464,9 @@ class _PostMediaPendingItem extends StatelessWidget {
|
||||
}
|
||||
|
||||
class AddPostMediaButton extends StatelessWidget {
|
||||
final VisualDensity? visualDensity;
|
||||
final Function(Iterable<PostWriteMedia>) onAdd;
|
||||
|
||||
const AddPostMediaButton({
|
||||
super.key,
|
||||
required this.onAdd,
|
||||
this.visualDensity,
|
||||
});
|
||||
const AddPostMediaButton({super.key, required this.onAdd});
|
||||
|
||||
void _takeMedia(bool isVideo) async {
|
||||
final picker = ImagePicker();
|
||||
@ -555,7 +550,11 @@ class AddPostMediaButton extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return PopupMenuButton(
|
||||
style: ButtonStyle(visualDensity: visualDensity),
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(),
|
||||
style: ButtonStyle(
|
||||
visualDensity: VisualDensity(horizontal: -4, vertical: -4),
|
||||
),
|
||||
icon: Icon(
|
||||
Symbols.add_photo_alternate,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
|
@ -7,8 +7,6 @@ PODS:
|
||||
- FlutterMacOS
|
||||
- device_info_plus (0.0.1):
|
||||
- FlutterMacOS
|
||||
- fast_rsa (0.6.0):
|
||||
- FlutterMacOS
|
||||
- file_picker (0.0.1):
|
||||
- FlutterMacOS
|
||||
- file_saver (0.0.1):
|
||||
@ -25,14 +23,14 @@ PODS:
|
||||
- Firebase/Messaging (11.8.0):
|
||||
- Firebase/CoreOnly
|
||||
- FirebaseMessaging (~> 11.8.0)
|
||||
- firebase_analytics (11.4.4):
|
||||
- firebase_analytics (11.4.3):
|
||||
- Firebase/Analytics (= 11.8.0)
|
||||
- firebase_core
|
||||
- FlutterMacOS
|
||||
- firebase_core (3.12.1):
|
||||
- firebase_core (3.12.0):
|
||||
- Firebase/CoreOnly (~> 11.8.0)
|
||||
- FlutterMacOS
|
||||
- firebase_messaging (15.2.4):
|
||||
- firebase_messaging (15.2.3):
|
||||
- Firebase/CoreOnly (~> 11.8.0)
|
||||
- Firebase/Messaging (~> 11.8.0)
|
||||
- firebase_core
|
||||
@ -78,8 +76,6 @@ PODS:
|
||||
- flutter_inappwebview_macos (0.0.1):
|
||||
- FlutterMacOS
|
||||
- OrderedSet (~> 6.0.3)
|
||||
- flutter_timezone (0.1.0):
|
||||
- FlutterMacOS
|
||||
- flutter_udid (0.0.1):
|
||||
- FlutterMacOS
|
||||
- SAMKeychain
|
||||
@ -90,8 +86,6 @@ PODS:
|
||||
- gal (1.0.0):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- geolocator_apple (1.2.0):
|
||||
- FlutterMacOS
|
||||
- GoogleAppMeasurement (11.8.0):
|
||||
- GoogleAppMeasurement/AdIdSupport (= 11.8.0)
|
||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
||||
@ -219,7 +213,6 @@ DEPENDENCIES:
|
||||
- connectivity_plus (from `Flutter/ephemeral/.symlinks/plugins/connectivity_plus/macos`)
|
||||
- croppy (from `Flutter/ephemeral/.symlinks/plugins/croppy/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_saver (from `Flutter/ephemeral/.symlinks/plugins/file_saver/macos`)
|
||||
- file_selector_macos (from `Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos`)
|
||||
@ -227,12 +220,10 @@ DEPENDENCIES:
|
||||
- firebase_core (from `Flutter/ephemeral/.symlinks/plugins/firebase_core/macos`)
|
||||
- firebase_messaging (from `Flutter/ephemeral/.symlinks/plugins/firebase_messaging/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_webrtc (from `Flutter/ephemeral/.symlinks/plugins/flutter_webrtc/macos`)
|
||||
- FlutterMacOS (from `Flutter/ephemeral`)
|
||||
- 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`)
|
||||
- in_app_review (from `Flutter/ephemeral/.symlinks/plugins/in_app_review/macos`)
|
||||
- livekit_client (from `Flutter/ephemeral/.symlinks/plugins/livekit_client/macos`)
|
||||
@ -281,8 +272,6 @@ EXTERNAL SOURCES:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/croppy/macos
|
||||
device_info_plus:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos
|
||||
fast_rsa:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/fast_rsa/macos
|
||||
file_picker:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/file_picker/macos
|
||||
file_saver:
|
||||
@ -297,8 +286,6 @@ EXTERNAL SOURCES:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/firebase_messaging/macos
|
||||
flutter_inappwebview_macos:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/flutter_inappwebview_macos/macos
|
||||
flutter_timezone:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/flutter_timezone/macos
|
||||
flutter_udid:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/flutter_udid/macos
|
||||
flutter_webrtc:
|
||||
@ -307,8 +294,6 @@ EXTERNAL SOURCES:
|
||||
:path: Flutter/ephemeral
|
||||
gal:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/gal/darwin
|
||||
geolocator_apple:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/geolocator_apple/macos
|
||||
hotkey_manager_macos:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/hotkey_manager_macos/macos
|
||||
in_app_review:
|
||||
@ -353,26 +338,23 @@ SPEC CHECKSUMS:
|
||||
connectivity_plus: 0a976dfd033b59192912fa3c6c7b54aab5093802
|
||||
croppy: 25a638bd7d05411d8c697f481568f261037694fc
|
||||
device_info_plus: 1b14eed9bf95428983aed283a8d51cce3d8c4215
|
||||
fast_rsa: 47a50bec1042c8c01726007dc0590a078418f997
|
||||
file_picker: e716a70a9fe5fd9e09ebc922d7541464289443af
|
||||
file_saver: 44e6fbf666677faf097302460e214e977fdd977b
|
||||
file_selector_macos: cc3858c981fe6889f364731200d6232dac1d812d
|
||||
Firebase: d80354ed7f6df5f9aca55e9eb47cc4b634735eaf
|
||||
firebase_analytics: 75b9d9ea8b21ce77898a3a46910e2051e93db8e1
|
||||
firebase_core: 1b573eac37729348cdc472516991dd7e269ae37e
|
||||
firebase_messaging: 0620038ea399ceae2218c9087fca00a28f576209
|
||||
firebase_analytics: 1a71372a9735d7046d2c69db848a8d178f9fb587
|
||||
firebase_core: 68e1d27035b096239f147a041643e14e156f1481
|
||||
firebase_messaging: 89b5e0e28413dd878a58d2f286cdc03887b5d467
|
||||
FirebaseAnalytics: 4fd42def128146e24e480e89f310e3d8534ea42b
|
||||
FirebaseCore: 99fe0c4b44a39f37d99e6404e02009d2db5d718d
|
||||
FirebaseCoreInternal: df24ce5af28864660ecbd13596fc8dd3a8c34629
|
||||
FirebaseInstallations: 6c963bd2a86aca0481eef4f48f5a4df783ae5917
|
||||
FirebaseMessaging: 487b634ccdf6f7b7ff180fdcb2a9935490f764e8
|
||||
flutter_inappwebview_macos: bdf207b8f4ebd58e86ae06cd96b147de99a67c9b
|
||||
flutter_timezone: 62400baa441155f2a4144188648f2ff861649747
|
||||
flutter_udid: 2e7b3da4b5fdfba86a396b97898f5fe8f4ec1a52
|
||||
flutter_webrtc: d55fd3f5c75b42940b6b4b2cf376a5797398d1f8
|
||||
FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
|
||||
gal: 6a522c75909f1244732d4596d11d6a2f86ff37a5
|
||||
geolocator_apple: 72a78ae3f3e4ec0db62117bd93e34523f5011d58
|
||||
GoogleAppMeasurement: fc0817122bd4d4189164f85374e06773b9561896
|
||||
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
|
||||
GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d
|
||||
|
@ -481,7 +481,7 @@ class SnLocalKeyPair extends Table
|
||||
String get actualTableName => $name;
|
||||
static const String $name = 'sn_local_key_pair';
|
||||
@override
|
||||
Set<GeneratedColumn> get $primaryKey => {id};
|
||||
Set<GeneratedColumn> get $primaryKey => const {};
|
||||
@override
|
||||
SnLocalKeyPairData map(Map<String, dynamic> data, {String? tablePrefix}) {
|
||||
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
|
||||
|
Loading…
x
Reference in New Issue
Block a user