From 0b48c2e7ab7e2631a8cf75efd8db0e86e71ce0fa Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Elias=20M=C3=BCller?= <elias@elias-mueller.com>
Date: Wed, 13 Sep 2023 18:57:52 +0200
Subject: [PATCH] Added details for chats with participants list

---
 .../talk/getParticipants/getParticipants.dart | 22 +++++
 .../getParticipants/getParticipantsCache.dart | 26 ++++++
 .../getParticipantsResponse.dart              | 70 ++++++++++++++++
 .../getParticipantsResponse.g.dart            | 83 +++++++++++++++++++
 lib/api/marianumcloud/talk/talkApi.dart       |  4 +-
 lib/view/pages/talk/chatDetails/chatInfo.dart | 79 ++++++++++++++++++
 .../participants/participantsListView.dart    | 32 +++++++
 lib/view/pages/talk/chatView.dart             | 26 ++++--
 lib/view/pages/talk/components/chatTile.dart  |  5 +-
 lib/view/pages/talk/talkNavigator.dart        |  7 +-
 lib/widget/clickableAppBar.dart               | 15 ++++
 lib/widget/largeProfilePictureView.dart       | 22 +++++
 lib/widget/userAvatar.dart                    |  9 +-
 13 files changed, 380 insertions(+), 20 deletions(-)
 create mode 100644 lib/api/marianumcloud/talk/getParticipants/getParticipants.dart
 create mode 100644 lib/api/marianumcloud/talk/getParticipants/getParticipantsCache.dart
 create mode 100644 lib/api/marianumcloud/talk/getParticipants/getParticipantsResponse.dart
 create mode 100644 lib/api/marianumcloud/talk/getParticipants/getParticipantsResponse.g.dart
 create mode 100644 lib/view/pages/talk/chatDetails/chatInfo.dart
 create mode 100644 lib/view/pages/talk/chatDetails/participants/participantsListView.dart
 create mode 100644 lib/widget/clickableAppBar.dart
 create mode 100644 lib/widget/largeProfilePictureView.dart

diff --git a/lib/api/marianumcloud/talk/getParticipants/getParticipants.dart b/lib/api/marianumcloud/talk/getParticipants/getParticipants.dart
new file mode 100644
index 0000000..2119d92
--- /dev/null
+++ b/lib/api/marianumcloud/talk/getParticipants/getParticipants.dart
@@ -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);
+  }
+
+}
\ No newline at end of file
diff --git a/lib/api/marianumcloud/talk/getParticipants/getParticipantsCache.dart b/lib/api/marianumcloud/talk/getParticipants/getParticipantsCache.dart
new file mode 100644
index 0000000..9ca1c70
--- /dev/null
+++ b/lib/api/marianumcloud/talk/getParticipants/getParticipantsCache.dart
@@ -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));
+  }
+
+}
\ No newline at end of file
diff --git a/lib/api/marianumcloud/talk/getParticipants/getParticipantsResponse.dart b/lib/api/marianumcloud/talk/getParticipants/getParticipantsResponse.dart
new file mode 100644
index 0000000..2d7682c
--- /dev/null
+++ b/lib/api/marianumcloud/talk/getParticipants/getParticipantsResponse.dart
@@ -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
+}
\ No newline at end of file
diff --git a/lib/api/marianumcloud/talk/getParticipants/getParticipantsResponse.g.dart b/lib/api/marianumcloud/talk/getParticipants/getParticipantsResponse.g.dart
new file mode 100644
index 0000000..6dafc62
--- /dev/null
+++ b/lib/api/marianumcloud/talk/getParticipants/getParticipantsResponse.g.dart
@@ -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,
+};
diff --git a/lib/api/marianumcloud/talk/talkApi.dart b/lib/api/marianumcloud/talk/talkApi.dart
index 0f3b254..cc0ef16 100644
--- a/lib/api/marianumcloud/talk/talkApi.dart
+++ b/lib/api/marianumcloud/talk/talkApi.dart
@@ -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");
diff --git a/lib/view/pages/talk/chatDetails/chatInfo.dart b/lib/view/pages/talk/chatDetails/chatInfo.dart
new file mode 100644
index 0000000..7b0d542
--- /dev/null
+++ b/lib/view/pages/talk/chatDetails/chatInfo.dart
@@ -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!)),
+              ),
+            ],
+          ],
+        ),
+      ),
+    );
+  }
+}
diff --git a/lib/view/pages/talk/chatDetails/participants/participantsListView.dart b/lib/view/pages/talk/chatDetails/participants/participantsListView.dart
new file mode 100644
index 0000000..82e5b06
--- /dev/null
+++ b/lib/view/pages/talk/chatDetails/participants/participantsListView.dart
@@ -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(),
+      ),
+    );
+  }
+}
diff --git a/lib/view/pages/talk/chatView.dart b/lib/view/pages/talk/chatView.dart
index 1c2fe91..29e696b 100644
--- a/lib/view/pages/talk/chatView.dart
+++ b/lib/view/pages/talk/chatView.dart
@@ -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,15 +77,20 @@ class _ChatViewState extends State<ChatView> {
 
         return Scaffold(
           backgroundColor: const Color(0xffefeae2),
-          appBar: AppBar(
-            title: Row(
-              children: [
-                widget.avatar,
-                const SizedBox(width: 10),
-                Expanded(
-                  child: Text(widget.room.displayName, overflow: TextOverflow.ellipsis, maxLines: 1),
-                )
-              ],
+          appBar: ClickableAppBar(
+            onTap: () {
+              TalkNavigator.pushSplitView(context, ChatInfo(widget.room));
+            },
+            appBar: AppBar(
+              title: Row(
+                children: [
+                  widget.avatar,
+                  const SizedBox(width: 10),
+                  Expanded(
+                    child: Text(widget.room.displayName, overflow: TextOverflow.ellipsis, maxLines: 1),
+                  )
+                ],
+              ),
             ),
           ),
           body: Container(
diff --git a/lib/view/pages/talk/components/chatTile.dart b/lib/view/pages/talk/components/chatTile.dart
index 8710b62..ca60972 100644
--- a/lib/view/pages/talk/components/chatTile.dart
+++ b/lib/view/pages/talk/components/chatTile.dart
@@ -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: () {
diff --git a/lib/view/pages/talk/talkNavigator.dart b/lib/view/pages/talk/talkNavigator.dart
index 8c773d9..96b09de 100644
--- a/lib/view/pages/talk/talkNavigator.dart
+++ b/lib/view/pages/talk/talkNavigator.dart
@@ -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);
     }
diff --git a/lib/widget/clickableAppBar.dart b/lib/widget/clickableAppBar.dart
new file mode 100644
index 0000000..b83cb10
--- /dev/null
+++ b/lib/widget/clickableAppBar.dart
@@ -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;
+}
diff --git a/lib/widget/largeProfilePictureView.dart b/lib/widget/largeProfilePictureView.dart
new file mode 100644
index 0000000..7ea7966
--- /dev/null
+++ b/lib/widget/largeProfilePictureView.dart
@@ -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),
+      ),
+    );
+  }
+}
diff --git a/lib/widget/userAvatar.dart b/lib/widget/userAvatar.dart
index 1afdb55..e564733 100644
--- a/lib/widget/userAvatar.dart
+++ b/lib/widget/userAvatar.dart
@@ -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()),
     );
+
   }
 }