implemented full interactive poll support in Talk, including creation, voting, and closing functionality

This commit is contained in:
2026-07-12 17:54:30 +02:00
parent 3f44e9302f
commit 44e45c9b78
15 changed files with 946 additions and 87 deletions
@@ -1,44 +1,112 @@
import 'package:flutter/material.dart';
import '../../../../api/errors/error_mapper.dart';
import '../../../../api/marianumcloud/talk/get_poll/get_poll_state.dart';
import '../../../../api/marianumcloud/talk/get_poll/get_poll_state_response.dart';
import '../../../../api/marianumcloud/talk/room/get_room_response.dart';
import '../../../../widget/details_bottom_sheet.dart';
import '../../../../widget/loading_spinner.dart';
import 'poll_options_list.dart';
/// Opens the poll dialog that lets a user vote on a Talk poll attached to
/// a message. Loads the poll state lazily and renders the option list.
void showChatBubblePollDialog(
/// Opens the poll bottom sheet for a Talk poll attached to a message. Loads the
/// poll state lazily; voting and closing reload the state in place and refresh
/// the chat so the surrounding message list stays in sync.
void showChatBubblePollSheet(
BuildContext context, {
required String chatToken,
required String messageToken,
required GetRoomResponseObject room,
required int pollId,
required String pollName,
required void Function({bool renew}) refetch,
}) {
final pollState = GetPollState(token: messageToken, pollId: pollId).run();
showDialog<void>(
context: context,
builder: (dialogCtx) => AlertDialog(
showDetailsBottomSheet(
context,
header: ListTile(
leading: const Icon(Icons.poll_outlined),
title: Text(pollName, overflow: TextOverflow.ellipsis),
content: FutureBuilder(
future: pollState,
builder: (_, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Column(
mainAxisSize: MainAxisSize.min,
children: [LoadingSpinner()],
);
}
final pollData = snapshot.data!.data;
return SingleChildScrollView(
child: PollOptionsList(pollData: pollData, chatToken: chatToken),
);
},
),
actions: [
TextButton(
onPressed: () => Navigator.of(dialogCtx).pop(),
child: const Text('Zurück'),
),
],
subtitle: const Text('Umfrage'),
),
children: (sheetCtx) => [
const SizedBox(height: 8),
_PollSheetBody(room: room, pollId: pollId, refetch: refetch),
const SizedBox(height: 16),
],
);
}
class _PollSheetBody extends StatefulWidget {
final GetRoomResponseObject room;
final int pollId;
final void Function({bool renew}) refetch;
const _PollSheetBody({
required this.room,
required this.pollId,
required this.refetch,
});
@override
State<_PollSheetBody> createState() => _PollSheetBodyState();
}
class _PollSheetBodyState extends State<_PollSheetBody> {
Future<GetPollStateResponse>? _future;
@override
void initState() {
super.initState();
_future = GetPollState(token: widget.room.token, pollId: widget.pollId).run();
}
void _reload() {
setState(() {
_future = GetPollState(
token: widget.room.token,
pollId: widget.pollId,
).run();
});
}
void _applyState(GetPollStateResponse state) {
setState(() {
// Block body: an arrow would return the Future, which setState rejects.
_future = Future.value(state);
});
widget.refetch(renew: true);
}
@override
Widget build(BuildContext context) => FutureBuilder<GetPollStateResponse>(
future: _future,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Padding(
padding: EdgeInsets.all(24),
child: LoadingSpinner(),
);
}
if (snapshot.hasError) {
return Padding(
padding: const EdgeInsets.all(24),
child: Column(
children: [
Text(
errorToUserMessage(snapshot.error),
textAlign: TextAlign.center,
),
const SizedBox(height: 12),
FilledButton.tonal(
onPressed: _reload,
child: const Text('Erneut versuchen'),
),
],
),
);
}
return PollOptionsList(
pollData: snapshot.data!.data,
room: widget.room,
onStateChanged: _applyState,
);
},
);
}