Send message with poll

This commit is contained in:
2025-11-16 22:43:18 +08:00
parent e43bc6b8a8
commit 99fb08dd55
5 changed files with 136 additions and 5 deletions

View File

@@ -46,6 +46,18 @@ sealed class SnPoll with _$SnPoll {
}) = _SnPoll; }) = _SnPoll;
factory SnPoll.fromJson(Map<String, dynamic> json) => _$SnPollFromJson(json); factory SnPoll.fromJson(Map<String, dynamic> 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 @freezed

View File

@@ -8,6 +8,7 @@ import "package:island/database/drift_db.dart";
import "package:island/database/message.dart"; import "package:island/database/message.dart";
import "package:island/models/chat.dart"; import "package:island/models/chat.dart";
import "package:island/models/file.dart"; import "package:island/models/file.dart";
import "package:island/models/poll.dart";
import "package:island/pods/database.dart"; import "package:island/pods/database.dart";
import "package:island/pods/lifecycle.dart"; import "package:island/pods/lifecycle.dart";
import "package:island/pods/network.dart"; import "package:island/pods/network.dart";
@@ -437,6 +438,7 @@ class MessagesNotifier extends _$MessagesNotifier {
WidgetRef ref, WidgetRef ref,
String content, String content,
List<UniversalFile> attachments, { List<UniversalFile> attachments, {
SnPoll? poll,
SnChatMessage? editingTo, SnChatMessage? editingTo,
SnChatMessage? forwardingTo, SnChatMessage? forwardingTo,
SnChatMessage? replyingTo, SnChatMessage? replyingTo,
@@ -498,6 +500,7 @@ class MessagesNotifier extends _$MessagesNotifier {
'attachments_id': cloudAttachments.map((e) => e.id).toList(), 'attachments_id': cloudAttachments.map((e) => e.id).toList(),
'replied_message_id': replyingTo?.id, 'replied_message_id': replyingTo?.id,
'forwarded_message_id': forwardingTo?.id, 'forwarded_message_id': forwardingTo?.id,
'poll_id': poll?.id,
'meta': {}, 'meta': {},
'nonce': nonce, 'nonce': nonce,
}, },

View File

@@ -11,6 +11,7 @@ import "package:hooks_riverpod/hooks_riverpod.dart";
import "package:island/database/message.dart"; import "package:island/database/message.dart";
import "package:island/models/chat.dart"; import "package:island/models/chat.dart";
import "package:island/models/file.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_rooms.dart";
import "package:island/pods/chat/chat_subscribe.dart"; import "package:island/pods/chat/chat_subscribe.dart";
import "package:island/pods/chat/messages_notifier.dart"; import "package:island/pods/chat/messages_notifier.dart";
@@ -170,6 +171,7 @@ class ChatRoomScreen extends HookConsumerWidget {
final messageReplyingTo = useState<SnChatMessage?>(null); final messageReplyingTo = useState<SnChatMessage?>(null);
final messageForwardingTo = useState<SnChatMessage?>(null); final messageForwardingTo = useState<SnChatMessage?>(null);
final messageEditingTo = useState<SnChatMessage?>(null); final messageEditingTo = useState<SnChatMessage?>(null);
final selectedPoll = useState<SnPoll?>(null);
final attachments = useState<List<UniversalFile>>([]); final attachments = useState<List<UniversalFile>>([]);
final attachmentProgress = useState<Map<String, Map<int, double?>>>({}); final attachmentProgress = useState<Map<String, Map<int, double?>>>({});
@@ -285,11 +287,13 @@ class ChatRoomScreen extends HookConsumerWidget {
void sendMessage() { void sendMessage() {
if (messageController.text.trim().isNotEmpty || if (messageController.text.trim().isNotEmpty ||
attachments.value.isNotEmpty) { attachments.value.isNotEmpty ||
selectedPoll.value != null) {
messagesNotifier.sendMessage( messagesNotifier.sendMessage(
ref, ref,
messageController.text.trim(), messageController.text.trim(),
attachments.value, attachments.value,
poll: selectedPoll.value,
editingTo: messageEditingTo.value, editingTo: messageEditingTo.value,
forwardingTo: messageForwardingTo.value, forwardingTo: messageForwardingTo.value,
replyingTo: messageReplyingTo.value, replyingTo: messageReplyingTo.value,
@@ -304,6 +308,7 @@ class ChatRoomScreen extends HookConsumerWidget {
messageEditingTo.value = null; messageEditingTo.value = null;
messageReplyingTo.value = null; messageReplyingTo.value = null;
messageForwardingTo.value = null; messageForwardingTo.value = null;
selectedPoll.value = null;
attachments.value = []; attachments.value = [];
} }
} }
@@ -1246,10 +1251,13 @@ class ChatRoomScreen extends HookConsumerWidget {
messageEditingTo.value = null; messageEditingTo.value = null;
messageReplyingTo.value = null; messageReplyingTo.value = null;
messageForwardingTo.value = null; messageForwardingTo.value = null;
selectedPoll.value = null;
}, },
messageEditingTo: messageEditingTo.value, messageEditingTo: messageEditingTo.value,
messageReplyingTo: messageReplyingTo.value, messageReplyingTo: messageReplyingTo.value,
messageForwardingTo: messageForwardingTo.value, messageForwardingTo: messageForwardingTo.value,
selectedPoll: selectedPoll.value,
onPollSelected: (poll) => selectedPoll.value = poll,
onPickFile: (bool isPhoto) { onPickFile: (bool isPhoto) {
if (isPhoto) { if (isPhoto) {
pickPhotoMedia(); pickPhotoMedia();

View File

@@ -11,6 +11,7 @@ import "package:island/models/account.dart";
import "package:island/models/autocomplete_response.dart"; import "package:island/models/autocomplete_response.dart";
import "package:island/models/chat.dart"; import "package:island/models/chat.dart";
import "package:island/models/file.dart"; import "package:island/models/file.dart";
import "package:island/models/poll.dart";
import "package:island/models/publisher.dart"; import "package:island/models/publisher.dart";
import "package:island/models/realm.dart"; import "package:island/models/realm.dart";
import "package:island/models/sticker.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:material_symbols_icons/symbols.dart";
import "package:island/widgets/stickers/sticker_picker.dart"; import "package:island/widgets/stickers/sticker_picker.dart";
import "package:island/pods/chat/chat_subscribe.dart"; import "package:island/pods/chat/chat_subscribe.dart";
import "package:island/widgets/post/compose_poll.dart";
void _insertPlaceholder(TextEditingController controller, String placeholder) { void _insertPlaceholder(TextEditingController controller, String placeholder) {
final text = controller.text; final text = controller.text;
@@ -43,8 +45,14 @@ const kInputDrawerExpandedHeight = 180.0;
class _ExpandedSection extends StatelessWidget { class _ExpandedSection extends StatelessWidget {
final TextEditingController messageController; 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -90,7 +98,16 @@ class _ExpandedSection extends StatelessWidget {
borderRadius: const BorderRadius.all( borderRadius: const BorderRadius.all(
Radius.circular(8), Radius.circular(8),
), ),
onTap: () {}, onTap: () async {
final poll = await showModalBottomSheet<SnPoll>(
context: context,
isScrollControlled: true,
builder: (context) => const ComposePollSheet(),
);
if (poll != null) {
onPollSelected(poll);
}
},
child: Card( child: Card(
margin: EdgeInsets.zero, margin: EdgeInsets.zero,
color: color:
@@ -176,6 +193,8 @@ class ChatInput extends HookConsumerWidget {
final Function(int, int) onMoveAttachment; final Function(int, int) onMoveAttachment;
final Function(List<UniversalFile>) onAttachmentsChanged; final Function(List<UniversalFile>) onAttachmentsChanged;
final Map<String, Map<int, double?>> attachmentProgress; final Map<String, Map<int, double?>> attachmentProgress;
final SnPoll? selectedPoll;
final Function(SnPoll?) onPollSelected;
const ChatInput({ const ChatInput({
super.key, super.key,
@@ -196,6 +215,8 @@ class ChatInput extends HookConsumerWidget {
required this.onMoveAttachment, required this.onMoveAttachment,
required this.onAttachmentsChanged, required this.onAttachmentsChanged,
required this.attachmentProgress, required this.attachmentProgress,
this.selectedPoll,
required this.onPollSelected,
}); });
@override @override
@@ -413,6 +434,87 @@ class ChatInput extends HookConsumerWidget {
key: ValueKey('no-attachments'), key: ValueKey('no-attachments'),
), ),
), ),
AnimatedSwitcher(
duration: const Duration(milliseconds: 200),
switchInCurve: Curves.easeOutCubic,
switchOutCurve: Curves.easeInCubic,
transitionBuilder: (Widget child, Animation<double> animation) {
return SlideTransition(
position: Tween<Offset>(
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( AnimatedSwitcher(
duration: const Duration(milliseconds: 200), duration: const Duration(milliseconds: 200),
switchInCurve: Curves.easeOutCubic, switchInCurve: Curves.easeOutCubic,
@@ -798,7 +900,11 @@ class ChatInput extends HookConsumerWidget {
}, },
child: child:
isExpanded.value isExpanded.value
? _ExpandedSection(messageController: messageController) ? _ExpandedSection(
messageController: messageController,
selectedPoll: selectedPoll,
onPollSelected: onPollSelected,
)
: const SizedBox.shrink(key: ValueKey('collapsed')), : const SizedBox.shrink(key: ValueKey('collapsed')),
), ),
], ],

View File

@@ -67,7 +67,9 @@ class ComposePollSheet extends HookConsumerWidget {
title: Text(poll.title ?? 'untitled'.tr()), title: Text(poll.title ?? 'untitled'.tr()),
subtitle: _buildPollSubtitle(poll), subtitle: _buildPollSubtitle(poll),
onTap: () { onTap: () {
Navigator.of(context).pop(poll); Navigator.of(
context,
).pop(SnPoll.fromPollWithStats(poll));
}, },
); );
}, },