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
@@ -0,0 +1,28 @@
import 'dart:convert';
import 'package:http/http.dart' as http;
import '../../../api_params.dart';
import '../get_poll/get_poll_state_response.dart';
import '../talk_api.dart';
/// Schließt eine Umfrage endgültig — nur Ersteller oder Moderatoren.
class ClosePoll extends TalkApi<GetPollStateResponse> {
ClosePoll({required String token, required int pollId})
: super('v1/poll/$token/$pollId', null);
@override
GetPollStateResponse assemble(String raw) {
final decoded = jsonDecode(raw) as Map<String, dynamic>;
return GetPollStateResponse.fromJson(
decoded['ocs'] as Map<String, dynamic>,
);
}
@override
Future<http.Response> request(
Uri uri,
ApiParams? body,
Map<String, String>? headers,
) => http.delete(uri, headers: headers);
}
@@ -0,0 +1,31 @@
import 'dart:convert';
import 'package:http/http.dart' as http;
import '../../../api_params.dart';
import '../talk_api.dart';
import 'create_poll_params.dart';
/// Erstellt eine Umfrage; der Server postet die Poll-Nachricht selbst in den
/// Chat, danach genügt ein Chat-Refresh. Nur in Gruppen-Chats erlaubt.
class CreatePoll extends TalkApi {
CreatePoll({required String token, required CreatePollParams params})
: super(
'v1/poll/$token',
params,
headers: {'Content-Type': 'application/json'},
);
@override
Null assemble(String raw) => null;
@override
Future<http.Response>? request(
Uri uri,
ApiParams? body,
Map<String, String>? headers,
) {
if (body is! CreatePollParams) return null;
return http.post(uri, headers: headers, body: jsonEncode(body.toJson()));
}
}
@@ -0,0 +1,27 @@
import 'package:json_annotation/json_annotation.dart';
import '../../../api_params.dart';
part 'create_poll_params.g.dart';
@JsonSerializable()
class CreatePollParams extends ApiParams {
String question;
List<String> options;
/// 0 = Ergebnisse öffentlich, 1 = bis zum Schließen verborgen.
int resultMode;
/// Stimmen pro Teilnehmer; 0 = unbegrenzt.
int maxVotes;
CreatePollParams({
required this.question,
required this.options,
required this.resultMode,
required this.maxVotes,
});
factory CreatePollParams.fromJson(Map<String, dynamic> json) =>
_$CreatePollParamsFromJson(json);
Map<String, dynamic> toJson() => _$CreatePollParamsToJson(this);
}
@@ -0,0 +1,25 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'create_poll_params.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
CreatePollParams _$CreatePollParamsFromJson(Map<String, dynamic> json) =>
CreatePollParams(
question: json['question'] as String,
options: (json['options'] as List<dynamic>)
.map((e) => e as String)
.toList(),
resultMode: (json['resultMode'] as num).toInt(),
maxVotes: (json['maxVotes'] as num).toInt(),
);
Map<String, dynamic> _$CreatePollParamsToJson(CreatePollParams instance) =>
<String, dynamic>{
'question': instance.question,
'options': instance.options,
'resultMode': instance.resultMode,
'maxVotes': instance.maxVotes,
};
@@ -4,6 +4,18 @@ import '../../../api_response.dart';
part 'get_poll_state_response.g.dart';
/// Poll-`status`-Werte der Talk-API.
const int pollStatusOpen = 0;
const int pollStatusClosed = 1;
/// Poll-`resultMode`-Werte der Talk-API.
const int pollResultModePublic = 0;
const int pollResultModeHidden = 1;
/// `participantType`-Werte (aus dem Room), die eine Umfrage schließen dürfen:
/// Owner (1), Moderator (2) und Gast-Moderator (6).
const Set<int> pollModeratorParticipantTypes = {1, 2, 6};
@JsonSerializable(explicitToJson: true)
class GetPollStateResponse extends ApiResponse {
GetPollStateResponseObject data;
@@ -50,4 +62,32 @@ class GetPollStateResponseObject {
factory GetPollStateResponseObject.fromJson(Map<String, dynamic> json) =>
_$GetPollStateResponseObjectFromJson(json);
Map<String, dynamic> toJson() => _$GetPollStateResponseObjectToJson(this);
bool get isClosed => status == pollStatusClosed;
bool get resultsHidden => resultMode == pollResultModeHidden;
/// Ergebnisse sichtbar: öffentliche Umfragen jederzeit, verborgene erst nach
/// dem Schließen. Der Typ von `votes` taugt nicht als Signal (siehe unten).
bool get resultsVisible => resultMode == pollResultModePublic || isClosed;
/// Normalisiert das dynamische `votes`-Feld zu einer Map: der Server liefert
/// bei verborgenen Ergebnissen (und ohne Stimmen) eine leere Liste statt Map.
Map<String, num> get voteCounts {
final raw = votes;
if (raw is! Map) return const {};
final result = <String, num>{};
raw.forEach((key, value) {
if (key is String && value is num) result[key] = value;
});
return result;
}
/// Darf der Nutzer die (offene) Umfrage schließen: als Ersteller oder Moderator.
bool canClose({required String selfId, required int participantType}) {
if (isClosed) return false;
final isCreator = actorType == 'users' && actorId == selfId;
final isModerator = pollModeratorParticipantTypes.contains(participantType);
return isCreator || isModerator;
}
}
@@ -0,0 +1,41 @@
import 'dart:convert';
import 'package:http/http.dart' as http;
import '../../../api_params.dart';
import '../get_poll/get_poll_state_response.dart';
import '../talk_api.dart';
import 'vote_poll_params.dart';
class VotePoll extends TalkApi<GetPollStateResponse> {
// Body als echtes JSON (nicht form-encoded wie die anderen Endpunkte): nur
// so kommt das int-Array an; sonst liest der Server optionIds als [] und
// löscht die eigene Stimme (Ursache des Readonly-Fallbacks, Issue #42).
VotePoll({
required String token,
required int pollId,
required VotePollParams params,
}) : super(
'v1/poll/$token/$pollId',
params,
headers: {'Content-Type': 'application/json'},
);
@override
GetPollStateResponse assemble(String raw) {
final decoded = jsonDecode(raw) as Map<String, dynamic>;
return GetPollStateResponse.fromJson(
decoded['ocs'] as Map<String, dynamic>,
);
}
@override
Future<http.Response>? request(
Uri uri,
ApiParams? body,
Map<String, String>? headers,
) {
if (body is! VotePollParams) return null;
return http.post(uri, headers: headers, body: jsonEncode(body.toJson()));
}
}
@@ -0,0 +1,16 @@
import 'package:json_annotation/json_annotation.dart';
import '../../../api_params.dart';
part 'vote_poll_params.g.dart';
@JsonSerializable()
class VotePollParams extends ApiParams {
/// Indizes der gewählten Optionen; leer = eigene Stimme zurückziehen.
List<int> optionIds;
VotePollParams({required this.optionIds});
factory VotePollParams.fromJson(Map<String, dynamic> json) =>
_$VotePollParamsFromJson(json);
Map<String, dynamic> toJson() => _$VotePollParamsToJson(this);
}
@@ -0,0 +1,17 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'vote_poll_params.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
VotePollParams _$VotePollParamsFromJson(Map<String, dynamic> json) =>
VotePollParams(
optionIds: (json['optionIds'] as List<dynamic>)
.map((e) => (e as num).toInt())
.toList(),
);
Map<String, dynamic> _$VotePollParamsToJson(VotePollParams instance) =>
<String, dynamic>{'optionIds': instance.optionIds};