From 99fb08dd55dc936c18db79bae7fc62b8d03c4eac Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Sun, 16 Nov 2025 22:43:18 +0800 Subject: [PATCH] :sparkles: Send message with poll --- lib/models/poll.dart | 12 +++ lib/pods/chat/messages_notifier.dart | 3 + lib/screens/chat/room.dart | 10 ++- lib/widgets/chat/chat_input.dart | 112 ++++++++++++++++++++++++++- lib/widgets/post/compose_poll.dart | 4 +- 5 files changed, 136 insertions(+), 5 deletions(-) diff --git a/lib/models/poll.dart b/lib/models/poll.dart index 5bf0643f..8f11ef2e 100644 --- a/lib/models/poll.dart +++ b/lib/models/poll.dart @@ -46,6 +46,18 @@ sealed class SnPoll with _$SnPoll { }) = _SnPoll; factory SnPoll.fromJson(Map json) => _$SnPollFromJson(json); + + factory SnPoll.fromPollWithStats(SnPollWithStats pollWithStats) => SnPoll( + id: pollWithStats.id, + questions: pollWithStats.questions, + title: pollWithStats.title, + description: pollWithStats.description, + endedAt: pollWithStats.endedAt, + publisherId: pollWithStats.publisherId, + createdAt: pollWithStats.createdAt, + updatedAt: pollWithStats.updatedAt, + deletedAt: pollWithStats.deletedAt, + ); } @freezed diff --git a/lib/pods/chat/messages_notifier.dart b/lib/pods/chat/messages_notifier.dart index f5f59477..ddf09e22 100644 --- a/lib/pods/chat/messages_notifier.dart +++ b/lib/pods/chat/messages_notifier.dart @@ -8,6 +8,7 @@ import "package:island/database/drift_db.dart"; import "package:island/database/message.dart"; import "package:island/models/chat.dart"; import "package:island/models/file.dart"; +import "package:island/models/poll.dart"; import "package:island/pods/database.dart"; import "package:island/pods/lifecycle.dart"; import "package:island/pods/network.dart"; @@ -437,6 +438,7 @@ class MessagesNotifier extends _$MessagesNotifier { WidgetRef ref, String content, List attachments, { + SnPoll? poll, SnChatMessage? editingTo, SnChatMessage? forwardingTo, SnChatMessage? replyingTo, @@ -498,6 +500,7 @@ class MessagesNotifier extends _$MessagesNotifier { 'attachments_id': cloudAttachments.map((e) => e.id).toList(), 'replied_message_id': replyingTo?.id, 'forwarded_message_id': forwardingTo?.id, + 'poll_id': poll?.id, 'meta': {}, 'nonce': nonce, }, diff --git a/lib/screens/chat/room.dart b/lib/screens/chat/room.dart index a774af60..45d7007c 100644 --- a/lib/screens/chat/room.dart +++ b/lib/screens/chat/room.dart @@ -11,6 +11,7 @@ import "package:hooks_riverpod/hooks_riverpod.dart"; import "package:island/database/message.dart"; import "package:island/models/chat.dart"; import "package:island/models/file.dart"; +import "package:island/models/poll.dart"; import "package:island/pods/chat/chat_rooms.dart"; import "package:island/pods/chat/chat_subscribe.dart"; import "package:island/pods/chat/messages_notifier.dart"; @@ -170,6 +171,7 @@ class ChatRoomScreen extends HookConsumerWidget { final messageReplyingTo = useState(null); final messageForwardingTo = useState(null); final messageEditingTo = useState(null); + final selectedPoll = useState(null); final attachments = useState>([]); final attachmentProgress = useState>>({}); @@ -285,11 +287,13 @@ class ChatRoomScreen extends HookConsumerWidget { void sendMessage() { if (messageController.text.trim().isNotEmpty || - attachments.value.isNotEmpty) { + attachments.value.isNotEmpty || + selectedPoll.value != null) { messagesNotifier.sendMessage( ref, messageController.text.trim(), attachments.value, + poll: selectedPoll.value, editingTo: messageEditingTo.value, forwardingTo: messageForwardingTo.value, replyingTo: messageReplyingTo.value, @@ -304,6 +308,7 @@ class ChatRoomScreen extends HookConsumerWidget { messageEditingTo.value = null; messageReplyingTo.value = null; messageForwardingTo.value = null; + selectedPoll.value = null; attachments.value = []; } } @@ -1246,10 +1251,13 @@ class ChatRoomScreen extends HookConsumerWidget { messageEditingTo.value = null; messageReplyingTo.value = null; messageForwardingTo.value = null; + selectedPoll.value = null; }, messageEditingTo: messageEditingTo.value, messageReplyingTo: messageReplyingTo.value, messageForwardingTo: messageForwardingTo.value, + selectedPoll: selectedPoll.value, + onPollSelected: (poll) => selectedPoll.value = poll, onPickFile: (bool isPhoto) { if (isPhoto) { pickPhotoMedia(); diff --git a/lib/widgets/chat/chat_input.dart b/lib/widgets/chat/chat_input.dart index 3a43c655..26c6f0f8 100644 --- a/lib/widgets/chat/chat_input.dart +++ b/lib/widgets/chat/chat_input.dart @@ -11,6 +11,7 @@ import "package:island/models/account.dart"; import "package:island/models/autocomplete_response.dart"; import "package:island/models/chat.dart"; import "package:island/models/file.dart"; +import "package:island/models/poll.dart"; import "package:island/models/publisher.dart"; import "package:island/models/realm.dart"; import "package:island/models/sticker.dart"; @@ -26,6 +27,7 @@ import "package:styled_widget/styled_widget.dart"; import "package:material_symbols_icons/symbols.dart"; import "package:island/widgets/stickers/sticker_picker.dart"; import "package:island/pods/chat/chat_subscribe.dart"; +import "package:island/widgets/post/compose_poll.dart"; void _insertPlaceholder(TextEditingController controller, String placeholder) { final text = controller.text; @@ -43,8 +45,14 @@ const kInputDrawerExpandedHeight = 180.0; class _ExpandedSection extends StatelessWidget { final TextEditingController messageController; + final SnPoll? selectedPoll; + final Function(SnPoll?) onPollSelected; - const _ExpandedSection({required this.messageController}); + const _ExpandedSection({ + required this.messageController, + this.selectedPoll, + required this.onPollSelected, + }); @override Widget build(BuildContext context) { @@ -90,7 +98,16 @@ class _ExpandedSection extends StatelessWidget { borderRadius: const BorderRadius.all( Radius.circular(8), ), - onTap: () {}, + onTap: () async { + final poll = await showModalBottomSheet( + context: context, + isScrollControlled: true, + builder: (context) => const ComposePollSheet(), + ); + if (poll != null) { + onPollSelected(poll); + } + }, child: Card( margin: EdgeInsets.zero, color: @@ -176,6 +193,8 @@ class ChatInput extends HookConsumerWidget { final Function(int, int) onMoveAttachment; final Function(List) onAttachmentsChanged; final Map> attachmentProgress; + final SnPoll? selectedPoll; + final Function(SnPoll?) onPollSelected; const ChatInput({ super.key, @@ -196,6 +215,8 @@ class ChatInput extends HookConsumerWidget { required this.onMoveAttachment, required this.onAttachmentsChanged, required this.attachmentProgress, + this.selectedPoll, + required this.onPollSelected, }); @override @@ -413,6 +434,87 @@ class ChatInput extends HookConsumerWidget { key: ValueKey('no-attachments'), ), ), + AnimatedSwitcher( + duration: const Duration(milliseconds: 200), + switchInCurve: Curves.easeOutCubic, + switchOutCurve: Curves.easeInCubic, + transitionBuilder: (Widget child, Animation animation) { + return SlideTransition( + position: Tween( + begin: const Offset(0, -0.25), + end: Offset.zero, + ).animate(animation), + child: FadeTransition( + opacity: animation, + child: SizeTransition( + sizeFactor: animation, + axisAlignment: -1.0, + child: child, + ), + ), + ); + }, + child: + selectedPoll != null + ? Container( + key: const ValueKey('selected-poll'), + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + decoration: BoxDecoration( + color: + Theme.of( + context, + ).colorScheme.surfaceContainerHigh, + borderRadius: BorderRadius.circular(24), + border: Border.all( + color: Theme.of( + context, + ).colorScheme.outline.withOpacity(0.2), + width: 1, + ), + ), + margin: const EdgeInsets.only( + left: 8, + right: 8, + top: 8, + bottom: 8, + ), + child: Row( + children: [ + Icon( + Symbols.how_to_vote, + size: 18, + color: Theme.of(context).colorScheme.primary, + ), + const Gap(8), + Expanded( + child: Text( + selectedPoll!.title ?? 'Poll', + style: Theme.of(context).textTheme.bodySmall! + .copyWith(fontWeight: FontWeight.w500), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + SizedBox( + width: 24, + height: 24, + child: IconButton( + padding: EdgeInsets.zero, + icon: const Icon(Icons.close, size: 18), + onPressed: () => onPollSelected(null), + tooltip: 'clear'.tr(), + ), + ), + ], + ), + ) + : const SizedBox.shrink( + key: ValueKey('no-selected-poll'), + ), + ), AnimatedSwitcher( duration: const Duration(milliseconds: 200), switchInCurve: Curves.easeOutCubic, @@ -798,7 +900,11 @@ class ChatInput extends HookConsumerWidget { }, child: isExpanded.value - ? _ExpandedSection(messageController: messageController) + ? _ExpandedSection( + messageController: messageController, + selectedPoll: selectedPoll, + onPollSelected: onPollSelected, + ) : const SizedBox.shrink(key: ValueKey('collapsed')), ), ], diff --git a/lib/widgets/post/compose_poll.dart b/lib/widgets/post/compose_poll.dart index 6950cff8..c5ed2cc2 100644 --- a/lib/widgets/post/compose_poll.dart +++ b/lib/widgets/post/compose_poll.dart @@ -67,7 +67,9 @@ class ComposePollSheet extends HookConsumerWidget { title: Text(poll.title ?? 'untitled'.tr()), subtitle: _buildPollSubtitle(poll), onTap: () { - Navigator.of(context).pop(poll); + Navigator.of( + context, + ).pop(SnPoll.fromPollWithStats(poll)); }, ); },