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;
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

View File

@@ -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<UniversalFile> 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,
},

View File

@@ -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<SnChatMessage?>(null);
final messageForwardingTo = useState<SnChatMessage?>(null);
final messageEditingTo = useState<SnChatMessage?>(null);
final selectedPoll = useState<SnPoll?>(null);
final attachments = useState<List<UniversalFile>>([]);
final attachmentProgress = useState<Map<String, Map<int, double?>>>({});
@@ -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();

View File

@@ -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<SnPoll>(
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<UniversalFile>) onAttachmentsChanged;
final Map<String, Map<int, double?>> 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<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(
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')),
),
],

View File

@@ -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));
},
);
},