Minor message view improvements
This commit is contained in:
parent
c12748de6c
commit
1b7af5f4a7
9 changed files with 278 additions and 134 deletions
|
@ -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';
|
||||
|
|
|
@ -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)
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
@ -117,8 +121,44 @@ class _MessageAttachmentListState extends State<MessageAttachmentList> {
|
|||
),
|
||||
),
|
||||
),
|
||||
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(
|
||||
onPressed: widget.disabled ? null : () async {
|
||||
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(() {
|
||||
|
@ -127,33 +167,143 @@ class _MessageAttachmentListState extends State<MessageAttachmentList> {
|
|||
.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);
|
||||
if (context.mounted) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.add_a_photo),
|
||||
icon: const Icon(Icons.image,),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: widget.disabled ? null : () async {
|
||||
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());
|
||||
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: 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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -88,7 +88,25 @@ class _MessageAudioPlayerState extends State<MessageAudioPlayer> with WidgetsBin
|
|||
return FutureBuilder(
|
||||
future: _audioFileFuture,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
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,
|
||||
|
@ -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()),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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,),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
@ -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"),
|
||||
|
|
|
@ -138,7 +138,7 @@ packages:
|
|||
source: hosted
|
||||
version: "0.3.3+4"
|
||||
crypto:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: crypto
|
||||
sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab
|
||||
|
|
|
@ -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:
|
||||
|
|
Loading…
Reference in a new issue