Minor message view improvements

This commit is contained in:
Nutcake 2023-05-21 14:23:53 +02:00
parent c12748de6c
commit 1b7af5f4a7
9 changed files with 278 additions and 134 deletions

View file

@ -1,5 +1,4 @@
import 'dart:developer'; import 'dart:developer';
import 'dart:io' show Platform;
import 'package:contacts_plus_plus/apis/github_api.dart'; import 'package:contacts_plus_plus/apis/github_api.dart';
import 'package:contacts_plus_plus/client_holder.dart'; import 'package:contacts_plus_plus/client_holder.dart';

View file

@ -49,7 +49,7 @@ class Message implements Comparable {
final MessageState state; final MessageState state;
Message({required this.id, required this.recipientId, required this.senderId, required this.type, 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(); : formattedContent = FormatNode.fromText(content), sendTime = sendTime.toUtc();
factory Message.fromMap(Map map, {MessageState? withState}) { factory Message.fromMap(Map map, {MessageState? withState}) {
@ -65,7 +65,7 @@ class Message implements Comparable {
type: type, type: type,
content: map["content"], content: map["content"],
sendTime: DateTime.parse(map["sendTime"]), sendTime: DateTime.parse(map["sendTime"]),
state: withState ?? (map["readTime"] != null ? MessageState.read : MessageState.local) state: withState ?? (map["readTime"] != null ? MessageState.read : MessageState.sent)
); );
} }

View file

@ -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/client_holder.dart';
import 'package:contacts_plus_plus/clients/messaging_client.dart'; import 'package:contacts_plus_plus/clients/messaging_client.dart';
import 'package:contacts_plus_plus/models/friend.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/default_error_widget.dart';
import 'package:contacts_plus_plus/widgets/friends/expanding_input_fab.dart'; import 'package:contacts_plus_plus/widgets/friends/expanding_input_fab.dart';
import 'package:contacts_plus_plus/widgets/friends/friend_list_tile.dart'; import 'package:contacts_plus_plus/widgets/friends/friend_list_tile.dart';
@ -42,7 +41,6 @@ class _FriendsListState extends State<FriendsList> {
final clientHolder = ClientHolder.of(context); final clientHolder = ClientHolder.of(context);
if (_clientHolder != clientHolder) { if (_clientHolder != clientHolder) {
_clientHolder = clientHolder; _clientHolder = clientHolder;
final apiClient = _clientHolder!.apiClient;
_refreshUserStatus(); _refreshUserStatus();
} }
} }

View file

@ -53,8 +53,12 @@ class _MessageAttachmentListState extends State<MessageAttachmentList> {
return LinearGradient( return LinearGradient(
begin: Alignment.centerLeft, begin: Alignment.centerLeft,
end: Alignment.centerRight, end: Alignment.centerRight,
colors: [Colors.transparent, Colors.transparent, Colors.transparent, Theme.of(context).colorScheme.background], colors: [Colors.transparent, Colors.transparent, Colors.transparent, Theme
stops: [0.0, 0.0, _showShadow ? 0.96 : 1.0, 1.0], // 10% purple, 80% transparent, 10% purple .of(context)
.colorScheme
.background
],
stops: [0.0, 0.0, _showShadow ? 0.90 : 1.0, 1.0], // 10% purple, 80% transparent, 10% purple
).createShader(bounds); ).createShader(bounds);
}, },
blendMode: BlendMode.dstOut, blendMode: BlendMode.dstOut,
@ -66,50 +70,50 @@ class _MessageAttachmentListState extends State<MessageAttachmentList> {
Padding( Padding(
padding: const EdgeInsets.only(left: 4.0, right: 4.0, top: 4.0), padding: const EdgeInsets.only(left: 4.0, right: 4.0, top: 4.0),
child: TextButton.icon( child: TextButton.icon(
onPressed: widget.disabled ? null : () { onPressed: widget.disabled ? null : () {
showDialog(context: context, builder: (context) => showDialog(context: context, builder: (context) =>
AlertDialog( AlertDialog(
title: const Text("Remove attachment"), title: const Text("Remove attachment"),
content: Text( content: Text(
"This will remove attachment '${basename( "This will remove attachment '${basename(
file.$2.path)}', are you sure?"), file.$2.path)}', are you sure?"),
actions: [ actions: [
TextButton( TextButton(
onPressed: () { onPressed: () {
Navigator.of(context).pop(); Navigator.of(context).pop();
}, },
child: const Text("No"), child: const Text("No"),
), ),
TextButton( TextButton(
onPressed: () async { onPressed: () async {
Navigator.of(context).pop(); Navigator.of(context).pop();
_loadedFiles.remove(file); _loadedFiles.remove(file);
await widget.onChange(_loadedFiles); await widget.onChange(_loadedFiles);
}, },
child: const Text("Yes"), child: const Text("Yes"),
) )
], ],
), ),
); );
}, },
style: TextButton.styleFrom( style: TextButton.styleFrom(
foregroundColor: Theme foregroundColor: Theme
.of(context) .of(context)
.colorScheme .colorScheme
.onBackground, .onBackground,
side: BorderSide( side: BorderSide(
color: Theme color: Theme
.of(context) .of(context)
.colorScheme .colorScheme
.primary, .primary,
width: 1 width: 1
),
), ),
), label: Text(basename(file.$2.path)),
label: Text(basename(file.$2.path)), icon: switch (file.$1) {
icon: switch (file.$1) { FileType.image => const Icon(Icons.image),
FileType.image => const Icon(Icons.image), _ => const Icon(Icons.attach_file)
_ => const Icon(Icons.attach_file) }
}
), ),
), ),
).toList() ).toList()
@ -117,43 +121,189 @@ class _MessageAttachmentListState extends State<MessageAttachmentList> {
), ),
), ),
), ),
IconButton( PopupMenuButton<DocumentType>(
onPressed: widget.disabled ? null : () async { offset: const Offset(0, -64),
final result = await FilePicker.platform.pickFiles(type: FileType.image, allowMultiple: true); constraints: const BoxConstraints.tightFor(width: 48 * 3, height: 64),
if (result != null) { shadowColor: Colors.transparent,
setState(() { position: PopupMenuPosition.over,
_loadedFiles.addAll( color: Colors.transparent,
result.files.map((e) => e.path != null ? (FileType.image, File(e.path!)) : null) enableFeedback: true,
.whereNotNull()); padding: EdgeInsets.zero,
}); surfaceTintColor: Colors.transparent,
} iconSize: 24,
}, itemBuilder: (context) =>
icon: const Icon(Icons.add_photo_alternate), [
), PopupMenuItem(
IconButton( padding: EdgeInsets.zero,
onPressed: widget.disabled ? null : () async { child: Row(
final picture = await Navigator.of(context).push( mainAxisAlignment: MainAxisAlignment.end,
MaterialPageRoute(builder: (context) => const MessageCameraView())); children: [
if (picture != null) { IconButton(
_loadedFiles.add(picture); iconSize: 24,
await widget.onChange(_loadedFiles); style: IconButton.styleFrom(
} backgroundColor: Theme
}, .of(context)
icon: const Icon(Icons.add_a_photo), .colorScheme
), .surface,
IconButton( foregroundColor: Theme
onPressed: widget.disabled ? null : () async { .of(context)
final result = await FilePicker.platform.pickFiles(type: FileType.any, allowMultiple: true); .colorScheme
if (result != null) { .onSurface,
setState(() { side: BorderSide(
_loadedFiles.addAll( width: 1,
result.files.map((e) => e.path != null ? (FileType.any, File(e.path!)) : null).whereNotNull()); color: Theme
}); .of(context)
} .colorScheme
}, .secondary,
icon: const Icon(Icons.file_present_rounded), )
),
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<T> extends PopupMenuEntry<T> {
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<StatefulWidget> createState() => _PopupMenuIconState();
@override
double get height => radius;
@override
bool represents(T? value) => this.value == value;
}
class _PopupMenuIconState extends State<PopupMenuIcon> {
@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,
),
),
);
}
}

View file

@ -88,8 +88,26 @@ class _MessageAudioPlayerState extends State<MessageAudioPlayer> with WidgetsBin
return FutureBuilder( return FutureBuilder(
future: _audioFileFuture, future: _audioFileFuture,
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.hasData) { if (snapshot.hasError) {
return IntrinsicWidth( 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<PlayerState>( child: StreamBuilder<PlayerState>(
stream: _audioPlayer.playerStateStream, stream: _audioPlayer.playerStateStream,
builder: (context, snapshot) { builder: (context, snapshot) {
@ -103,7 +121,7 @@ class _MessageAudioPlayerState extends State<MessageAudioPlayer> with WidgetsBin
mainAxisSize: MainAxisSize.max, mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
IconButton( snapshot.hasData ? IconButton(
onPressed: () { onPressed: () {
switch (playerState.processingState) { switch (playerState.processingState) {
case ProcessingState.idle: case ProcessingState.idle:
@ -124,16 +142,15 @@ class _MessageAudioPlayerState extends State<MessageAudioPlayer> with WidgetsBin
} }
}, },
color: widget.foregroundColor, color: widget.foregroundColor,
icon: SizedBox( icon: SizedBox.square(
width: 24, dimension: 24,
height: 24,
child: playerState.processingState == ProcessingState.loading child: playerState.processingState == ProcessingState.loading
? const Center(child: CircularProgressIndicator(),) ? const Center(child: CircularProgressIndicator(),)
: Icon(((_audioPlayer.duration ?? Duration.zero) - _audioPlayer.position).inMilliseconds < : Icon(((_audioPlayer.duration ?? Duration.zero) - _audioPlayer.position).inMilliseconds <
10 ? Icons.replay 10 ? Icons.replay
: (playerState.playing ? Icons.pause : Icons.play_arrow)), : (playerState.playing ? Icons.pause : Icons.play_arrow)),
), ),
), ) : const SizedBox.square(dimension: 24, child: CircularProgressIndicator(),),
StreamBuilder( StreamBuilder(
stream: _audioPlayer.positionStream, stream: _audioPlayer.positionStream,
builder: (context, snapshot) { builder: (context, snapshot) {
@ -200,36 +217,6 @@ class _MessageAudioPlayerState extends State<MessageAudioPlayer> 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()),
],
),
);
}
} }
); );
} }

View file

@ -34,6 +34,7 @@ class _MessageRecordButtonState extends State<MessageRecordButton> {
child: GestureDetector( child: GestureDetector(
onTapDown: widget.disabled ? null : (_) async { onTapDown: widget.disabled ? null : (_) async {
HapticFeedback.vibrate(); HapticFeedback.vibrate();
/*
widget.onRecordStart?.call(); widget.onRecordStart?.call();
final dir = await getTemporaryDirectory(); final dir = await getTemporaryDirectory();
await _recorder.start( await _recorder.start(
@ -41,16 +42,19 @@ class _MessageRecordButtonState extends State<MessageRecordButton> {
encoder: AudioEncoder.opus, encoder: AudioEncoder.opus,
samplingRate: 44100, samplingRate: 44100,
); );
*/
}, },
onTapUp: (_) async { onLongPressUp: () async {
/*
if (await _recorder.isRecording()) { if (await _recorder.isRecording()) {
final recording = await _recorder.stop(); final recording = await _recorder.stop();
widget.onRecordEnd?.call(recording == null ? null : File(recording)); widget.onRecordEnd?.call(recording == null ? null : File(recording));
} }
*/
}, },
child: const Padding( child: Padding(
padding: EdgeInsets.all(8.0), padding: const EdgeInsets.all(8),
child: Icon(Icons.mic_outlined), child: Icon(Icons.mic_outlined, size: 28, color: Theme.of(context).colorScheme.onSurface,),
), ),
), ),
); );

View file

@ -98,6 +98,7 @@ class _MessagesListState extends State<MessagesList> with SingleTickerProviderSt
type: MessageType.text, type: MessageType.text,
content: content, content: content,
sendTime: DateTime.now().toUtc(), sendTime: DateTime.now().toUtc(),
state: MessageState.local,
); );
mClient.sendMessage(message); mClient.sendMessage(message);
_messageTextController.clear(); _messageTextController.clear();
@ -119,6 +120,7 @@ class _MessagesListState extends State<MessagesList> with SingleTickerProviderSt
type: MessageType.object, type: MessageType.object,
content: jsonEncode(record.toMap()), content: jsonEncode(record.toMap()),
sendTime: DateTime.now().toUtc(), sendTime: DateTime.now().toUtc(),
state: MessageState.local
); );
mClient.sendMessage(message); mClient.sendMessage(message);
_messageTextController.clear(); _messageTextController.clear();
@ -140,6 +142,7 @@ class _MessagesListState extends State<MessagesList> with SingleTickerProviderSt
type: MessageType.sound, type: MessageType.sound,
content: jsonEncode(record.toMap()), content: jsonEncode(record.toMap()),
sendTime: DateTime.now().toUtc(), sendTime: DateTime.now().toUtc(),
state: MessageState.local,
); );
mClient.sendMessage(message); mClient.sendMessage(message);
_messageTextController.clear(); _messageTextController.clear();
@ -161,6 +164,7 @@ class _MessagesListState extends State<MessagesList> with SingleTickerProviderSt
type: MessageType.object, type: MessageType.object,
content: jsonEncode(record.toMap()), content: jsonEncode(record.toMap()),
sendTime: DateTime.now().toUtc(), sendTime: DateTime.now().toUtc(),
state: MessageState.local,
); );
mClient.sendMessage(message); mClient.sendMessage(message);
_messageTextController.clear(); _messageTextController.clear();
@ -195,7 +199,8 @@ class _MessagesListState extends State<MessagesList> with SingleTickerProviderSt
.of(context) .of(context)
.colorScheme .colorScheme
.onSecondaryContainer .onSecondaryContainer
.withAlpha(150),), .withAlpha(150),
),
), ),
], ],
), ),
@ -452,7 +457,7 @@ class _MessagesListState extends State<MessagesList> with SingleTickerProviderSt
_attachmentPickerOpen = true; _attachmentPickerOpen = true;
}); });
}, },
icon: const Icon(Icons.attach_file), icon: const Icon(Icons.attach_file, size: 28,),
) : ) :
IconButton( IconButton(
key: const ValueKey("remove-attachment-icon"), key: const ValueKey("remove-attachment-icon"),
@ -487,12 +492,12 @@ class _MessagesListState extends State<MessagesList> with SingleTickerProviderSt
}); });
} }
}, },
icon: const Icon(Icons.close), icon: const Icon(Icons.close, size: 28,),
), ),
), ),
Expanded( Expanded(
child: Padding( child: Padding(
padding: const EdgeInsets.all(8), padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 4),
child: TextField( child: TextField(
enabled: cache != null && cache.error == null && !_isSending, enabled: cache != null && cache.error == null && !_isSending,
autocorrect: true, autocorrect: true,
@ -518,13 +523,13 @@ class _MessagesListState extends State<MessagesList> with SingleTickerProviderSt
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
border: OutlineInputBorder( border: OutlineInputBorder(
borderRadius: BorderRadius.circular(24) borderRadius: BorderRadius.circular(24)
) ),
), ),
), ),
), ),
), ),
Padding( Padding(
padding: const EdgeInsets.only(left: 8, right: 4.0), padding: const EdgeInsets.all(4),
child: AnimatedSwitcher( child: AnimatedSwitcher(
duration: const Duration(milliseconds: 200), duration: const Duration(milliseconds: 200),
transitionBuilder: (Widget child, Animation<double> animation) => transitionBuilder: (Widget child, Animation<double> animation) =>
@ -533,6 +538,7 @@ class _MessagesListState extends State<MessagesList> with SingleTickerProviderSt
child: _hasText || _loadedFiles.isNotEmpty ? IconButton( child: _hasText || _loadedFiles.isNotEmpty ? IconButton(
key: const ValueKey("send-button"), key: const ValueKey("send-button"),
splashRadius: 24, splashRadius: 24,
padding: EdgeInsets.zero,
onPressed: _isSending ? null : () async { onPressed: _isSending ? null : () async {
final sMsgnr = ScaffoldMessenger.of(context); final sMsgnr = ScaffoldMessenger.of(context);
final settings = ClientHolder final settings = ClientHolder
@ -584,7 +590,6 @@ class _MessagesListState extends State<MessagesList> with SingleTickerProviderSt
_sendProgress = null; _sendProgress = null;
}); });
}, },
iconSize: 28,
icon: const Icon(Icons.send), icon: const Icon(Icons.send),
) : MessageRecordButton( ) : MessageRecordButton(
key: const ValueKey("mic-button"), key: const ValueKey("mic-button"),

View file

@ -138,7 +138,7 @@ packages:
source: hosted source: hosted
version: "0.3.3+4" version: "0.3.3+4"
crypto: crypto:
dependency: transitive dependency: "direct main"
description: description:
name: crypto name: crypto
sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab

View file

@ -60,6 +60,7 @@ dependencies:
record: ^4.4.4 record: ^4.4.4
camera: ^0.10.5 camera: ^0.10.5
path_provider: ^2.0.15 path_provider: ^2.0.15
crypto: ^3.0.3
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: