Added details for chats with participants list
This commit is contained in:
		| @@ -0,0 +1,22 @@ | ||||
| import 'dart:convert'; | ||||
|  | ||||
| import 'package:http/http.dart' as http; | ||||
|  | ||||
| import '../talkApi.dart'; | ||||
| import 'getParticipantsResponse.dart'; | ||||
|  | ||||
| class GetParticipants extends TalkApi<GetParticipantsResponse> { | ||||
|   String token; | ||||
|   GetParticipants(this.token) : super("v4/room/$token/participants", null); | ||||
|  | ||||
|   @override | ||||
|   GetParticipantsResponse assemble(String raw) { | ||||
|     return GetParticipantsResponse.fromJson(jsonDecode(raw)['ocs']); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Future<http.Response> request(Uri uri, Object? body, Map<String, String>? headers) { | ||||
|     return http.get(uri, headers: headers); | ||||
|   } | ||||
|  | ||||
| } | ||||
| @@ -0,0 +1,26 @@ | ||||
| import 'dart:convert'; | ||||
|  | ||||
| import '../../../requestCache.dart'; | ||||
| import 'getParticipants.dart'; | ||||
| import 'getParticipantsResponse.dart'; | ||||
|  | ||||
| class GetParticipantsCache extends RequestCache<GetParticipantsResponse> { | ||||
|   String chatToken; | ||||
|  | ||||
|   GetParticipantsCache({required onUpdate, required this.chatToken}) : super(RequestCache.cacheNothing, onUpdate) { | ||||
|     start("MarianumMobile", "nc-chat-participants-$chatToken"); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Future<GetParticipantsResponse> onLoad() { | ||||
|     return GetParticipants( | ||||
|         chatToken, | ||||
|     ).run(); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   GetParticipantsResponse onLocalData(String json) { | ||||
|     return GetParticipantsResponse.fromJson(jsonDecode(json)); | ||||
|   } | ||||
|  | ||||
| } | ||||
| @@ -0,0 +1,70 @@ | ||||
|  | ||||
| import 'package:json_annotation/json_annotation.dart'; | ||||
|  | ||||
| part 'getParticipantsResponse.g.dart'; | ||||
|  | ||||
| @JsonSerializable(explicitToJson: true) | ||||
| class GetParticipantsResponse { | ||||
|   Set<GetParticipantsResponseObject> data; | ||||
|  | ||||
|   GetParticipantsResponse(this.data); | ||||
|  | ||||
|   factory GetParticipantsResponse.fromJson(Map<String, dynamic> json) => _$GetParticipantsResponseFromJson(json); | ||||
|   Map<String, dynamic> toJson() => _$GetParticipantsResponseToJson(this); | ||||
| } | ||||
|  | ||||
| @JsonSerializable(explicitToJson: true) | ||||
| class GetParticipantsResponseObject { | ||||
|   int attendeeId; | ||||
|   String actorType; | ||||
|   String actorId; | ||||
|   String displayName; | ||||
|   GetParticipantsResponseObjectParticipantType participantType; | ||||
|   int lastPing; | ||||
|   GetParticipantsResponseObjectParticipantsInCallFlags inCall; | ||||
|   int permissions; | ||||
|   int attendeePermissions; | ||||
|   String? sessionId; | ||||
|   List<String> sessionIds; | ||||
|   String? status; | ||||
|   String? statusIcon; | ||||
|   String? statusMessage; | ||||
|   String? roomToken; | ||||
|  | ||||
|   GetParticipantsResponseObject( | ||||
|       this.attendeeId, | ||||
|       this.actorType, | ||||
|       this.actorId, | ||||
|       this.displayName, | ||||
|       this.participantType, | ||||
|       this.lastPing, | ||||
|       this.inCall, | ||||
|       this.permissions, | ||||
|       this.attendeePermissions, | ||||
|       this.sessionId, | ||||
|       this.sessionIds, | ||||
|       this.status, | ||||
|       this.statusIcon, | ||||
|       this.statusMessage, | ||||
|       this.roomToken); | ||||
|  | ||||
|   factory GetParticipantsResponseObject.fromJson(Map<String, dynamic> json) => _$GetParticipantsResponseObjectFromJson(json); | ||||
|   Map<String, dynamic> toJson() => _$GetParticipantsResponseObjectToJson(this); | ||||
| } | ||||
|  | ||||
| enum GetParticipantsResponseObjectParticipantType { | ||||
|   @JsonValue(1) owner, | ||||
|   @JsonValue(2) moderator, | ||||
|   @JsonValue(3) user, | ||||
|   @JsonValue(4) guest, | ||||
|   @JsonValue(5) userFollowingPublicLink, | ||||
|   @JsonValue(6) guestWithModeratorPermissions | ||||
| } | ||||
|  | ||||
| enum GetParticipantsResponseObjectParticipantsInCallFlags { | ||||
|   @JsonValue(0) disconnected, | ||||
|   @JsonValue(1) inCall, | ||||
|   @JsonValue(2) providesAudio, | ||||
|   @JsonValue(3) providesVideo, | ||||
|   @JsonValue(4) usesSipDialIn | ||||
| } | ||||
| @@ -0,0 +1,83 @@ | ||||
| // GENERATED CODE - DO NOT MODIFY BY HAND | ||||
|  | ||||
| part of 'getParticipantsResponse.dart'; | ||||
|  | ||||
| // ************************************************************************** | ||||
| // JsonSerializableGenerator | ||||
| // ************************************************************************** | ||||
|  | ||||
| GetParticipantsResponse _$GetParticipantsResponseFromJson( | ||||
|         Map<String, dynamic> json) => | ||||
|     GetParticipantsResponse( | ||||
|       (json['data'] as List<dynamic>) | ||||
|           .map((e) => | ||||
|               GetParticipantsResponseObject.fromJson(e as Map<String, dynamic>)) | ||||
|           .toSet(), | ||||
|     ); | ||||
|  | ||||
| Map<String, dynamic> _$GetParticipantsResponseToJson( | ||||
|         GetParticipantsResponse instance) => | ||||
|     <String, dynamic>{ | ||||
|       'data': instance.data.map((e) => e.toJson()).toList(), | ||||
|     }; | ||||
|  | ||||
| GetParticipantsResponseObject _$GetParticipantsResponseObjectFromJson( | ||||
|         Map<String, dynamic> json) => | ||||
|     GetParticipantsResponseObject( | ||||
|       json['attendeeId'] as int, | ||||
|       json['actorType'] as String, | ||||
|       json['actorId'] as String, | ||||
|       json['displayName'] as String, | ||||
|       $enumDecode(_$GetParticipantsResponseObjectParticipantTypeEnumMap, | ||||
|           json['participantType']), | ||||
|       json['lastPing'] as int, | ||||
|       $enumDecode(_$GetParticipantsResponseObjectParticipantsInCallFlagsEnumMap, | ||||
|           json['inCall']), | ||||
|       json['permissions'] as int, | ||||
|       json['attendeePermissions'] as int, | ||||
|       json['sessionId'] as String?, | ||||
|       (json['sessionIds'] as List<dynamic>).map((e) => e as String).toList(), | ||||
|       json['status'] as String?, | ||||
|       json['statusIcon'] as String?, | ||||
|       json['statusMessage'] as String?, | ||||
|       json['roomToken'] as String?, | ||||
|     ); | ||||
|  | ||||
| Map<String, dynamic> _$GetParticipantsResponseObjectToJson( | ||||
|         GetParticipantsResponseObject instance) => | ||||
|     <String, dynamic>{ | ||||
|       'attendeeId': instance.attendeeId, | ||||
|       'actorType': instance.actorType, | ||||
|       'actorId': instance.actorId, | ||||
|       'displayName': instance.displayName, | ||||
|       'participantType': _$GetParticipantsResponseObjectParticipantTypeEnumMap[ | ||||
|           instance.participantType]!, | ||||
|       'lastPing': instance.lastPing, | ||||
|       'inCall': _$GetParticipantsResponseObjectParticipantsInCallFlagsEnumMap[ | ||||
|           instance.inCall]!, | ||||
|       'permissions': instance.permissions, | ||||
|       'attendeePermissions': instance.attendeePermissions, | ||||
|       'sessionId': instance.sessionId, | ||||
|       'sessionIds': instance.sessionIds, | ||||
|       'status': instance.status, | ||||
|       'statusIcon': instance.statusIcon, | ||||
|       'statusMessage': instance.statusMessage, | ||||
|       'roomToken': instance.roomToken, | ||||
|     }; | ||||
|  | ||||
| const _$GetParticipantsResponseObjectParticipantTypeEnumMap = { | ||||
|   GetParticipantsResponseObjectParticipantType.owner: 1, | ||||
|   GetParticipantsResponseObjectParticipantType.moderator: 2, | ||||
|   GetParticipantsResponseObjectParticipantType.user: 3, | ||||
|   GetParticipantsResponseObjectParticipantType.guest: 4, | ||||
|   GetParticipantsResponseObjectParticipantType.userFollowingPublicLink: 5, | ||||
|   GetParticipantsResponseObjectParticipantType.guestWithModeratorPermissions: 6, | ||||
| }; | ||||
|  | ||||
| const _$GetParticipantsResponseObjectParticipantsInCallFlagsEnumMap = { | ||||
|   GetParticipantsResponseObjectParticipantsInCallFlags.disconnected: 0, | ||||
|   GetParticipantsResponseObjectParticipantsInCallFlags.inCall: 1, | ||||
|   GetParticipantsResponseObjectParticipantsInCallFlags.providesAudio: 2, | ||||
|   GetParticipantsResponseObjectParticipantsInCallFlags.providesVideo: 3, | ||||
|   GetParticipantsResponseObjectParticipantsInCallFlags.usesSipDialIn: 4, | ||||
| }; | ||||
| @@ -55,9 +55,9 @@ abstract class TalkApi<T> extends ApiRequest { | ||||
|     try { | ||||
|       assembled = assemble(data.body); | ||||
|       return assembled; | ||||
|     } catch (_) { | ||||
|     } catch (e) { | ||||
|       // TODO report error | ||||
|       log("Error assembling Talk API ${T.toString()} response on ${endpoint.path} with body: $body and headers: ${headers.toString()}"); | ||||
|       log("Error assembling Talk API ${T.toString()} message: ${e.toString()} response on ${endpoint.path} with request body: $body and request headers: ${headers.toString()}"); | ||||
|     } | ||||
|  | ||||
|     throw Exception("Error assembling Talk API response"); | ||||
|   | ||||
							
								
								
									
										79
									
								
								lib/view/pages/talk/chatDetails/chatInfo.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								lib/view/pages/talk/chatDetails/chatInfo.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,79 @@ | ||||
| import 'package:flutter/material.dart'; | ||||
|  | ||||
| import '../../../../api/marianumcloud/talk/getParticipants/getParticipantsCache.dart'; | ||||
| import '../../../../api/marianumcloud/talk/getParticipants/getParticipantsResponse.dart'; | ||||
| import '../../../../api/marianumcloud/talk/room/getRoomResponse.dart'; | ||||
| import '../../../../widget/largeProfilePictureView.dart'; | ||||
| import '../../../../widget/loadingSpinner.dart'; | ||||
| import '../../../../widget/userAvatar.dart'; | ||||
| import '../talkNavigator.dart'; | ||||
| import 'participants/participantsListView.dart'; | ||||
|  | ||||
| class ChatInfo extends StatefulWidget { | ||||
|   final GetRoomResponseObject room; | ||||
|   const ChatInfo(this.room, {super.key}); | ||||
|  | ||||
|   @override | ||||
|   State<ChatInfo> createState() => _ChatInfoState(); | ||||
| } | ||||
|  | ||||
| class _ChatInfoState extends State<ChatInfo> { | ||||
|   GetParticipantsResponse? participants; | ||||
|  | ||||
|   @override | ||||
|   void initState() { | ||||
|     GetParticipantsCache( | ||||
|       chatToken: widget.room.token, | ||||
|       onUpdate: (GetParticipantsResponse data) { | ||||
|         setState(() { | ||||
|           participants = data; | ||||
|         }); | ||||
|       } | ||||
|     ); | ||||
|     super.initState(); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     bool isGroup = widget.room.type != GetRoomResponseObjectConversationType.oneToOne; | ||||
|     return Scaffold( | ||||
|       appBar: AppBar( | ||||
|         title: Text(widget.room.displayName), | ||||
|       ), | ||||
|       body: Center( | ||||
|         child: Column( | ||||
|           crossAxisAlignment: CrossAxisAlignment.center, | ||||
|           children: [ | ||||
|             const SizedBox(height: 30), | ||||
|             GestureDetector( | ||||
|               child: UserAvatar( | ||||
|                 username: widget.room.name, | ||||
|                 isGroup: isGroup, | ||||
|                 size: 80, | ||||
|               ), | ||||
|               onTap: () { | ||||
|                 if(isGroup) return; | ||||
|                 TalkNavigator.pushSplitView(context, LargeProfilePictureView(widget.room.name)); | ||||
|               }, | ||||
|             ), | ||||
|             const SizedBox(height: 30), | ||||
|             Text(widget.room.displayName, textAlign: TextAlign.center, style: const TextStyle(fontSize: 30)), | ||||
|             if(!isGroup) Text(widget.room.name), | ||||
|             const SizedBox(height: 10), | ||||
|             if(isGroup) Text(widget.room.description, textAlign: TextAlign.center), | ||||
|             const SizedBox(height: 30), | ||||
|             if(participants == null) const LoadingSpinner(), | ||||
|             if(participants != null) ...[ | ||||
|               ListTile( | ||||
|                 leading: const Icon(Icons.supervised_user_circle), | ||||
|                 title: Text("${participants!.data.length} Teilnehmer"), | ||||
|                 trailing: const Icon(Icons.arrow_right), | ||||
|                 onTap: () => TalkNavigator.pushSplitView(context, ParticipantsListView(participants!)), | ||||
|               ), | ||||
|             ], | ||||
|           ], | ||||
|         ), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| @@ -0,0 +1,32 @@ | ||||
| import 'package:flutter/material.dart'; | ||||
|  | ||||
| import '../../../../../api/marianumcloud/talk/getParticipants/getParticipantsResponse.dart'; | ||||
| import '../../../../../widget/userAvatar.dart'; | ||||
|  | ||||
| class ParticipantsListView extends StatefulWidget { | ||||
|   final GetParticipantsResponse participantsResponse; | ||||
|   const ParticipantsListView(this.participantsResponse, {super.key}); | ||||
|  | ||||
|   @override | ||||
|   State<ParticipantsListView> createState() => _ParticipantsListViewState(); | ||||
| } | ||||
|  | ||||
| class _ParticipantsListViewState extends State<ParticipantsListView> { | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return Scaffold( | ||||
|       appBar: AppBar( | ||||
|         title: const Text("Teilnehmende"), | ||||
|       ), | ||||
|       body: ListView( | ||||
|         children: widget.participantsResponse.data.map((participant) { | ||||
|           return ListTile( | ||||
|             leading: UserAvatar(username: participant.actorId), | ||||
|             title: Text(participant.displayName), | ||||
|             subtitle: participant.statusMessage != null ? Text(participant.statusMessage!) : null, | ||||
|           ); | ||||
|         }).toList(), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| @@ -7,10 +7,13 @@ import '../../../api/marianumcloud/talk/chat/getChatResponse.dart'; | ||||
| import '../../../api/marianumcloud/talk/room/getRoomResponse.dart'; | ||||
| import '../../../theming/appTheme.dart'; | ||||
| import '../../../model/chatList/chatProps.dart'; | ||||
| import '../../../widget/clickableAppBar.dart'; | ||||
| import '../../../widget/loadingSpinner.dart'; | ||||
| import '../../../widget/userAvatar.dart'; | ||||
| import 'chatDetails/chatInfo.dart'; | ||||
| import 'components/chatBubble.dart'; | ||||
| import 'components/chatTextfield.dart'; | ||||
| import 'talkNavigator.dart'; | ||||
|  | ||||
| class ChatView extends StatefulWidget { | ||||
|   final GetRoomResponseObject room; | ||||
| @@ -74,6 +77,10 @@ class _ChatViewState extends State<ChatView> { | ||||
|  | ||||
|         return Scaffold( | ||||
|           backgroundColor: const Color(0xffefeae2), | ||||
|           appBar: ClickableAppBar( | ||||
|             onTap: () { | ||||
|               TalkNavigator.pushSplitView(context, ChatInfo(widget.room)); | ||||
|             }, | ||||
|             appBar: AppBar( | ||||
|               title: Row( | ||||
|                 children: [ | ||||
| @@ -85,6 +92,7 @@ class _ChatViewState extends State<ChatView> { | ||||
|                 ], | ||||
|               ), | ||||
|             ), | ||||
|           ), | ||||
|           body: Container( | ||||
|             decoration: BoxDecoration( | ||||
|               image: DecorationImage( | ||||
|   | ||||
| @@ -2,8 +2,6 @@ | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_split_view/flutter_split_view.dart'; | ||||
| import 'package:jiffy/jiffy.dart'; | ||||
| import 'package:marianum_mobile/view/pages/talk/talkNavigator.dart'; | ||||
| import 'package:persistent_bottom_nav_bar/persistent_tab_view.dart'; | ||||
| import 'package:provider/provider.dart'; | ||||
| import 'package:shared_preferences/shared_preferences.dart'; | ||||
|  | ||||
| @@ -18,6 +16,7 @@ import '../../../../widget/confirmDialog.dart'; | ||||
| import '../../../../widget/debug/debugTile.dart'; | ||||
| import '../../../../widget/userAvatar.dart'; | ||||
| import '../chatView.dart'; | ||||
| import '../talkNavigator.dart'; | ||||
|  | ||||
| class ChatTile extends StatefulWidget { | ||||
|   final GetRoomResponseObject data; | ||||
| @@ -120,7 +119,7 @@ class _ChatTileState extends State<ChatTile> { | ||||
|         onTap: () async { | ||||
|           setCurrentAsRead(); | ||||
|           ChatView view = ChatView(room: widget.data, selfId: username, avatar: circleAvatar); | ||||
|           TalkNavigator.pushSplitView(context, view); | ||||
|           TalkNavigator.pushSplitView(context, view, overrideToSingleSubScreen: true); | ||||
|           Provider.of<ChatProps>(context, listen: false).setQueryToken(widget.data.token); | ||||
|         }, | ||||
|         onLongPress: () { | ||||
|   | ||||
| @@ -4,9 +4,10 @@ import 'package:flutter_split_view/flutter_split_view.dart'; | ||||
| import 'package:persistent_bottom_nav_bar/persistent_tab_view.dart'; | ||||
|  | ||||
| class TalkNavigator { | ||||
|   static void pushSplitView(BuildContext context, Widget view) { | ||||
|     if(SplitView.of(context).isSecondaryVisible) { | ||||
|       SplitView.of(context).setSecondary(view); | ||||
|   static void pushSplitView(BuildContext context, Widget view, {bool overrideToSingleSubScreen = false}) { | ||||
|     if(context.findAncestorStateOfType<SplitViewState>() != null && SplitView.of(context).isSecondaryVisible) { | ||||
|       SplitViewState splitView = SplitView.of(context); | ||||
|       overrideToSingleSubScreen ? splitView.setSecondary(view) : splitView.push(view); | ||||
|     } else { | ||||
|       PersistentNavBarNavigator.pushNewScreen(context, screen: view, withNavBar: false); | ||||
|     } | ||||
|   | ||||
							
								
								
									
										15
									
								
								lib/widget/clickableAppBar.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								lib/widget/clickableAppBar.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | ||||
| import 'package:flutter/material.dart'; | ||||
|  | ||||
| class ClickableAppBar extends StatelessWidget implements PreferredSizeWidget { | ||||
|   final VoidCallback onTap; | ||||
|   final AppBar appBar; | ||||
|   const ClickableAppBar({required this.onTap, required this.appBar, super.key}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return GestureDetector(onTap: onTap, child: appBar); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Size get preferredSize => appBar.preferredSize; | ||||
| } | ||||
							
								
								
									
										22
									
								
								lib/widget/largeProfilePictureView.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								lib/widget/largeProfilePictureView.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,22 @@ | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:photo_view/photo_view.dart'; | ||||
|  | ||||
| import '../model/endpointData.dart'; | ||||
|  | ||||
| class LargeProfilePictureView extends StatelessWidget { | ||||
|   final String username; | ||||
|   const LargeProfilePictureView(this.username, {super.key}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return Scaffold( | ||||
|       appBar: AppBar( | ||||
|         title: const Text("Profilbild"), | ||||
|       ), | ||||
|       body: PhotoView( | ||||
|         imageProvider: Image.network("https://${EndpointData().nextcloud().full()}/avatar/$username/1024").image, | ||||
|         backgroundDecoration: BoxDecoration(color: Theme.of(context).colorScheme.surface), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| @@ -5,16 +5,19 @@ import '../model/endpointData.dart'; | ||||
| class UserAvatar extends StatelessWidget { | ||||
|   final String username; | ||||
|   final bool isGroup; | ||||
|   const UserAvatar({required this.username, this.isGroup = false, super.key}); | ||||
|   final int size; | ||||
|   const UserAvatar({required this.username, this.isGroup = false, this.size = 20, super.key}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return CircleAvatar( | ||||
|       foregroundImage: !isGroup ? Image.network("https://${EndpointData().nextcloud().full()}/avatar/$username/128").image : null, | ||||
|       foregroundImage: !isGroup ? Image.network("https://${EndpointData().nextcloud().full()}/avatar/$username/$size").image : null, | ||||
|       backgroundColor: Theme.of(context).primaryColor, | ||||
|       foregroundColor: Colors.white, | ||||
|       onForegroundImageError: !isGroup ? (o, t) {} : null, | ||||
|       child: isGroup ? const Icon(Icons.group) : const Icon(Icons.person), | ||||
|       radius: size.toDouble(), | ||||
|       child: isGroup ? Icon(Icons.group, size: size.toDouble()) : Icon(Icons.person, size: size.toDouble()), | ||||
|     ); | ||||
|  | ||||
|   } | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user