diff --git a/lib/api/marianumcloud/talk/chat/getChatResponse.dart b/lib/api/marianumcloud/talk/chat/getChatResponse.dart index b8f9bcf..04955f7 100644 --- a/lib/api/marianumcloud/talk/chat/getChatResponse.dart +++ b/lib/api/marianumcloud/talk/chat/getChatResponse.dart @@ -1,3 +1,4 @@ +import 'package:jiffy/jiffy.dart'; import 'package:json_annotation/json_annotation.dart'; import '../../../apiResponse.dart'; @@ -34,6 +35,8 @@ class GetChatResponseObject { bool isReplyable; String referenceId; String message; + Map? reactions; + List? reactionsSelf; @JsonKey(fromJson: _fromJson) Map? messageParameters; GetChatResponseObject( @@ -48,11 +51,35 @@ class GetChatResponseObject { this.isReplyable, this.referenceId, this.message, - this.messageParameters); + this.messageParameters, + this.reactions, + this.reactionsSelf + ); factory GetChatResponseObject.fromJson(Map json) => _$GetChatResponseObjectFromJson(json); Map toJson() => _$GetChatResponseObjectToJson(this); + static GetChatResponseObject getDateDummy(int timestamp) { + DateTime elementDate = DateTime.fromMillisecondsSinceEpoch(timestamp * 1000); + + return GetChatResponseObject( + 0, + "", + GetRoomResponseObjectMessageActorType.user, + "", + "", + timestamp, + elementDate.toIso8601String(), + GetRoomResponseObjectMessageType.system, + false, + "", + Jiffy.parseFromDateTime(elementDate).format(pattern: "dd.MM.yyyy"), + null, + null, + null + ); + } + } Map? _fromJson(json) { diff --git a/lib/api/marianumcloud/talk/chat/getChatResponse.g.dart b/lib/api/marianumcloud/talk/chat/getChatResponse.g.dart index c705ec0..82c7102 100644 --- a/lib/api/marianumcloud/talk/chat/getChatResponse.g.dart +++ b/lib/api/marianumcloud/talk/chat/getChatResponse.g.dart @@ -35,6 +35,12 @@ GetChatResponseObject _$GetChatResponseObjectFromJson( json['referenceId'] as String, json['message'] as String, _fromJson(json['messageParameters']), + (json['reactions'] as Map?)?.map( + (k, e) => MapEntry(k, e as int), + ), + (json['reactionsSelf'] as List?) + ?.map((e) => e as String) + .toList(), ); Map _$GetChatResponseObjectToJson( @@ -53,6 +59,8 @@ Map _$GetChatResponseObjectToJson( 'isReplyable': instance.isReplyable, 'referenceId': instance.referenceId, 'message': instance.message, + 'reactions': instance.reactions, + 'reactionsSelf': instance.reactionsSelf, 'messageParameters': instance.messageParameters?.map((k, e) => MapEntry(k, e.toJson())), }; diff --git a/lib/api/marianumcloud/talk/talkApi.dart b/lib/api/marianumcloud/talk/talkApi.dart index 3a8b3b3..26e0bc7 100644 --- a/lib/api/marianumcloud/talk/talkApi.dart +++ b/lib/api/marianumcloud/talk/talkApi.dart @@ -54,7 +54,7 @@ abstract class TalkApi extends ApiRequest { return assembled; } catch (_) { // TODO report error - log("Error assembling Talk API response on $endpoint with body: $body and headers: ${headers.toString()}"); + log("Error assembling Talk API ${T.toString()} response on ${endpoint.path} with body: $body and headers: ${headers.toString()}"); } throw Exception("Error assembling Talk API response"); diff --git a/lib/view/pages/talk/chatBubble.dart b/lib/view/pages/talk/chatBubble.dart index 10b355b..a5235aa 100644 --- a/lib/view/pages/talk/chatBubble.dart +++ b/lib/view/pages/talk/chatBubble.dart @@ -56,9 +56,10 @@ class _ChatBubbleState extends State { } BubbleStyle getSelfStyle(bool seamless) { + var color = AppTheme.isDarkMode(context) ? const Color(0xff005c4b) : const Color(0xffd3d3d3); return BubbleStyle( nip: BubbleNip.rightBottom, - color: seamless ? Colors.transparent : const Color(0xff005c4b), + color: seamless ? Colors.transparent : color, borderWidth: seamless ? 0 : 1, elevation: seamless ? 0 : 1, margin: const BubbleEdges.only(bottom: 10, right: 10, left: 50), @@ -99,162 +100,191 @@ class _ChatBubbleState extends State { bool showBubbleTime = widget.bubbleData.messageType != GetRoomResponseObjectMessageType.system; var actorTextStyle = TextStyle(color: Theme.of(context).primaryColor, fontWeight: FontWeight.bold); - return GestureDetector( - child: Bubble( + return Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.end, + textDirection: TextDirection.ltr, + crossAxisAlignment: CrossAxisAlignment.end, - style: getStyle(), - child: Container( - constraints: BoxConstraints( - maxWidth: MediaQuery.of(context).size.width * 0.9, - minWidth: showActorDisplayName ? _textSize(widget.bubbleData.actorDisplayName, actorTextStyle).width : 30, - ), - child: Stack( - children: [ - Padding( - padding: EdgeInsets.only(bottom: showBubbleTime ? 18 : 0, top: showActorDisplayName ? 18 : 0), - child: FutureBuilder( - future: message.getWidget(), - builder: (context, snapshot) { - if(!snapshot.hasData) return const CircularProgressIndicator(); - return snapshot.data ?? const Icon(Icons.error); - }, - ) + children: [ + GestureDetector( + child: Bubble( + + style: getStyle(), + child: Container( + constraints: BoxConstraints( + maxWidth: MediaQuery.of(context).size.width * 0.9, + minWidth: showActorDisplayName ? _textSize(widget.bubbleData.actorDisplayName, actorTextStyle).width : 30, ), - Visibility( - visible: showActorDisplayName, - child: Positioned( - top: 0, - left: 0, - child: Text( - widget.bubbleData.actorDisplayName, - textAlign: TextAlign.start, - style: actorTextStyle, + child: Stack( + children: [ + Padding( + padding: EdgeInsets.only(bottom: showBubbleTime ? 18 : 0, top: showActorDisplayName ? 18 : 0), + child: message.getWidget() ), - ), - ), - Visibility( - visible: showBubbleTime, - child: Positioned( - bottom: 0, - right: 0, - child: Text( - Jiffy.parseFromMillisecondsSinceEpoch(widget.bubbleData.timestamp * 1000).format(pattern: "HH:mm"), - textAlign: TextAlign.end, - style: const TextStyle(color: Colors.grey, fontSize: 12), + Visibility( + visible: showActorDisplayName, + child: Positioned( + top: 0, + left: 0, + child: Text( + widget.bubbleData.actorDisplayName, + textAlign: TextAlign.start, + style: actorTextStyle, + ), + ), ), - ), + Visibility( + visible: showBubbleTime, + child: Positioned( + bottom: 0, + right: 0, + child: Text( + Jiffy.parseFromMillisecondsSinceEpoch(widget.bubbleData.timestamp * 1000).format(pattern: "HH:mm"), + textAlign: TextAlign.end, + style: const TextStyle(color: Colors.grey, fontSize: 12), + ), + ), + ), + Visibility( + visible: downloadProgress > 0, + child: Positioned( + top: 0, + left: 0, + right: 0, + bottom: 0, + child: Stack( + children: [ + const Center(child: Icon(Icons.download)), + const Center(child: CircularProgressIndicator(color: Colors.white)), + Center(child: CircularProgressIndicator(value: downloadProgress/100)), + ], + ) + ), + ), + ], ), - Visibility( - visible: downloadProgress > 0, - child: Positioned( - top: 0, - left: 0, - right: 0, - bottom: 0, - child: Stack( - children: [ - const Center(child: Icon(Icons.download)), - const Center(child: CircularProgressIndicator(color: Colors.white)), - Center(child: CircularProgressIndicator(value: downloadProgress/100)), - ], - ) - ), - ), - ], + ), ), - ), - ), - onLongPress: () { - showDialog(context: context, builder: (context) { - return SimpleDialog( - children: [ - Visibility( - visible: !message.containsFile && widget.bubbleData.messageType == GetRoomResponseObjectMessageType.comment, - child: ListTile( - leading: const Icon(Icons.copy), - title: const Text("Nachricht kopieren"), - onTap: () => { - Clipboard.setData(ClipboardData(text: widget.bubbleData.message)), - Navigator.of(context).pop(), - }, - ), - ), - Visibility( - visible: !widget.isSender && widget.chatData.type != GetRoomResponseObjectConversationType.oneToOne, - child: ListTile( - leading: const Icon(Icons.sms_outlined), - title: Text("Private Nachricht an '${widget.bubbleData.actorDisplayName}'"), - onTap: () => {}, - ), - ), - Visibility( - visible: widget.isSender, - child: ListTile( - leading: const Icon(Icons.delete_outline), - title: const Text("Nachricht löschen"), - onTap: () { - DeleteMessage(widget.chatData.token, widget.bubbleData.id).run().then((value) { - Provider.of(context, listen: false).run(); - Navigator.of(context).pop(); - }); - }, - ), - ), - DebugTile(widget.bubbleData.toJson()).asTile(context), - ], - ); - }); - }, - onTap: () { - if(message.file == null) return; - - if(downloadProgress > 0) { - showDialog(context: context, builder: (context) { - return AlertDialog( - title: const Text("Download abbrechen?"), - content: const Text("Möchtest du den Download abbrechen?"), - actions: [ - TextButton(onPressed: () { - Navigator.of(context).pop(); - }, child: const Text("Nein")), - TextButton(onPressed: () { - downloadCore?.then((value) { - if(!value.isCancelled) value.cancel(); - Navigator.of(context).pop(); - }); - setState(() { - downloadProgress = 0; - downloadCore = null; - }); - }, child: const Text("Ja, Abbrechen")) - ], - ); - }); - - return; - } - - downloadProgress = 1; - downloadCore = FileElement.download(context, message.file!.path!, message.file!.name, (progress) { - if(progress > 1) { - setState(() { - downloadProgress = progress; - }); - } - }, (result) { - setState(() { - downloadProgress = 0; - }); - - if(result.type != ResultType.done) { + onLongPress: () { showDialog(context: context, builder: (context) { - return AlertDialog( - content: Text(result.message), + return SimpleDialog( + children: [ + Visibility( + visible: !message.containsFile && widget.bubbleData.messageType == GetRoomResponseObjectMessageType.comment, + child: ListTile( + leading: const Icon(Icons.copy), + title: const Text("Nachricht kopieren"), + onTap: () => { + Clipboard.setData(ClipboardData(text: widget.bubbleData.message)), + Navigator.of(context).pop(), + }, + ), + ), + Visibility( + visible: !widget.isSender && widget.chatData.type != GetRoomResponseObjectConversationType.oneToOne, + child: ListTile( + leading: const Icon(Icons.sms_outlined), + title: Text("Private Nachricht an '${widget.bubbleData.actorDisplayName}'"), + onTap: () => {}, + ), + ), + Visibility( + visible: widget.isSender, + child: ListTile( + leading: const Icon(Icons.delete_outline), + title: const Text("Nachricht löschen"), + onTap: () { + DeleteMessage(widget.chatData.token, widget.bubbleData.id).run().then((value) { + Provider.of(context, listen: false).run(); + Navigator.of(context).pop(); + }); + }, + ), + ), + DebugTile(widget.bubbleData.toJson()).asTile(context), + ], ); }); - } - }); - }, + }, + onTap: () { + if(message.file == null) return; + + if(downloadProgress > 0) { + showDialog(context: context, builder: (context) { + return AlertDialog( + title: const Text("Download abbrechen?"), + content: const Text("Möchtest du den Download abbrechen?"), + actions: [ + TextButton(onPressed: () { + Navigator.of(context).pop(); + }, child: const Text("Nein")), + TextButton(onPressed: () { + downloadCore?.then((value) { + if(!value.isCancelled) value.cancel(); + Navigator.of(context).pop(); + }); + setState(() { + downloadProgress = 0; + downloadCore = null; + }); + }, child: const Text("Ja, Abbrechen")) + ], + ); + }); + + return; + } + + downloadProgress = 1; + downloadCore = FileElement.download(context, message.file!.path!, message.file!.name, (progress) { + if(progress > 1) { + setState(() { + downloadProgress = progress; + }); + } + }, (result) { + setState(() { + downloadProgress = 0; + }); + + if(result.type != ResultType.done) { + showDialog(context: context, builder: (context) { + return AlertDialog( + content: Text(result.message), + ); + }); + } + }); + }, + ), + Visibility( + visible: widget.bubbleData.reactions != null, + child: Transform.translate( + offset: const Offset(0, -10), + child: Container( + width: MediaQuery.of(context).size.width, + margin: const EdgeInsets.only(left: 5, right: 5), + child: Wrap( + alignment: widget.isSender ? WrapAlignment.end : WrapAlignment.start, + crossAxisAlignment: WrapCrossAlignment.start, + //mainAxisSize: MainAxisSize.max, + children: widget.bubbleData.reactions?.entries.map((e) { + return Container( + margin: const EdgeInsets.only(right: 2.5, left: 2.5), + child: Chip( + label: Text("${e.key} ${e.value}"), + visualDensity: const VisualDensity(vertical: VisualDensity.minimumDensity, horizontal: VisualDensity.minimumDensity), + padding: EdgeInsets.zero, + backgroundColor: widget.bubbleData.reactionsSelf?.contains(e.key) ?? false ? Theme.of(context).primaryColor : null, + ), + ); + }).toList() ?? [], + ), + ), + ), + ), + ], ); } } diff --git a/lib/view/pages/talk/chatMessage.dart b/lib/view/pages/talk/chatMessage.dart index 95fd3a4..8446d3c 100644 --- a/lib/view/pages/talk/chatMessage.dart +++ b/lib/view/pages/talk/chatMessage.dart @@ -1,13 +1,12 @@ -import 'dart:convert'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter_linkify/flutter_linkify.dart'; -import 'package:shared_preferences/shared_preferences.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'; class ChatMessage { String originalMessage; @@ -27,8 +26,7 @@ class ChatMessage { } } - Future getWidget() async { - SharedPreferences preferences = await SharedPreferences.getInstance(); + Widget getWidget() { if(file == null) { return SelectableLinkify( @@ -50,10 +48,9 @@ class ChatMessage { placeholder: (context, url) { return const Padding(padding: EdgeInsets.all(10), child: CircularProgressIndicator()); }, - fadeInDuration: const Duration(seconds: 1), imageUrl: "https://cloud.marianum-fulda.de/core/preview?fileId=${file!.id}&x=100&y=-1&a=1", httpHeaders: { - "Authorization": "Basic ${base64.encode(utf8.encode("${preferences.getString("username")}:${preferences.getString("password")}"))}" // TODO move authentication + "Authorization": "Basic ${AccountData().buildHttpAuthString()}" }, ); } diff --git a/lib/view/pages/talk/chatView.dart b/lib/view/pages/talk/chatView.dart index 8a891dd..e522c72 100644 --- a/lib/view/pages/talk/chatView.dart +++ b/lib/view/pages/talk/chatView.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:jiffy/jiffy.dart'; import 'package:loader_overlay/loader_overlay.dart'; import 'package:provider/provider.dart'; @@ -47,25 +46,15 @@ class _ChatViewState extends State { DateTime lastDate = DateTime.now(); data.getChatResponse.sortByTimestamp().forEach((element) { DateTime elementDate = DateTime.fromMillisecondsSinceEpoch(element.timestamp * 1000); + + if(element.systemMessage.contains("reaction")) return; + if(elementDate.weekday != lastDate.weekday) { lastDate = elementDate; messages.add(ChatBubble( context: context, isSender: true, - bubbleData: GetChatResponseObject( - 1, - "asd", - GetRoomResponseObjectMessageActorType.bridge, - "system", - "System", - element.timestamp, - elementDate.toIso8601String(), - GetRoomResponseObjectMessageType.system, - false, - "", - Jiffy.parseFromDateTime(elementDate).format(pattern: "dd.MM.yyyy"), - null - ), + bubbleData: GetChatResponseObject.getDateDummy(element.timestamp), chatData: widget.room )); }