From 1b7af5f4a7556551b7560463b9cde72b6b4acdbf Mon Sep 17 00:00:00 2001 From: Nutcake Date: Sun, 21 May 2023 14:23:53 +0200 Subject: [PATCH] Minor message view improvements --- lib/main.dart | 1 - lib/models/message.dart | 4 +- lib/widgets/friends/friends_list.dart | 2 - .../messages/message_attachment_list.dart | 310 +++++++++++++----- .../messages/message_audio_player.dart | 61 ++-- .../messages/message_record_button.dart | 12 +- lib/widgets/messages/messages_list.dart | 19 +- pubspec.lock | 2 +- pubspec.yaml | 1 + 9 files changed, 278 insertions(+), 134 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 7563673..81b5a0b 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,5 +1,4 @@ import 'dart:developer'; -import 'dart:io' show Platform; import 'package:contacts_plus_plus/apis/github_api.dart'; import 'package:contacts_plus_plus/client_holder.dart'; diff --git a/lib/models/message.dart b/lib/models/message.dart index 63768e8..ad06399 100644 --- a/lib/models/message.dart +++ b/lib/models/message.dart @@ -49,7 +49,7 @@ class Message implements Comparable { final MessageState state; Message({required this.id, required this.recipientId, required this.senderId, required this.type, - required this.content, required DateTime sendTime, this.state=MessageState.local}) + required this.content, required DateTime sendTime, required this.state}) : formattedContent = FormatNode.fromText(content), sendTime = sendTime.toUtc(); factory Message.fromMap(Map map, {MessageState? withState}) { @@ -65,7 +65,7 @@ class Message implements Comparable { type: type, content: map["content"], sendTime: DateTime.parse(map["sendTime"]), - state: withState ?? (map["readTime"] != null ? MessageState.read : MessageState.local) + state: withState ?? (map["readTime"] != null ? MessageState.read : MessageState.sent) ); } diff --git a/lib/widgets/friends/friends_list.dart b/lib/widgets/friends/friends_list.dart index 7af63df..86fbff7 100644 --- a/lib/widgets/friends/friends_list.dart +++ b/lib/widgets/friends/friends_list.dart @@ -4,7 +4,6 @@ import 'package:contacts_plus_plus/apis/user_api.dart'; import 'package:contacts_plus_plus/client_holder.dart'; import 'package:contacts_plus_plus/clients/messaging_client.dart'; import 'package:contacts_plus_plus/models/friend.dart'; -import 'package:contacts_plus_plus/models/personal_profile.dart'; import 'package:contacts_plus_plus/widgets/default_error_widget.dart'; import 'package:contacts_plus_plus/widgets/friends/expanding_input_fab.dart'; import 'package:contacts_plus_plus/widgets/friends/friend_list_tile.dart'; @@ -42,7 +41,6 @@ class _FriendsListState extends State { final clientHolder = ClientHolder.of(context); if (_clientHolder != clientHolder) { _clientHolder = clientHolder; - final apiClient = _clientHolder!.apiClient; _refreshUserStatus(); } } diff --git a/lib/widgets/messages/message_attachment_list.dart b/lib/widgets/messages/message_attachment_list.dart index 1d84243..70f59d4 100644 --- a/lib/widgets/messages/message_attachment_list.dart +++ b/lib/widgets/messages/message_attachment_list.dart @@ -53,8 +53,12 @@ class _MessageAttachmentListState extends State { return LinearGradient( begin: Alignment.centerLeft, end: Alignment.centerRight, - colors: [Colors.transparent, Colors.transparent, Colors.transparent, Theme.of(context).colorScheme.background], - stops: [0.0, 0.0, _showShadow ? 0.96 : 1.0, 1.0], // 10% purple, 80% transparent, 10% purple + colors: [Colors.transparent, Colors.transparent, Colors.transparent, Theme + .of(context) + .colorScheme + .background + ], + stops: [0.0, 0.0, _showShadow ? 0.90 : 1.0, 1.0], // 10% purple, 80% transparent, 10% purple ).createShader(bounds); }, blendMode: BlendMode.dstOut, @@ -66,50 +70,50 @@ class _MessageAttachmentListState extends State { Padding( padding: const EdgeInsets.only(left: 4.0, right: 4.0, top: 4.0), child: TextButton.icon( - onPressed: widget.disabled ? null : () { - showDialog(context: context, builder: (context) => - AlertDialog( - title: const Text("Remove attachment"), - content: Text( - "This will remove attachment '${basename( - file.$2.path)}', are you sure?"), - actions: [ - TextButton( - onPressed: () { - Navigator.of(context).pop(); - }, - child: const Text("No"), - ), - TextButton( - onPressed: () async { - Navigator.of(context).pop(); - _loadedFiles.remove(file); - await widget.onChange(_loadedFiles); - }, - child: const Text("Yes"), - ) - ], - ), - ); - }, - style: TextButton.styleFrom( - foregroundColor: Theme - .of(context) - .colorScheme - .onBackground, - side: BorderSide( - color: Theme - .of(context) - .colorScheme - .primary, - width: 1 + onPressed: widget.disabled ? null : () { + showDialog(context: context, builder: (context) => + AlertDialog( + title: const Text("Remove attachment"), + content: Text( + "This will remove attachment '${basename( + file.$2.path)}', are you sure?"), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: const Text("No"), + ), + TextButton( + onPressed: () async { + Navigator.of(context).pop(); + _loadedFiles.remove(file); + await widget.onChange(_loadedFiles); + }, + child: const Text("Yes"), + ) + ], + ), + ); + }, + style: TextButton.styleFrom( + foregroundColor: Theme + .of(context) + .colorScheme + .onBackground, + side: BorderSide( + color: Theme + .of(context) + .colorScheme + .primary, + width: 1 + ), ), - ), - label: Text(basename(file.$2.path)), - icon: switch (file.$1) { - FileType.image => const Icon(Icons.image), - _ => const Icon(Icons.attach_file) - } + label: Text(basename(file.$2.path)), + icon: switch (file.$1) { + FileType.image => const Icon(Icons.image), + _ => const Icon(Icons.attach_file) + } ), ), ).toList() @@ -117,43 +121,189 @@ class _MessageAttachmentListState extends State { ), ), ), - IconButton( - onPressed: widget.disabled ? null : () async { - final result = await FilePicker.platform.pickFiles(type: FileType.image, allowMultiple: true); - if (result != null) { - setState(() { - _loadedFiles.addAll( - result.files.map((e) => e.path != null ? (FileType.image, File(e.path!)) : null) - .whereNotNull()); - }); - } - }, - icon: const Icon(Icons.add_photo_alternate), - ), - IconButton( - onPressed: widget.disabled ? null : () async { - final picture = await Navigator.of(context).push( - MaterialPageRoute(builder: (context) => const MessageCameraView())); - if (picture != null) { - _loadedFiles.add(picture); - await widget.onChange(_loadedFiles); - } - }, - icon: const Icon(Icons.add_a_photo), - ), - IconButton( - onPressed: widget.disabled ? null : () async { - final result = await FilePicker.platform.pickFiles(type: FileType.any, allowMultiple: true); - if (result != null) { - setState(() { - _loadedFiles.addAll( - result.files.map((e) => e.path != null ? (FileType.any, File(e.path!)) : null).whereNotNull()); - }); - } - }, - icon: const Icon(Icons.file_present_rounded), + PopupMenuButton( + offset: const Offset(0, -64), + constraints: const BoxConstraints.tightFor(width: 48 * 3, height: 64), + shadowColor: Colors.transparent, + position: PopupMenuPosition.over, + color: Colors.transparent, + enableFeedback: true, + padding: EdgeInsets.zero, + surfaceTintColor: Colors.transparent, + iconSize: 24, + itemBuilder: (context) => + [ + PopupMenuItem( + padding: EdgeInsets.zero, + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + IconButton( + iconSize: 24, + style: IconButton.styleFrom( + backgroundColor: Theme + .of(context) + .colorScheme + .surface, + foregroundColor: Theme + .of(context) + .colorScheme + .onSurface, + side: BorderSide( + width: 1, + color: Theme + .of(context) + .colorScheme + .secondary, + ) + ), + padding: EdgeInsets.zero, + onPressed: () async { + final result = await FilePicker.platform.pickFiles(type: FileType.image, allowMultiple: true); + if (result != null) { + setState(() { + _loadedFiles.addAll( + result.files.map((e) => e.path != null ? (FileType.image, File(e.path!)) : null) + .whereNotNull()); + }); + } + if (context.mounted) { + Navigator.of(context).pop(); + } + }, + icon: const Icon(Icons.image,), + ), + IconButton( + iconSize: 24, + style: IconButton.styleFrom( + backgroundColor: Theme + .of(context) + .colorScheme + .surface, + foregroundColor: Theme + .of(context) + .colorScheme + .onSurface, + side: BorderSide( + width: 1, + color: Theme + .of(context) + .colorScheme + .secondary, + ) + ), + padding: EdgeInsets.zero, + onPressed: () async { + final picture = await Navigator.of(context).push( + MaterialPageRoute(builder: (context) => const MessageCameraView())) as File?; + if (picture != null) { + _loadedFiles.add((FileType.image, picture)); + await widget.onChange(_loadedFiles); + } + if (context.mounted) { + Navigator.of(context).pop(); + } + }, + icon: const Icon(Icons.camera,), + ), + IconButton( + iconSize: 24, + style: IconButton.styleFrom( + backgroundColor: Theme + .of(context) + .colorScheme + .surface, + foregroundColor: Theme + .of(context) + .colorScheme + .onSurface, + side: BorderSide( + width: 1, + color: Theme + .of(context) + .colorScheme + .secondary, + ) + ), + padding: EdgeInsets.zero, + onPressed: () async { + final result = await FilePicker.platform.pickFiles(type: FileType.any, allowMultiple: true); + if (result != null) { + setState(() { + _loadedFiles.addAll( + result.files.map((e) => e.path != null ? (FileType.any, File(e.path!)) : null) + .whereNotNull()); + }); + } + if (context.mounted) { + Navigator.of(context).pop(); + } + }, + icon: const Icon(Icons.file_present_rounded,), + ), + ], + ), + ), + ], + icon: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(64), + border: Border.all( + color: Theme + .of(context) + .colorScheme + .primary, + ), + ), + child: Icon( + Icons.add, + color: Theme.of(context).colorScheme.onSurface, + ), + ), ), ], ); } +} + +enum DocumentType { + gallery, + camera, + rawFile; +} + +class PopupMenuIcon extends PopupMenuEntry { + const PopupMenuIcon({this.radius=24, this.value, required this.icon, this.onPressed, super.key}); + + final T? value; + final double radius; + final Widget icon; + final void Function()? onPressed; + + @override + State createState() => _PopupMenuIconState(); + + @override + double get height => radius; + + @override + bool represents(T? value) => this.value == value; + +} + +class _PopupMenuIconState extends State { + @override + Widget build(BuildContext context) { + return ClipRRect( + borderRadius: BorderRadius.circular(128), + child: Container( + color: Theme.of(context).colorScheme.surface, + padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 2), + margin: const EdgeInsets.all(1), + child: InkWell( + child: widget.icon, + ), + ), + ); + } } \ No newline at end of file diff --git a/lib/widgets/messages/message_audio_player.dart b/lib/widgets/messages/message_audio_player.dart index d5c3054..445b51d 100644 --- a/lib/widgets/messages/message_audio_player.dart +++ b/lib/widgets/messages/message_audio_player.dart @@ -88,8 +88,26 @@ class _MessageAudioPlayerState extends State with WidgetsBin return FutureBuilder( future: _audioFileFuture, builder: (context, snapshot) { - if (snapshot.hasData) { - return IntrinsicWidth( + if (snapshot.hasError) { + return SizedBox( + width: 300, + child: Row( + children: [ + const Icon(Icons.volume_off), + const SizedBox(width: 8,), + Expanded( + child: Text( + "Failed to load voice message: ${snapshot.error}", + maxLines: 4, + overflow: TextOverflow.ellipsis, + softWrap: true, + ), + ), + ], + ), + ); + } + return IntrinsicWidth( child: StreamBuilder( stream: _audioPlayer.playerStateStream, builder: (context, snapshot) { @@ -103,7 +121,7 @@ class _MessageAudioPlayerState extends State with WidgetsBin mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - IconButton( + snapshot.hasData ? IconButton( onPressed: () { switch (playerState.processingState) { case ProcessingState.idle: @@ -124,16 +142,15 @@ class _MessageAudioPlayerState extends State with WidgetsBin } }, color: widget.foregroundColor, - icon: SizedBox( - width: 24, - height: 24, + icon: SizedBox.square( + dimension: 24, child: playerState.processingState == ProcessingState.loading ? const Center(child: CircularProgressIndicator(),) : Icon(((_audioPlayer.duration ?? Duration.zero) - _audioPlayer.position).inMilliseconds < 10 ? Icons.replay : (playerState.playing ? Icons.pause : Icons.play_arrow)), ), - ), + ) : const SizedBox.square(dimension: 24, child: CircularProgressIndicator(),), StreamBuilder( stream: _audioPlayer.positionStream, builder: (context, snapshot) { @@ -200,36 +217,6 @@ class _MessageAudioPlayerState extends State with WidgetsBin } ), ); - } else if (snapshot.hasError) { - return SizedBox( - width: 300, - child: Row( - children: [ - const Icon(Icons.volume_off), - const SizedBox(width: 8,), - Expanded( - child: Text( - "Failed to load voice message: ${snapshot.error}", - maxLines: 4, - overflow: TextOverflow.ellipsis, - softWrap: true, - ), - ), - ], - ), - ); - } else { - return const Padding( - padding: EdgeInsets.all(8.0), - child: Row( - children: [ - Icon(Icons.volume_up), - SizedBox(width: 8,), - Center(child: CircularProgressIndicator()), - ], - ), - ); - } } ); } diff --git a/lib/widgets/messages/message_record_button.dart b/lib/widgets/messages/message_record_button.dart index c636882..a048ecf 100644 --- a/lib/widgets/messages/message_record_button.dart +++ b/lib/widgets/messages/message_record_button.dart @@ -34,6 +34,7 @@ class _MessageRecordButtonState extends State { child: GestureDetector( onTapDown: widget.disabled ? null : (_) async { HapticFeedback.vibrate(); + /* widget.onRecordStart?.call(); final dir = await getTemporaryDirectory(); await _recorder.start( @@ -41,16 +42,19 @@ class _MessageRecordButtonState extends State { encoder: AudioEncoder.opus, samplingRate: 44100, ); + */ }, - onTapUp: (_) async { + onLongPressUp: () async { + /* if (await _recorder.isRecording()) { final recording = await _recorder.stop(); widget.onRecordEnd?.call(recording == null ? null : File(recording)); } + */ }, - child: const Padding( - padding: EdgeInsets.all(8.0), - child: Icon(Icons.mic_outlined), + child: Padding( + padding: const EdgeInsets.all(8), + child: Icon(Icons.mic_outlined, size: 28, color: Theme.of(context).colorScheme.onSurface,), ), ), ); diff --git a/lib/widgets/messages/messages_list.dart b/lib/widgets/messages/messages_list.dart index 74e7f45..747b4d9 100644 --- a/lib/widgets/messages/messages_list.dart +++ b/lib/widgets/messages/messages_list.dart @@ -98,6 +98,7 @@ class _MessagesListState extends State with SingleTickerProviderSt type: MessageType.text, content: content, sendTime: DateTime.now().toUtc(), + state: MessageState.local, ); mClient.sendMessage(message); _messageTextController.clear(); @@ -119,6 +120,7 @@ class _MessagesListState extends State with SingleTickerProviderSt type: MessageType.object, content: jsonEncode(record.toMap()), sendTime: DateTime.now().toUtc(), + state: MessageState.local ); mClient.sendMessage(message); _messageTextController.clear(); @@ -140,6 +142,7 @@ class _MessagesListState extends State with SingleTickerProviderSt type: MessageType.sound, content: jsonEncode(record.toMap()), sendTime: DateTime.now().toUtc(), + state: MessageState.local, ); mClient.sendMessage(message); _messageTextController.clear(); @@ -161,6 +164,7 @@ class _MessagesListState extends State with SingleTickerProviderSt type: MessageType.object, content: jsonEncode(record.toMap()), sendTime: DateTime.now().toUtc(), + state: MessageState.local, ); mClient.sendMessage(message); _messageTextController.clear(); @@ -195,7 +199,8 @@ class _MessagesListState extends State with SingleTickerProviderSt .of(context) .colorScheme .onSecondaryContainer - .withAlpha(150),), + .withAlpha(150), + ), ), ], ), @@ -452,7 +457,7 @@ class _MessagesListState extends State with SingleTickerProviderSt _attachmentPickerOpen = true; }); }, - icon: const Icon(Icons.attach_file), + icon: const Icon(Icons.attach_file, size: 28,), ) : IconButton( key: const ValueKey("remove-attachment-icon"), @@ -487,12 +492,12 @@ class _MessagesListState extends State with SingleTickerProviderSt }); } }, - icon: const Icon(Icons.close), + icon: const Icon(Icons.close, size: 28,), ), ), Expanded( child: Padding( - padding: const EdgeInsets.all(8), + padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 4), child: TextField( enabled: cache != null && cache.error == null && !_isSending, autocorrect: true, @@ -518,13 +523,13 @@ class _MessagesListState extends State with SingleTickerProviderSt contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), border: OutlineInputBorder( borderRadius: BorderRadius.circular(24) - ) + ), ), ), ), ), Padding( - padding: const EdgeInsets.only(left: 8, right: 4.0), + padding: const EdgeInsets.all(4), child: AnimatedSwitcher( duration: const Duration(milliseconds: 200), transitionBuilder: (Widget child, Animation animation) => @@ -533,6 +538,7 @@ class _MessagesListState extends State with SingleTickerProviderSt child: _hasText || _loadedFiles.isNotEmpty ? IconButton( key: const ValueKey("send-button"), splashRadius: 24, + padding: EdgeInsets.zero, onPressed: _isSending ? null : () async { final sMsgnr = ScaffoldMessenger.of(context); final settings = ClientHolder @@ -584,7 +590,6 @@ class _MessagesListState extends State with SingleTickerProviderSt _sendProgress = null; }); }, - iconSize: 28, icon: const Icon(Icons.send), ) : MessageRecordButton( key: const ValueKey("mic-button"), diff --git a/pubspec.lock b/pubspec.lock index 72ba257..d173443 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -138,7 +138,7 @@ packages: source: hosted version: "0.3.3+4" crypto: - dependency: transitive + dependency: "direct main" description: name: crypto sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab diff --git a/pubspec.yaml b/pubspec.yaml index 9a75ed4..a17e11c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -60,6 +60,7 @@ dependencies: record: ^4.4.4 camera: ^0.10.5 path_provider: ^2.0.15 + crypto: ^3.0.3 dev_dependencies: flutter_test: