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:io' show Platform;
import 'package:contacts_plus_plus/apis/github_api.dart';
import 'package:contacts_plus_plus/client_holder.dart';

View file

@ -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)
);
}

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/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<FriendsList> {
final clientHolder = ClientHolder.of(context);
if (_clientHolder != clientHolder) {
_clientHolder = clientHolder;
final apiClient = _clientHolder!.apiClient;
_refreshUserStatus();
}
}

View file

@ -53,8 +53,12 @@ class _MessageAttachmentListState extends State<MessageAttachmentList> {
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<MessageAttachmentList> {
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<MessageAttachmentList> {
),
),
),
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<DocumentType>(
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<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(
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<PlayerState>(
stream: _audioPlayer.playerStateStream,
builder: (context, snapshot) {
@ -103,7 +121,7 @@ class _MessageAudioPlayerState extends State<MessageAudioPlayer> 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<MessageAudioPlayer> 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<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(
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<MessageRecordButton> {
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,),
),
),
);

View file

@ -98,6 +98,7 @@ class _MessagesListState extends State<MessagesList> 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<MessagesList> 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<MessagesList> 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<MessagesList> 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<MessagesList> with SingleTickerProviderSt
.of(context)
.colorScheme
.onSecondaryContainer
.withAlpha(150),),
.withAlpha(150),
),
),
],
),
@ -452,7 +457,7 @@ class _MessagesListState extends State<MessagesList> 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<MessagesList> 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<MessagesList> 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<double> animation) =>
@ -533,6 +538,7 @@ class _MessagesListState extends State<MessagesList> 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<MessagesList> with SingleTickerProviderSt
_sendProgress = null;
});
},
iconSize: 28,
icon: const Icon(Icons.send),
) : MessageRecordButton(
key: const ValueKey("mic-button"),

View file

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

View file

@ -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: