diff --git a/lib/api/marianumcloud/talk/getPoll/getPollState.dart b/lib/api/marianumcloud/talk/getPoll/getPollState.dart new file mode 100644 index 0000000..503c1d0 --- /dev/null +++ b/lib/api/marianumcloud/talk/getPoll/getPollState.dart @@ -0,0 +1,18 @@ +import 'dart:convert'; + +import 'package:http/http.dart' as http; + +import '../talkApi.dart'; +import 'getPollStateResponse.dart'; + +class GetPollState extends TalkApi { + String token; + int pollId; + GetPollState({required this.token, required this.pollId}) : super('v1/poll/$token/$pollId', null); + + @override + GetPollStateResponse assemble(String raw) => GetPollStateResponse.fromJson(jsonDecode(raw)['ocs']); + + @override + Future request(Uri uri, Object? body, Map? headers) => http.get(uri, headers: headers); +} diff --git a/lib/api/marianumcloud/talk/getPoll/getPollStateResponse.dart b/lib/api/marianumcloud/talk/getPoll/getPollStateResponse.dart new file mode 100644 index 0000000..75d20c0 --- /dev/null +++ b/lib/api/marianumcloud/talk/getPoll/getPollStateResponse.dart @@ -0,0 +1,50 @@ +import 'package:json_annotation/json_annotation.dart'; + +import '../../../apiResponse.dart'; + +part 'getPollStateResponse.g.dart'; + +@JsonSerializable(explicitToJson: true) +class GetPollStateResponse extends ApiResponse { + GetPollStateResponseObject data; + + GetPollStateResponse(this.data); + + factory GetPollStateResponse.fromJson(Map json) => _$GetPollStateResponseFromJson(json); + Map toJson() => _$GetPollStateResponseToJson(this); +} + +@JsonSerializable(explicitToJson: true) +class GetPollStateResponseObject { + int id; + String question; + List options; + dynamic votes; + String actorType; + String actorId; + String actorDisplayName; + int status; + int resultMode; + int maxVotes; + List votedSelf; + int? numVoters; + List? details; + + GetPollStateResponseObject( + this.id, + this.question, + this.options, + this.votes, + this.actorType, + this.actorId, + this.actorDisplayName, + this.status, + this.resultMode, + this.maxVotes, + this.votedSelf, + this.numVoters, + this.details); + + factory GetPollStateResponseObject.fromJson(Map json) => _$GetPollStateResponseObjectFromJson(json); + Map toJson() => _$GetPollStateResponseObjectToJson(this); +} diff --git a/lib/api/marianumcloud/talk/getPoll/getPollStateResponse.g.dart b/lib/api/marianumcloud/talk/getPoll/getPollStateResponse.g.dart new file mode 100644 index 0000000..3015979 --- /dev/null +++ b/lib/api/marianumcloud/talk/getPoll/getPollStateResponse.g.dart @@ -0,0 +1,62 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'getPollStateResponse.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +GetPollStateResponse _$GetPollStateResponseFromJson( + Map json, +) => + GetPollStateResponse( + GetPollStateResponseObject.fromJson( + json['data'] as Map, + ), + ) + ..headers = (json['headers'] as Map?)?.map( + (k, e) => MapEntry(k, e as String), + ); + +Map _$GetPollStateResponseToJson( + GetPollStateResponse instance, +) => { + 'headers': ?instance.headers, + 'data': instance.data.toJson(), +}; + +GetPollStateResponseObject _$GetPollStateResponseObjectFromJson( + Map json, +) => GetPollStateResponseObject( + (json['id'] as num).toInt(), + json['question'] as String, + (json['options'] as List).map((e) => e as String).toList(), + json['votes'], + json['actorType'] as String, + json['actorId'] as String, + json['actorDisplayName'] as String, + (json['status'] as num).toInt(), + (json['resultMode'] as num).toInt(), + (json['maxVotes'] as num).toInt(), + (json['votedSelf'] as List).map((e) => (e as num).toInt()).toList(), + (json['numVoters'] as num?)?.toInt(), + json['details'] as List?, +); + +Map _$GetPollStateResponseObjectToJson( + GetPollStateResponseObject instance, +) => { + 'id': instance.id, + 'question': instance.question, + 'options': instance.options, + 'votes': instance.votes, + 'actorType': instance.actorType, + 'actorId': instance.actorId, + 'actorDisplayName': instance.actorDisplayName, + 'status': instance.status, + 'resultMode': instance.resultMode, + 'maxVotes': instance.maxVotes, + 'votedSelf': instance.votedSelf, + 'numVoters': instance.numVoters, + 'details': instance.details, +}; diff --git a/lib/api/marianumcloud/talk/votePoll/votePoll.dart b/lib/api/marianumcloud/talk/votePoll/votePoll.dart new file mode 100644 index 0000000..7cedfe1 --- /dev/null +++ b/lib/api/marianumcloud/talk/votePoll/votePoll.dart @@ -0,0 +1,26 @@ +import 'dart:convert'; + +import 'package:http/http.dart' as http; +import 'package:http/http.dart'; + +import '../getPoll/getPollStateResponse.dart'; +import '../talkApi.dart'; +import 'votePollParams.dart'; + +@Deprecated('VotePoll is broken') +class VotePoll extends TalkApi { + String token; + int pollId; + VotePoll({required this.token, required this.pollId, required VotePollParams params}) : super('v1/poll/$token/$pollId', params); + + @override + GetPollStateResponse assemble(String raw) => GetPollStateResponse.fromJson(jsonDecode(raw)['ocs']); + + @override + Future? request(Uri uri, Object? body, Map? headers) { + if(body is VotePollParams) { + return http.post(uri, headers: headers, body: body.toJson().toString()); + } + return null; + } +} diff --git a/lib/api/marianumcloud/talk/votePoll/votePollParams.dart b/lib/api/marianumcloud/talk/votePoll/votePollParams.dart new file mode 100644 index 0000000..151459d --- /dev/null +++ b/lib/api/marianumcloud/talk/votePoll/votePollParams.dart @@ -0,0 +1,15 @@ +import 'package:json_annotation/json_annotation.dart'; + +import '../../../apiParams.dart'; + +part 'votePollParams.g.dart'; + +@JsonSerializable() +@Deprecated('VotePoll is broken') +class VotePollParams extends ApiParams { + List optionIds; + + VotePollParams({required this.optionIds}); + factory VotePollParams.fromJson(Map json) => _$VotePollParamsFromJson(json); + Map toJson() => _$VotePollParamsToJson(this); +} diff --git a/lib/api/marianumcloud/talk/votePoll/votePollParams.g.dart b/lib/api/marianumcloud/talk/votePoll/votePollParams.g.dart new file mode 100644 index 0000000..5b43858 --- /dev/null +++ b/lib/api/marianumcloud/talk/votePoll/votePollParams.g.dart @@ -0,0 +1,17 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'votePollParams.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +VotePollParams _$VotePollParamsFromJson(Map json) => + VotePollParams( + optionIds: (json['optionIds'] as List) + .map((e) => (e as num).toInt()) + .toList(), + ); + +Map _$VotePollParamsToJson(VotePollParams instance) => + {'optionIds': instance.optionIds}; diff --git a/lib/utils/UrlOpener.dart b/lib/utils/UrlOpener.dart new file mode 100644 index 0000000..450ed94 --- /dev/null +++ b/lib/utils/UrlOpener.dart @@ -0,0 +1,10 @@ +import 'package:flutter_linkify/flutter_linkify.dart'; +import 'package:url_launcher/url_launcher_string.dart'; + +class UrlOpener { + static Future onOpen(LinkableElement link) async { + if(await canLaunchUrlString(link.url)) { + await launchUrlString(link.url); + } + } +} diff --git a/lib/view/pages/talk/chatView.dart b/lib/view/pages/talk/chatView.dart index 34577bb..4d6ca48 100644 --- a/lib/view/pages/talk/chatView.dart +++ b/lib/view/pages/talk/chatView.dart @@ -51,6 +51,7 @@ class _ChatViewState extends State { var elementDate = DateTime.fromMillisecondsSinceEpoch(element.timestamp * 1000); if(element.systemMessage.contains('reaction')) return; + if(element.systemMessage.contains('poll_voted')) return; var commonRead = int.parse(data.getChatResponse.headers?['x-chat-last-common-read'] ?? '0'); if(!elementDate.isSameDay(lastDate)) { @@ -128,7 +129,7 @@ class _ChatViewState extends State { ), ), Container( - color: Theme.of(context).colorScheme.background, + color: Theme.of(context).colorScheme.surface, child: TalkNavigator.isSecondaryVisible(context) ? ChatTextfield(widget.room.token, selfId: widget.selfId) : SafeArea(child: ChatTextfield(widget.room.token, selfId: widget.selfId) diff --git a/lib/view/pages/talk/components/chatBubble.dart b/lib/view/pages/talk/components/chatBubble.dart index fdf5239..08b9866 100644 --- a/lib/view/pages/talk/components/chatBubble.dart +++ b/lib/view/pages/talk/components/chatBubble.dart @@ -6,6 +6,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:jiffy/jiffy.dart'; import 'package:open_filex/open_filex.dart'; +import '../../../../api/marianumcloud/talk/getPoll/getPollState.dart'; import '../../../../extensions/text.dart'; import 'package:provider/provider.dart'; @@ -18,11 +19,13 @@ import '../../../../api/marianumcloud/talk/reactMessage/reactMessageParams.dart' import '../../../../api/marianumcloud/talk/room/getRoomResponse.dart'; import '../../../../model/chatList/chatProps.dart'; import '../../../../widget/debug/debugTile.dart'; +import '../../../../widget/loadingSpinner.dart'; import '../../files/fileElement.dart'; import 'answerReference.dart'; import 'chatBubbleStyles.dart'; import 'chatMessage.dart'; import '../messageReactions.dart'; +import 'pollOptionsList.dart'; class ChatBubble extends StatefulWidget { final BuildContext context; @@ -297,6 +300,37 @@ class _ChatBubbleState extends State with SingleTickerProviderStateM onLongPress: showOptionsDialog, onDoubleTap: showOptionsDialog, onTap: () { + if(message.originalData?['object']?.type == RichObjectStringObjectType.talkPoll) { + var pollId = int.parse(message.originalData!['object']!.id); + var pollState = GetPollState(token: widget.bubbleData.token, pollId: pollId).run(); + showDialog(context: context, builder: (context) => AlertDialog( + title: Text(message.originalData!['object']!.name, overflow: TextOverflow.ellipsis), + content: FutureBuilder( + future: pollState, + builder: (context, snapshot) { + if(snapshot.connectionState == ConnectionState.waiting) return const Column(mainAxisSize: MainAxisSize.min, children: [LoadingSpinner()]); + + var pollData = snapshot.data!.data; + return ListView( + shrinkWrap: true, + children: [ + PollOptionsList( + pollData: pollData, + chatToken: widget.chatData.token + ) + ] + ); + } + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('Zurück') + ), + ], + )); + } + if(message.file == null) return; if(downloadProgress > 0) { diff --git a/lib/view/pages/talk/components/chatMessage.dart b/lib/view/pages/talk/components/chatMessage.dart index 90abb14..468805f 100644 --- a/lib/view/pages/talk/components/chatMessage.dart +++ b/lib/view/pages/talk/components/chatMessage.dart @@ -2,12 +2,12 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter_linkify/flutter_linkify.dart'; -import 'package:url_launcher/url_launcher_string.dart'; import '../../../../api/marianumcloud/talk/chat/getChatResponse.dart'; import '../../../../api/marianumcloud/talk/chat/richObjectStringProcessor.dart'; import '../../../../model/accountData.dart'; import '../../../../model/endpointData.dart'; +import '../../../../utils/UrlOpener.dart'; class ChatMessage { String originalMessage; @@ -29,9 +29,17 @@ class ChatMessage { var contentWidget = Linkify( text: content, - onOpen: onOpen, + onOpen: UrlOpener.onOpen, ); + if(originalData?['object']?.type == RichObjectStringObjectType.talkPoll) { + return ListTile( + leading: const Icon(Icons.poll_outlined), + title: Text(originalData!['object']!.name), + contentPadding: const EdgeInsets.only(left: 10), + ); + } + if(file == null) return contentWidget; return Padding( @@ -65,10 +73,4 @@ class ChatMessage { ) ); } - - Future onOpen(LinkableElement link) async { - if(await canLaunchUrlString(link.url)) { - await launchUrlString(link.url); - } - } } diff --git a/lib/view/pages/talk/components/pollOptionsList.dart b/lib/view/pages/talk/components/pollOptionsList.dart new file mode 100644 index 0000000..3e4eec4 --- /dev/null +++ b/lib/view/pages/talk/components/pollOptionsList.dart @@ -0,0 +1,67 @@ + +import 'package:flutter/material.dart'; +import 'package:flutter_linkify/flutter_linkify.dart'; + +import '../../../../api/marianumcloud/talk/getPoll/getPollStateResponse.dart'; +import '../../../../utils/UrlOpener.dart'; + +class PollOptionsList extends StatefulWidget { + final GetPollStateResponseObject pollData; + final String chatToken; + const PollOptionsList({super.key, required this.pollData, required this.chatToken}); + + @override + State createState() => _PollOptionsListState(); +} + +class _PollOptionsListState extends State { + @override + Widget build(BuildContext context) => Column( + children: [ + ...widget.pollData.options.map((option) { + var optionId = widget.pollData.options.indexOf(option); + var votedSelf = widget.pollData.votedSelf.contains(optionId); + var portionsVisible = widget.pollData.votes is Map; + var votes = portionsVisible + ? (widget.pollData.votes['option-$optionId'] as num?) ?? 0 + : 0; + var numVoters = widget.pollData.numVoters ?? 0; + double portion = numVoters == 0 ? 0 : (votes / numVoters); + + return ListTile( + enabled: false, + isThreeLine: portionsVisible, + dense: true, + title: Text( + option, + style: Theme.of(context).textTheme.bodyLarge, + ), + leading: Icon( + votedSelf ? Icons.check_circle_outlined : Icons.circle_outlined, + color: votedSelf + ? Theme.of(context).colorScheme.primary.withValues(alpha: 0.6) + : Theme.of(context).colorScheme.onSurfaceVariant.withValues(alpha: 0.6), + ), + subtitle: portionsVisible ? Row( + children: [ + Expanded( + child: LinearProgressIndicator(value: portion.clamp(0.0, 1.0)), + ), + Container( + margin: const EdgeInsets.only(left: 10), + child: Text('${(portion * 100).round()}%'), + ), + ], + ) : null, + ); + }), + ListTile( + title: Linkify( + text: 'Wenn du abstimmen möchtest, verwende die Webversion unter https://cloud.marianum-fulda.de/call/${widget.chatToken}', + onOpen: UrlOpener.onOpen, + style: Theme.of(context).textTheme.bodySmall, + ), + ) + ], + ); +}