Add session invites to message view

This commit is contained in:
Nutcake 2023-05-02 22:50:35 +02:00
parent 158fccbf3f
commit 38205fe8e1
5 changed files with 241 additions and 115 deletions

View file

@ -4,6 +4,7 @@ import 'dart:developer';
import 'package:contacts_plus_plus/api_client.dart'; import 'package:contacts_plus_plus/api_client.dart';
import 'package:contacts_plus_plus/apis/message_api.dart'; import 'package:contacts_plus_plus/apis/message_api.dart';
import 'package:contacts_plus_plus/auxiliary.dart'; import 'package:contacts_plus_plus/auxiliary.dart';
import 'package:contacts_plus_plus/models/session.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
enum MessageType { enum MessageType {
@ -165,4 +166,4 @@ class AudioClipContent {
assetUri: map["assetUri"], assetUri: map["assetUri"],
); );
} }
} }

View file

@ -9,10 +9,11 @@ class Session {
final String description; final String description;
final List<String> tags; final List<String> tags;
final bool headlessHost; final bool headlessHost;
final String hostUsername;
Session({required this.id, required this.name, required this.sessionUsers, required this.thumbnail, Session({required this.id, required this.name, required this.sessionUsers, required this.thumbnail,
required this.maxUsers, required this.hasEnded, required this.isValid, required this.description, required this.maxUsers, required this.hasEnded, required this.isValid, required this.description,
required this.tags, required this.headlessHost, required this.tags, required this.headlessHost, required this.hostUsername,
}); });
factory Session.fromMap(Map map) { factory Session.fromMap(Map map) {
@ -21,12 +22,13 @@ class Session {
name: map["name"], name: map["name"],
sessionUsers: (map["sessionUsers"] as List? ?? []).map((entry) => SessionUser.fromMap(entry)).toList(), sessionUsers: (map["sessionUsers"] as List? ?? []).map((entry) => SessionUser.fromMap(entry)).toList(),
thumbnail: map["thumbnail"] ?? "", thumbnail: map["thumbnail"] ?? "",
maxUsers: map["maxUsers"], maxUsers: map["maxUsers"] ?? 0,
hasEnded: map["hasEnded"], hasEnded: map["hasEnded"] ?? false,
isValid: map["isValid"], isValid: map["isValid"] ?? true,
description: map["description"] ?? "", description: map["description"] ?? "",
tags: ((map["tags"] as List?) ?? []).map((e) => e.toString()).toList(), tags: ((map["tags"] as List?) ?? []).map((e) => e.toString()).toList(),
headlessHost: map["headlessHost"], headlessHost: map["headlessHost"] ?? false,
hostUsername: map["hostUsername"] ?? "",
); );
} }

View file

@ -0,0 +1,83 @@
import 'dart:convert';
import 'package:contacts_plus_plus/api_client.dart';
import 'package:contacts_plus_plus/auxiliary.dart';
import 'package:contacts_plus_plus/models/message.dart';
import 'package:contacts_plus_plus/models/session.dart';
import 'package:contacts_plus_plus/widgets/generic_avatar.dart';
import 'package:contacts_plus_plus/widgets/messages.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
class MessageSessionInvite extends StatelessWidget {
MessageSessionInvite({required this.message, super.key});
final DateFormat _dateFormat = DateFormat.Hm();
final Message message;
@override
Widget build(BuildContext context) {
final sessionInfo = Session.fromMap(jsonDecode(message.content));
return TextButton(
onPressed: () {
showDialog(context: context, builder: (context) => SessionPopup(session: sessionInfo));
},
style: TextButton.styleFrom(padding: EdgeInsets.zero),
child: Container(
padding: const EdgeInsets.only(left: 10),
constraints: const BoxConstraints(maxWidth: 300),
child: Column(
children: [
Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(sessionInfo.name, maxLines: null, softWrap: true, style: Theme.of(context).textTheme.titleMedium,),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Column(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
children: [
GenericAvatar(
imageUri: Aux.neosDbToHttp(Aux.neosDbToHttp(sessionInfo.thumbnail)),
placeholderIcon: Icons.no_photography,
),
const SizedBox(height: 4,),
Text("${sessionInfo.sessionUsers.length}/${sessionInfo.maxUsers}")
],
),
),
],
),
const SizedBox(height: 8,),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(child: Text("Hosted by ${sessionInfo.hostUsername}", overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.bodySmall?.copyWith(color: Colors.white60),)),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 4.0),
child: Text(
_dateFormat.format(message.sendTime),
style: Theme
.of(context)
.textTheme
.labelMedium
?.copyWith(color: Colors.white54),
),
),
const SizedBox(width: 4,),
if (message.senderId == ClientHolder.of(context).client.userId) Padding(
padding: const EdgeInsets.only(right: 12.0),
child: MessageStateIndicator(messageState: message.state),
),
],
)
],
),
),
);
}
}

