diff --git a/lib/auxiliary.dart b/lib/auxiliary.dart index c7143d1..6d1f223 100644 --- a/lib/auxiliary.dart +++ b/lib/auxiliary.dart @@ -73,6 +73,8 @@ extension Strip on String { return htmlparser.parse(document.body?.text).documentElement?.text ?? ""; } + // This won't be accurate since userIds can't contain certain characters that usernames can + // but it's fine for just having a name to display String stripUid() => startsWith("U-") ? substring(2) : this; } diff --git a/lib/models/photo_asset.dart b/lib/models/photo_asset.dart new file mode 100644 index 0000000..7b96e88 --- /dev/null +++ b/lib/models/photo_asset.dart @@ -0,0 +1,34 @@ + +import 'dart:math'; + +import 'package:collection/collection.dart'; + +class PhotoAsset { + final String locationName; + final List userIds; + final DateTime timestamp; + final String imageUri; + + PhotoAsset({required this.locationName, required this.userIds, required this.timestamp, required this.imageUri}); + + factory PhotoAsset.fromTags(List tags) { + final List userIds = []; + Map parsedTags = Map.fromEntries(tags.map((e) { + final delimIdx = e.indexOf(":"); + if (delimIdx == -1) return null; + final key = e.substring(0, delimIdx); + final value = e.substring(min(delimIdx+1, e.length)); + if (key == "user") { + userIds.add(value); + return null; + } + return MapEntry(key, value); + }).whereNotNull()); + return PhotoAsset( + locationName: parsedTags["location_name"]!, + userIds: userIds, + timestamp: DateTime.parse(parsedTags["timestamp"]!), + imageUri: parsedTags["texture_asset"]!, + ); + } +} \ No newline at end of file diff --git a/lib/widgets/messages/message_asset.dart b/lib/widgets/messages/message_asset.dart new file mode 100644 index 0000000..a3e05cc --- /dev/null +++ b/lib/widgets/messages/message_asset.dart @@ -0,0 +1,89 @@ + +import 'dart:convert'; + +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:contacts_plus_plus/auxiliary.dart'; +import 'package:contacts_plus_plus/client_holder.dart'; +import 'package:contacts_plus_plus/models/photo_asset.dart'; +import 'package:contacts_plus_plus/models/message.dart'; +import 'package:contacts_plus_plus/widgets/messages/message_state_indicator.dart'; +import 'package:flutter/material.dart'; +import 'package:full_screen_image/full_screen_image.dart'; +import 'package:intl/intl.dart'; +import 'package:photo_view/photo_view.dart'; + +class MessageAsset extends StatelessWidget { + MessageAsset({required this.message, super.key}); + + final Message message; + final DateFormat _dateFormat = DateFormat.Hm(); + + + @override + Widget build(BuildContext context) { + final content = jsonDecode(message.content); + PhotoAsset? photoAsset; + try { + photoAsset = PhotoAsset.fromTags((content["tags"] as List).map((e) => "$e").toList()); + } catch (_) {} + + return Container( + constraints: const BoxConstraints(maxWidth: 300), + padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8), + child: Column( + children: [ + CachedNetworkImage( + imageUrl: Aux.neosDbToHttp(content["thumbnailUri"]), + imageBuilder: (context, image) { + return InkWell( + onTap: () async { + await Navigator.push( + context, MaterialPageRoute(builder: (context) => + PhotoView( + minScale: PhotoViewComputedScale.contained, + imageProvider: photoAsset == null + ? image + : CachedNetworkImageProvider(Aux.neosDbToHttp(photoAsset.imageUri)), + heroAttributes: PhotoViewHeroAttributes(tag: message.id), + ), + ),); + }, + child: Hero( + tag: message.id, + child: ClipRRect(borderRadius: BorderRadius.circular(16), child: Image(image: image,)), + ), + ); + }, + placeholder: (context, uri) => const CircularProgressIndicator(), + ), + const SizedBox(height: 8,), + Row( + crossAxisAlignment: CrossAxisAlignment.end, + mainAxisSize: MainAxisSize.min, + children: [ + Expanded(child: Text("${content["name"]}", maxLines: null, 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), + ), + ), + if (message.senderId == ClientHolder + .of(context) + .apiClient + .userId) Padding( + padding: const EdgeInsets.only(left: 4.0), + child: MessageStateIndicator(messageState: message.state), + ), + ], + ), + ], + ), + ); + } +} \ No newline at end of file diff --git a/lib/widgets/messages/message_bubble.dart b/lib/widgets/messages/message_bubble.dart index 0869a67..d8dd2f9 100644 --- a/lib/widgets/messages/message_bubble.dart +++ b/lib/widgets/messages/message_bubble.dart @@ -1,5 +1,5 @@ - import 'package:contacts_plus_plus/models/message.dart'; +import 'package:contacts_plus_plus/widgets/messages/message_asset.dart'; import 'package:contacts_plus_plus/widgets/messages/message_audio_player.dart'; import 'package:contacts_plus_plus/widgets/messages/message_session_invite.dart'; import 'package:contacts_plus_plus/widgets/messages/message_state_indicator.dart'; @@ -14,7 +14,6 @@ class MyMessageBubble extends StatelessWidget { @override Widget build(BuildContext context) { - var content = message.content; switch (message.type) { case MessageType.sessionInvite: return Row( @@ -38,10 +37,28 @@ class MyMessageBubble extends StatelessWidget { ], ); case MessageType.object: - content = "[Asset]"; - continue rawText; + return Row( + 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: Container( + constraints: const BoxConstraints(maxWidth: 300), + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 12), + child: MessageAsset(message: message,), + ), + ), + ], + ); case MessageType.unknown: - rawText: case MessageType.text: return Row( mainAxisAlignment: MainAxisAlignment.end, @@ -63,7 +80,7 @@ class MyMessageBubble extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.end, children: [ Text( - content, + message.content, softWrap: true, maxLines: null, style: Theme @@ -155,8 +172,26 @@ class OtherMessageBubble extends StatelessWidget { ], ); case MessageType.object: - content = "[Asset]"; - continue rawText; + return Row( + 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: MessageAsset(message: message,), + ), + ), + ], + ); case MessageType.unknown: rawText: case MessageType.text: diff --git a/pubspec.lock b/pubspec.lock index d84fb34..20484d3 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -256,6 +256,14 @@ packages: description: flutter source: sdk version: "0.0.0" + full_screen_image: + dependency: "direct main" + description: + name: full_screen_image + sha256: "97831b00d07b17c674ba3a4eddcee306dbd0d7c66851f1b67856e12daeb9b3aa" + url: "https://pub.dev" + source: hosted + version: "2.0.0" html: dependency: "direct main" description: @@ -472,6 +480,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.1.0" + photo_view: + dependency: "direct main" + description: + name: photo_view + sha256: "8036802a00bae2a78fc197af8a158e3e2f7b500561ed23b4c458107685e645bb" + url: "https://pub.dev" + source: hosted + version: "0.14.0" platform: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 1e79b97..11ad770 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 1.0.2+1 +version: 1.1.0+1 environment: sdk: '>=2.19.6 <3.0.0' @@ -53,6 +53,8 @@ dependencies: collection: ^1.17.0 package_info_plus: ^3.1.2 provider: ^6.0.5 + full_screen_image: ^2.0.0 + photo_view: ^0.14.0 dev_dependencies: flutter_test: