✨ Sending encrypted message
This commit is contained in:
parent
cb2de52bee
commit
5a6b841253
@ -9,6 +9,7 @@ import 'package:provider/provider.dart';
|
|||||||
import 'package:surface/database/database.dart';
|
import 'package:surface/database/database.dart';
|
||||||
import 'package:surface/logger.dart';
|
import 'package:surface/logger.dart';
|
||||||
import 'package:surface/providers/database.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_attachment.dart';
|
||||||
import 'package:surface/providers/sn_network.dart';
|
import 'package:surface/providers/sn_network.dart';
|
||||||
import 'package:surface/providers/user_directory.dart';
|
import 'package:surface/providers/user_directory.dart';
|
||||||
@ -25,6 +26,7 @@ class ChatMessageController extends ChangeNotifier {
|
|||||||
late final WebSocketProvider _ws;
|
late final WebSocketProvider _ws;
|
||||||
late final SnAttachmentProvider _attach;
|
late final SnAttachmentProvider _attach;
|
||||||
late final DatabaseProvider _dt;
|
late final DatabaseProvider _dt;
|
||||||
|
late final KeyPairProvider _kp;
|
||||||
|
|
||||||
StreamSubscription? _wsSubscription;
|
StreamSubscription? _wsSubscription;
|
||||||
|
|
||||||
@ -34,6 +36,7 @@ class ChatMessageController extends ChangeNotifier {
|
|||||||
_ws = context.read<WebSocketProvider>();
|
_ws = context.read<WebSocketProvider>();
|
||||||
_attach = context.read<SnAttachmentProvider>();
|
_attach = context.read<SnAttachmentProvider>();
|
||||||
_dt = context.read<DatabaseProvider>();
|
_dt = context.read<DatabaseProvider>();
|
||||||
|
_kp = context.read<KeyPairProvider>();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool isPending = true;
|
bool isPending = true;
|
||||||
@ -245,6 +248,24 @@ 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(
|
Future<void> sendMessage(
|
||||||
String type,
|
String type,
|
||||||
String content, {
|
String content, {
|
||||||
@ -252,13 +273,13 @@ class ChatMessageController extends ChangeNotifier {
|
|||||||
int? relatedId,
|
int? relatedId,
|
||||||
List<String>? attachments,
|
List<String>? attachments,
|
||||||
SnChatMessage? editingMessage,
|
SnChatMessage? editingMessage,
|
||||||
|
bool isEncrypted = false,
|
||||||
}) async {
|
}) async {
|
||||||
if (channel == null) return;
|
if (channel == null) return;
|
||||||
const uuid = Uuid();
|
const uuid = Uuid();
|
||||||
final nonce = uuid.v4();
|
final nonce = uuid.v4();
|
||||||
final body = {
|
final body = {
|
||||||
'text': content,
|
...(await _encodeMessageBody(content, isEncrypted)),
|
||||||
'algorithm': 'plain',
|
|
||||||
if (quoteId != null) 'quote_event': quoteId,
|
if (quoteId != null) 'quote_event': quoteId,
|
||||||
if (relatedId != null) 'related_event': relatedId,
|
if (relatedId != null) 'related_event': relatedId,
|
||||||
if (attachments != null && attachments.isNotEmpty)
|
if (attachments != null && attachments.isNotEmpty)
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
import 'dart:developer';
|
import 'dart:developer';
|
||||||
|
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
@ -19,6 +20,7 @@ import 'package:surface/providers/user_directory.dart';
|
|||||||
import 'package:surface/providers/userinfo.dart';
|
import 'package:surface/providers/userinfo.dart';
|
||||||
import 'package:surface/providers/websocket.dart';
|
import 'package:surface/providers/websocket.dart';
|
||||||
import 'package:surface/types/chat.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/call/call_prejoin.dart';
|
||||||
import 'package:surface/widgets/chat/chat_message.dart';
|
import 'package:surface/widgets/chat/chat_message.dart';
|
||||||
import 'package:surface/widgets/chat/chat_message_input.dart';
|
import 'package:surface/widgets/chat/chat_message_input.dart';
|
||||||
@ -58,6 +60,8 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
|
|||||||
final GlobalKey<ChatMessageInputState> _inputGlobalKey = GlobalKey();
|
final GlobalKey<ChatMessageInputState> _inputGlobalKey = GlobalKey();
|
||||||
late final ChatMessageController _messageController;
|
late final ChatMessageController _messageController;
|
||||||
|
|
||||||
|
bool _isEncrypted = false;
|
||||||
|
|
||||||
StreamSubscription? _wsSubscription;
|
StreamSubscription? _wsSubscription;
|
||||||
|
|
||||||
// TODO fetch user identity and ask them to join the channel or not
|
// TODO fetch user identity and ask them to join the channel or not
|
||||||
@ -91,9 +95,14 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
|
|||||||
nty.skippableNotifyChannel = _channel!.id;
|
nty.skippableNotifyChannel = _channel!.id;
|
||||||
final ws = context.read<WebSocketProvider>();
|
final ws = context.read<WebSocketProvider>();
|
||||||
if (_channel != null) {
|
if (_channel != null) {
|
||||||
ws.conn?.sink.add({
|
ws.conn?.sink.add(
|
||||||
'channel_id': _channel?.id,
|
jsonEncode(WebSocketPackage(
|
||||||
});
|
method: 'events.subscribe',
|
||||||
|
endpoint: 'im',
|
||||||
|
payload: {
|
||||||
|
'channel_id': _channel!.id,
|
||||||
|
})),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
@ -247,9 +256,15 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
|
|||||||
nty.skippableNotifyChannel = null;
|
nty.skippableNotifyChannel = null;
|
||||||
final ws = context.read<WebSocketProvider>();
|
final ws = context.read<WebSocketProvider>();
|
||||||
if (_channel != null) {
|
if (_channel != null) {
|
||||||
ws.conn?.sink.add({
|
ws.conn?.sink.add(
|
||||||
'channel_id': _channel?.id,
|
jsonEncode(WebSocketPackage(
|
||||||
});
|
method: 'events.unsubscribe',
|
||||||
|
endpoint: 'im',
|
||||||
|
payload: {
|
||||||
|
'channel_id': _channel!.id,
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
@ -268,6 +283,15 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
|
|||||||
: _channel?.name ?? 'loading'.tr(),
|
: _channel?.name ?? 'loading'.tr(),
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
setState(() => _isEncrypted = !_isEncrypted);
|
||||||
|
_inputGlobalKey.currentState?.setEncrypted(_isEncrypted);
|
||||||
|
},
|
||||||
|
icon: _isEncrypted
|
||||||
|
? const Icon(Symbols.lock)
|
||||||
|
: const Icon(Symbols.no_encryption),
|
||||||
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: _ongoingCall == null
|
icon: _ongoingCall == null
|
||||||
? const Icon(Symbols.call)
|
? const Icon(Symbols.call)
|
||||||
|
@ -28,7 +28,8 @@ class ChatMessageInput extends StatefulWidget {
|
|||||||
final ChatMessageController controller;
|
final ChatMessageController controller;
|
||||||
final SnChannelMember? otherMember;
|
final SnChannelMember? otherMember;
|
||||||
|
|
||||||
const ChatMessageInput({super.key, required this.controller, this.otherMember});
|
const ChatMessageInput(
|
||||||
|
{super.key, required this.controller, this.otherMember});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<ChatMessageInput> createState() => ChatMessageInputState();
|
State<ChatMessageInput> createState() => ChatMessageInputState();
|
||||||
@ -38,6 +39,8 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
|||||||
bool _isBusy = false;
|
bool _isBusy = false;
|
||||||
double? _progress;
|
double? _progress;
|
||||||
|
|
||||||
|
bool _isEncrypted = false;
|
||||||
|
|
||||||
SnChatMessage? _replyingMessage, _editingMessage;
|
SnChatMessage? _replyingMessage, _editingMessage;
|
||||||
|
|
||||||
final TextEditingController _contentController = TextEditingController();
|
final TextEditingController _contentController = TextEditingController();
|
||||||
@ -45,12 +48,20 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
|||||||
|
|
||||||
final HotKey _pasteHotKey = HotKey(
|
final HotKey _pasteHotKey = HotKey(
|
||||||
key: PhysicalKeyboardKey.keyV,
|
key: PhysicalKeyboardKey.keyV,
|
||||||
modifiers: [(!kIsWeb && Platform.isMacOS) ? HotKeyModifier.meta : HotKeyModifier.control],
|
modifiers: [
|
||||||
|
(!kIsWeb && Platform.isMacOS)
|
||||||
|
? HotKeyModifier.meta
|
||||||
|
: HotKeyModifier.control
|
||||||
|
],
|
||||||
scope: HotKeyScope.inapp,
|
scope: HotKeyScope.inapp,
|
||||||
);
|
);
|
||||||
final HotKey _newLineHotKey = HotKey(
|
final HotKey _newLineHotKey = HotKey(
|
||||||
key: PhysicalKeyboardKey.enter,
|
key: PhysicalKeyboardKey.enter,
|
||||||
modifiers: [(!kIsWeb && Platform.isMacOS) ? HotKeyModifier.meta : HotKeyModifier.control],
|
modifiers: [
|
||||||
|
(!kIsWeb && Platform.isMacOS)
|
||||||
|
? HotKeyModifier.meta
|
||||||
|
: HotKeyModifier.control
|
||||||
|
],
|
||||||
scope: HotKeyScope.inapp,
|
scope: HotKeyScope.inapp,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -83,6 +94,10 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void setEncrypted(bool value) {
|
||||||
|
setState(() => _isEncrypted = value);
|
||||||
|
}
|
||||||
|
|
||||||
void setReply(SnChatMessage? value) {
|
void setReply(SnChatMessage? value) {
|
||||||
setState(() => _replyingMessage = value);
|
setState(() => _replyingMessage = value);
|
||||||
}
|
}
|
||||||
@ -100,7 +115,8 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
|||||||
void setEdit(SnChatMessage? value) {
|
void setEdit(SnChatMessage? value) {
|
||||||
_contentController.text = value?.body['text'] ?? '';
|
_contentController.text = value?.body['text'] ?? '';
|
||||||
_attachments.clear();
|
_attachments.clear();
|
||||||
_attachments.addAll(value?.preload?.attachments?.map((e) => PostWriteMedia(e)) ?? []);
|
_attachments.addAll(
|
||||||
|
value?.preload?.attachments?.map((e) => PostWriteMedia(e)) ?? []);
|
||||||
setState(() => _editingMessage = value);
|
setState(() => _editingMessage = value);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -139,7 +155,9 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
|||||||
media.name,
|
media.name,
|
||||||
'messaging',
|
'messaging',
|
||||||
null,
|
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(
|
final item = await attach.chunkedUploadParts(
|
||||||
@ -171,10 +189,14 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
|||||||
widget.controller.sendMessage(
|
widget.controller.sendMessage(
|
||||||
_editingMessage != null ? 'messages.edit' : 'messages.new',
|
_editingMessage != null ? 'messages.edit' : 'messages.new',
|
||||||
_contentController.text,
|
_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,
|
relatedId: _editingMessage?.id,
|
||||||
quoteId: _replyingMessage?.id,
|
quoteId: _replyingMessage?.id,
|
||||||
editingMessage: _editingMessage,
|
editingMessage: _editingMessage,
|
||||||
|
isEncrypted: _isEncrypted,
|
||||||
);
|
);
|
||||||
_contentController.clear();
|
_contentController.clear();
|
||||||
_attachments.clear();
|
_attachments.clear();
|
||||||
@ -232,12 +254,15 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
|||||||
TweenAnimationBuilder<double>(
|
TweenAnimationBuilder<double>(
|
||||||
tween: Tween(begin: 0, end: _progress),
|
tween: Tween(begin: 0, end: _progress),
|
||||||
duration: Duration(milliseconds: 300),
|
duration: Duration(milliseconds: 300),
|
||||||
builder: (context, value, _) => LinearProgressIndicator(value: value, minHeight: 2),
|
builder: (context, value, _) =>
|
||||||
|
LinearProgressIndicator(value: value, minHeight: 2),
|
||||||
)
|
)
|
||||||
else if (_isBusy)
|
else if (_isBusy)
|
||||||
const LinearProgressIndicator(value: null, minHeight: 2),
|
const LinearProgressIndicator(value: null, minHeight: 2),
|
||||||
Padding(
|
Padding(
|
||||||
padding: _attachments.isNotEmpty ? const EdgeInsets.only(top: 8) : EdgeInsets.zero,
|
padding: _attachments.isNotEmpty
|
||||||
|
? const EdgeInsets.only(top: 8)
|
||||||
|
: EdgeInsets.zero,
|
||||||
child: PostMediaPendingList(
|
child: PostMediaPendingList(
|
||||||
attachments: _attachments,
|
attachments: _attachments,
|
||||||
isBusy: _isBusy,
|
isBusy: _isBusy,
|
||||||
@ -249,9 +274,8 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
|||||||
},
|
},
|
||||||
onUpdateBusy: (state) => setState(() => _isBusy = state),
|
onUpdateBusy: (state) => setState(() => _isBusy = state),
|
||||||
),
|
),
|
||||||
)
|
).height(_attachments.isNotEmpty ? 80 + 8 : 0, animate: true).animate(
|
||||||
.height(_attachments.isNotEmpty ? 80 + 8 : 0, animate: true)
|
const Duration(milliseconds: 300), Curves.fastEaseInToSlowEaseOut),
|
||||||
.animate(const Duration(milliseconds: 300), Curves.fastEaseInToSlowEaseOut),
|
|
||||||
SingleChildScrollView(
|
SingleChildScrollView(
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
child: _replyingMessage != null
|
child: _replyingMessage != null
|
||||||
@ -272,7 +296,8 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
|||||||
const Gap(8),
|
const Gap(8),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
_replyingMessage?.body['text'] ?? '${_replyingMessage?.sender.nick}',
|
_replyingMessage?.body['text'] ??
|
||||||
|
'${_replyingMessage?.sender.nick}',
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
@ -289,9 +314,8 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
|||||||
).padding(vertical: 8),
|
).padding(vertical: 8),
|
||||||
)
|
)
|
||||||
: const SizedBox.shrink(),
|
: const SizedBox.shrink(),
|
||||||
)
|
).height(_replyingMessage != null ? 38 : 0, animate: true).animate(
|
||||||
.height(_replyingMessage != null ? 38 : 0, animate: true)
|
const Duration(milliseconds: 300), Curves.fastEaseInToSlowEaseOut),
|
||||||
.animate(const Duration(milliseconds: 300), Curves.fastEaseInToSlowEaseOut),
|
|
||||||
SingleChildScrollView(
|
SingleChildScrollView(
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
child: _editingMessage != null
|
child: _editingMessage != null
|
||||||
@ -312,7 +336,8 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
|||||||
const Gap(8),
|
const Gap(8),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
_editingMessage?.body['text'] ?? '${_editingMessage?.sender.nick}',
|
_editingMessage?.body['text'] ??
|
||||||
|
'${_editingMessage?.sender.nick}',
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
@ -330,30 +355,41 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
|||||||
).padding(vertical: 8),
|
).padding(vertical: 8),
|
||||||
)
|
)
|
||||||
: const SizedBox.shrink(),
|
: const SizedBox.shrink(),
|
||||||
)
|
).height(_editingMessage != null ? 38 : 0, animate: true).animate(
|
||||||
.height(_editingMessage != null ? 38 : 0, animate: true)
|
const Duration(milliseconds: 300), Curves.fastEaseInToSlowEaseOut),
|
||||||
.animate(const Duration(milliseconds: 300), Curves.fastEaseInToSlowEaseOut),
|
|
||||||
Container(
|
Container(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 16),
|
padding: EdgeInsets.only(
|
||||||
constraints: BoxConstraints(minHeight: 56, maxHeight: 240),
|
left: 16,
|
||||||
|
right: 16,
|
||||||
|
top: 6,
|
||||||
|
),
|
||||||
|
constraints: BoxConstraints(maxHeight: 240),
|
||||||
child: Row(
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: TextField(
|
child: TextField(
|
||||||
focusNode: _focusNode,
|
focusNode: _focusNode,
|
||||||
controller: _contentController,
|
controller: _contentController,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
isCollapsed: true,
|
prefixIconConstraints: const BoxConstraints(
|
||||||
|
minWidth: 24,
|
||||||
|
maxWidth: 24,
|
||||||
|
),
|
||||||
|
prefixIcon:
|
||||||
|
_isEncrypted ? Icon(Symbols.lock, size: 18) : null,
|
||||||
hintText: widget.otherMember != null
|
hintText: widget.otherMember != null
|
||||||
? 'fieldChatMessageDirect'.tr(args: [
|
? 'fieldChatMessageDirect'.tr(args: [
|
||||||
'@${ud.getAccountFromCache(widget.otherMember?.accountId)?.name}',
|
'@${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,
|
border: InputBorder.none,
|
||||||
),
|
),
|
||||||
textInputAction: TextInputAction.send,
|
textInputAction: TextInputAction.send,
|
||||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
onTapOutside: (_) =>
|
||||||
|
FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
onSubmitted: (_) {
|
onSubmitted: (_) {
|
||||||
if (_isBusy) return;
|
if (_isBusy) return;
|
||||||
_sendMessage();
|
_sendMessage();
|
||||||
@ -362,20 +398,18 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
|||||||
maxLines: null,
|
maxLines: null,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const Gap(8),
|
|
||||||
IconButton(
|
IconButton(
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
Symbols.mood,
|
Symbols.mood,
|
||||||
color: Theme.of(context).colorScheme.primary,
|
color: Theme.of(context).colorScheme.primary,
|
||||||
),
|
),
|
||||||
visualDensity: const VisualDensity(horizontal: -4, vertical: -4),
|
|
||||||
padding: EdgeInsets.zero,
|
|
||||||
constraints: const BoxConstraints(),
|
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
_showEmojiPicker(context);
|
_showEmojiPicker(context);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
AddPostMediaButton(
|
AddPostMediaButton(
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
onAdd: (items) {
|
onAdd: (items) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_attachments.addAll(items);
|
_attachments.addAll(items);
|
||||||
@ -383,18 +417,18 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
onPressed: _isBusy ? null : _sendMessage,
|
onPressed: _isBusy ? null : _sendMessage,
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
Symbols.send,
|
Symbols.send,
|
||||||
color: Theme.of(context).colorScheme.primary,
|
color: Theme.of(context).colorScheme.primary,
|
||||||
),
|
),
|
||||||
visualDensity: const VisualDensity(horizontal: -4, vertical: -4),
|
|
||||||
padding: EdgeInsets.zero,
|
|
||||||
constraints: const BoxConstraints(),
|
constraints: const BoxConstraints(),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
Gap(MediaQuery.of(context).padding.bottom),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -405,7 +439,8 @@ class _StickerPicker extends StatelessWidget {
|
|||||||
final Function? onDismiss;
|
final Function? onDismiss;
|
||||||
final Function(String)? onInsert;
|
final Function(String)? onInsert;
|
||||||
|
|
||||||
const _StickerPicker({this.onDismiss, required this.originalText, this.onInsert});
|
const _StickerPicker(
|
||||||
|
{this.onDismiss, required this.originalText, this.onInsert});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -431,8 +466,10 @@ class _StickerPicker extends StatelessWidget {
|
|||||||
return <Widget>[
|
return <Widget>[
|
||||||
Container(
|
Container(
|
||||||
margin: EdgeInsets.only(bottom: 8),
|
margin: EdgeInsets.only(bottom: 8),
|
||||||
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
padding:
|
||||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||||
|
color:
|
||||||
|
Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
@ -444,7 +481,8 @@ class _StickerPicker extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
GridView.builder(
|
GridView.builder(
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
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,
|
shrinkWrap: true,
|
||||||
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
|
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
|
||||||
maxCrossAxisExtent: 48,
|
maxCrossAxisExtent: 48,
|
||||||
@ -467,7 +505,8 @@ class _StickerPicker extends StatelessWidget {
|
|||||||
richMessage: TextSpan(
|
richMessage: TextSpan(
|
||||||
children: [
|
children: [
|
||||||
TextSpan(
|
TextSpan(
|
||||||
text: ':${element.pack.prefix}${element.alias}:\n',
|
text:
|
||||||
|
':${element.pack.prefix}${element.alias}:\n',
|
||||||
style: GoogleFonts.robotoMono()),
|
style: GoogleFonts.robotoMono()),
|
||||||
TextSpan(text: element.name).bold(),
|
TextSpan(text: element.name).bold(),
|
||||||
],
|
],
|
||||||
@ -476,11 +515,15 @@ class _StickerPicker extends StatelessWidget {
|
|||||||
width: 48,
|
width: 48,
|
||||||
height: 48,
|
height: 48,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
borderRadius: const BorderRadius.all(
|
||||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
Radius.circular(8)),
|
||||||
|
color: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.surfaceContainerHigh,
|
||||||
),
|
),
|
||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
borderRadius: const BorderRadius.all(
|
||||||
|
Radius.circular(8)),
|
||||||
child: UniversalImage(
|
child: UniversalImage(
|
||||||
sn.getAttachmentUrl(element.attachment.rid),
|
sn.getAttachmentUrl(element.attachment.rid),
|
||||||
width: 48,
|
width: 48,
|
||||||
|
@ -464,9 +464,14 @@ class _PostMediaPendingItem extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class AddPostMediaButton extends StatelessWidget {
|
class AddPostMediaButton extends StatelessWidget {
|
||||||
|
final VisualDensity? visualDensity;
|
||||||
final Function(Iterable<PostWriteMedia>) onAdd;
|
final Function(Iterable<PostWriteMedia>) onAdd;
|
||||||
|
|
||||||
const AddPostMediaButton({super.key, required this.onAdd});
|
const AddPostMediaButton({
|
||||||
|
super.key,
|
||||||
|
required this.onAdd,
|
||||||
|
this.visualDensity,
|
||||||
|
});
|
||||||
|
|
||||||
void _takeMedia(bool isVideo) async {
|
void _takeMedia(bool isVideo) async {
|
||||||
final picker = ImagePicker();
|
final picker = ImagePicker();
|
||||||
@ -550,11 +555,7 @@ class AddPostMediaButton extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return PopupMenuButton(
|
return PopupMenuButton(
|
||||||
padding: EdgeInsets.zero,
|
style: ButtonStyle(visualDensity: visualDensity),
|
||||||
constraints: const BoxConstraints(),
|
|
||||||
style: ButtonStyle(
|
|
||||||
visualDensity: VisualDensity(horizontal: -4, vertical: -4),
|
|
||||||
),
|
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
Symbols.add_photo_alternate,
|
Symbols.add_photo_alternate,
|
||||||
color: Theme.of(context).colorScheme.primary,
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user