View file

@ -1,4 +1,3 @@
import 'dart:convert';
import 'dart:developer'; import 'dart:developer';
import 'package:cached_network_image/cached_network_image.dart'; import 'package:cached_network_image/cached_network_image.dart';
@ -7,8 +6,9 @@ import 'package:contacts_plus_plus/auxiliary.dart';
import 'package:contacts_plus_plus/models/friend.dart'; import 'package:contacts_plus_plus/models/friend.dart';
import 'package:contacts_plus_plus/models/message.dart'; import 'package:contacts_plus_plus/models/message.dart';
import 'package:contacts_plus_plus/models/session.dart'; import 'package:contacts_plus_plus/models/session.dart';
import 'package:contacts_plus_plus/widgets/audio_clip_player.dart'; import 'package:contacts_plus_plus/widgets/message_audio_player.dart';
import 'package:contacts_plus_plus/widgets/generic_avatar.dart'; import 'package:contacts_plus_plus/widgets/generic_avatar.dart';
import 'package:contacts_plus_plus/widgets/message_session_invite.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
@ -338,8 +338,26 @@ class MyMessageBubble extends StatelessWidget {
var content = message.content; var content = message.content;
switch (message.type) { switch (message.type) {
case MessageType.sessionInvite: case MessageType.sessionInvite:
content = "[Session Invite]"; return Row(
continue rawText; mainAxisAlignment: MainAxisAlignment.end,
mainAxisSize: MainAxisSize.min,
children: [
Card(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
color: Theme
.of(context)
.colorScheme
.primaryContainer,
margin: const EdgeInsets.only(left: 32, bottom: 16, right: 12),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 12),
child: MessageSessionInvite(message: message,),
),
),
],
);
case MessageType.object: case MessageType.object:
content = "[Asset]"; content = "[Asset]";
continue rawText; continue rawText;
@ -435,17 +453,28 @@ class OtherMessageBubble extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var content = message.content; var content = message.content;
if (message.type == MessageType.sessionInvite) {
content = "[Session Invite]";
} else if (message.type == MessageType.sound) {
content = "[Voice Message]";
} else if (message.type == MessageType.object) {
content = "[Asset]";
}
switch (message.type) { switch (message.type) {
case MessageType.sessionInvite: case MessageType.sessionInvite:
content = "[Session Invite]"; return Row(
continue rawText; mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
children: [
Card(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
color: Theme
.of(context)
.colorScheme
.secondaryContainer,
margin: const EdgeInsets.only(right: 32, bottom: 16, left: 12),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: MessageSessionInvite(message: message,),
),
),
],
);
case MessageType.object: case MessageType.object:
content = "[Asset]"; content = "[Asset]";
continue rawText; continue rawText;
@ -500,7 +529,8 @@ class OtherMessageBubble extends StatelessWidget {
return Row( return Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
children: [Card( children: [
Card(
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
), ),
@ -547,6 +577,110 @@ class MessageStateIndicator extends StatelessWidget {
} }
} }
class SessionPopup extends StatelessWidget {
const SessionPopup({required this.session, super.key});
final Session session;
@override
Widget build(BuildContext context) {
final ScrollController userListScrollController = ScrollController();
final thumbnailUri = Aux.neosDbToHttp(session.thumbnail);
return Dialog(
insetPadding: const EdgeInsets.all(32),
child: Container(
constraints: const BoxConstraints(maxHeight: 400),
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Expanded(
child: Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: ListView(
children: [
Text(session.name, style: Theme.of(context).textTheme.titleMedium),
Text(session.description.isEmpty ? "No description." : session.description, style: Theme.of(context).textTheme.labelMedium),
Text("Tags: ${session.tags.isEmpty ? "None" : session.tags.join(", ")}",
style: Theme.of(context).textTheme.labelMedium,
softWrap: true,
),
Text("Users: ${session.sessionUsers.length}", style: Theme.of(context).textTheme.labelMedium),
Text("Maximum users: ${session.maxUsers}", style: Theme.of(context).textTheme.labelMedium),
Text("Headless: ${session.headlessHost ? "Yes" : "No"}", style: Theme.of(context).textTheme.labelMedium),
],
),
),
if (session.sessionUsers.isNotEmpty) Expanded(
child: Scrollbar(
trackVisibility: true,
controller: userListScrollController,
thumbVisibility: true,
child: ListView.builder(
controller: userListScrollController,
shrinkWrap: true,
itemCount: session.sessionUsers.length,
itemBuilder: (context, index) {
final user = session.sessionUsers[index];
return ListTile(
dense: true,
title: Text(user.username, textAlign: TextAlign.end,),
subtitle: Text(user.isPresent ? "Active" : "Inactive", textAlign: TextAlign.end,),
);
},
),
),
) else Expanded(
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: const [
Icon(Icons.person_remove_alt_1_rounded),
Padding(
padding: EdgeInsets.all(16.0),
child: Text("No one is currently playing.", textAlign: TextAlign.center,),
)
],
),
),
),
],
),
),
Expanded(
child: Center(
child: CachedNetworkImage(
imageUrl: thumbnailUri,
placeholder: (context, url) {
return const CircularProgressIndicator();
},
errorWidget: (context, error, what) => Column(
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: const [
Icon(Icons.no_photography),
Padding(
padding: EdgeInsets.all(16.0),
child: Text("Failed to load Image"),
)
],
),
),
),
)
],
),
),
);
}
}
class SessionTile extends StatelessWidget { class SessionTile extends StatelessWidget {
const SessionTile({required this.session, super.key}); const SessionTile({required this.session, super.key});
final Session session; final Session session;
@ -555,101 +689,7 @@ class SessionTile extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return TextButton( return TextButton(
onPressed: () { onPressed: () {
showDialog(context: context, builder: (context) { showDialog(context: context, builder: (context) => SessionPopup(session: session));
final ScrollController userListScrollController = ScrollController();
final thumbnailUri = Aux.neosDbToHttp(session.thumbnail);
return Dialog(
insetPadding: const EdgeInsets.all(32),
child: Container(
constraints: const BoxConstraints(maxHeight: 400),
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Expanded(
child: Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: ListView(
children: [
Text(session.name, style: Theme.of(context).textTheme.titleMedium),
Text(session.description.isEmpty ? "No description." : session.description, style: Theme.of(context).textTheme.labelMedium),
Text("Tags: ${session.tags.isEmpty ? "None" : session.tags.join(", ")}",
style: Theme.of(context).textTheme.labelMedium,
softWrap: true,
),
Text("Users: ${session.sessionUsers.length}", style: Theme.of(context).textTheme.labelMedium),
Text("Maximum users: ${session.maxUsers}", style: Theme.of(context).textTheme.labelMedium),
Text("Headless: ${session.headlessHost ? "Yes" : "No"}", style: Theme.of(context).textTheme.labelMedium),
],
),
),
if (session.sessionUsers.isNotEmpty) Expanded(
child: Scrollbar(
trackVisibility: true,
controller: userListScrollController,
thumbVisibility: true,
child: ListView.builder(
controller: userListScrollController,
shrinkWrap: true,
itemCount: session.sessionUsers.length,
itemBuilder: (context, index) {
final user = session.sessionUsers[index];
return ListTile(
dense: true,
title: Text(user.username, textAlign: TextAlign.end,),
subtitle: Text(user.isPresent ? "Active" : "Inactive", textAlign: TextAlign.end,),
);
},
),
),
) else Expanded(
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: const [
Icon(Icons.person_remove_alt_1_rounded),
Padding(
padding: EdgeInsets.all(16.0),
child: Text("No one is currently playing.", textAlign: TextAlign.center,),
)
],
),
),
),
],
),
),
Expanded(
child: Center(
child: thumbnailUri.isEmpty ? const Text("No Image") : CachedNetworkImage(
imageUrl: thumbnailUri,
placeholder: (context, url) {
return const CircularProgressIndicator();
},
errorWidget: (context, error, what) => Column(
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: const [
Icon(Icons.no_photography),
Padding(
padding: EdgeInsets.all(16.0),
child: Text("Failed to load Image"),
)
],
),
),
),
)
],
),
),
);
});
}, },
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,