Add session invites to message view
This commit is contained in:
parent
158fccbf3f
commit
38205fe8e1
5 changed files with 241 additions and 115 deletions
|
@ -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 {
|
||||||
|
|
|
@ -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"] ?? "",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
83
lib/widgets/message_session_invite.dart
Normal file
83
lib/widgets/message_session_invite.dart
Normal 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),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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,
|
||||||
|
|
Loading…
Reference in a new issue