Compare commits
4 Commits
38a73d2890
...
a63205d5e1
Author | SHA1 | Date | |
---|---|---|---|
a63205d5e1 | |||
e7a4e93366 | |||
2c23f40415 | |||
daf4ee79f2 |
@ -96,7 +96,7 @@ class Conversation with _$Conversation {
|
|||||||
'open': boolToInt(open),
|
'open': boolToInt(open),
|
||||||
'muted': boolToInt(muted),
|
'muted': boolToInt(muted),
|
||||||
'encrypted': boolToInt(encrypted),
|
'encrypted': boolToInt(encrypted),
|
||||||
'lastMessage': lastMessage?.id,
|
'lastMessageId': lastMessage?.id,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -181,7 +181,7 @@ class ConversationBloc extends Bloc<ConversationEvent, ConversationState> {
|
|||||||
_stopComposeTimer();
|
_stopComposeTimer();
|
||||||
|
|
||||||
// ignore: cast_nullable_to_non_nullable
|
// ignore: cast_nullable_to_non_nullable
|
||||||
final r = await MoxplatformPlugin.handler.getDataSender().sendData(
|
await MoxplatformPlugin.handler.getDataSender().sendData(
|
||||||
SendMessageCommand(
|
SendMessageCommand(
|
||||||
recipients: [state.conversation!.jid],
|
recipients: [state.conversation!.jid],
|
||||||
body: state.messageText,
|
body: state.messageText,
|
||||||
@ -190,24 +190,21 @@ class ConversationBloc extends Bloc<ConversationEvent, ConversationState> {
|
|||||||
editId: state.messageEditingId,
|
editId: state.messageEditingId,
|
||||||
editSid: state.messageEditingSid,
|
editSid: state.messageEditingSid,
|
||||||
),
|
),
|
||||||
|
awaitable: false,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!state.messageEditing) {
|
emit(
|
||||||
final result = r! as events.MessageAddedEvent;
|
state.copyWith(
|
||||||
emit(
|
messageText: '',
|
||||||
state.copyWith(
|
quotedMessage: null,
|
||||||
messages: List<Message>.from(<Message>[ ...state.messages, result.message ]),
|
showSendButton: false,
|
||||||
messageText: '',
|
emojiPickerVisible: false,
|
||||||
quotedMessage: null,
|
messageEditing: false,
|
||||||
showSendButton: false,
|
messageEditingOriginalBody: '',
|
||||||
emojiPickerVisible: false,
|
messageEditingId: null,
|
||||||
messageEditing: false,
|
messageEditingSid: null,
|
||||||
messageEditingOriginalBody: '',
|
),
|
||||||
messageEditingId: null,
|
);
|
||||||
messageEditingSid: null,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _onMessageQuoted(MessageQuotedEvent event, Emitter<ConversationState> emit) async {
|
Future<void> _onMessageQuoted(MessageQuotedEvent event, Emitter<ConversationState> emit) async {
|
||||||
|
@ -83,7 +83,7 @@ class SharedMediaPage extends StatelessWidget {
|
|||||||
spacing: 5,
|
spacing: 5,
|
||||||
runSpacing: 5,
|
runSpacing: 5,
|
||||||
children: row.map((medium) {
|
children: row.map((medium) {
|
||||||
return buildSharedMediaWidget(medium, state.jid);
|
return buildSharedMediaWidget(medium, state.jid);
|
||||||
}).toList(),
|
}).toList(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -216,7 +216,7 @@ class ChatBubbleState extends State<ChatBubble>
|
|||||||
GestureDetector(
|
GestureDetector(
|
||||||
onLongPressStart: widget.onLongPressed,
|
onLongPressStart: widget.onLongPressed,
|
||||||
child: widget.bubble,
|
child: widget.bubble,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
228
lib/ui/widgets/chat/media/audio.dart
Normal file
228
lib/ui/widgets/chat/media/audio.dart
Normal file
@ -0,0 +1,228 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:io';
|
||||||
|
import 'package:audiofileplayer/audiofileplayer.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:moxplatform/moxplatform.dart';
|
||||||
|
import 'package:moxxyv2/shared/commands.dart';
|
||||||
|
import 'package:moxxyv2/shared/helpers.dart';
|
||||||
|
import 'package:moxxyv2/shared/models/message.dart';
|
||||||
|
import 'package:moxxyv2/ui/widgets/chat/bottom.dart';
|
||||||
|
import 'package:moxxyv2/ui/widgets/chat/downloadbutton.dart';
|
||||||
|
import 'package:moxxyv2/ui/widgets/chat/media/base.dart';
|
||||||
|
import 'package:moxxyv2/ui/widgets/chat/media/file.dart';
|
||||||
|
import 'package:moxxyv2/ui/widgets/chat/progress.dart';
|
||||||
|
|
||||||
|
String doubleToTimestamp(double p) {
|
||||||
|
if (p < 60) {
|
||||||
|
return '0:${padInt(p.floor())}';
|
||||||
|
}
|
||||||
|
|
||||||
|
final minutes = (p / 60).floor();
|
||||||
|
final seconds = padInt((p - minutes * 60).floor());
|
||||||
|
return '$minutes:$seconds';
|
||||||
|
}
|
||||||
|
|
||||||
|
enum _AudioPlaybackState {
|
||||||
|
playing,
|
||||||
|
paused,
|
||||||
|
stopped
|
||||||
|
}
|
||||||
|
|
||||||
|
class AudioChatWidget extends StatefulWidget {
|
||||||
|
const AudioChatWidget(
|
||||||
|
this.message,
|
||||||
|
this.radius,
|
||||||
|
this.maxWidth,
|
||||||
|
this.sent,
|
||||||
|
{
|
||||||
|
super.key,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
final Message message;
|
||||||
|
final BorderRadius radius;
|
||||||
|
final double maxWidth;
|
||||||
|
final bool sent;
|
||||||
|
|
||||||
|
@override
|
||||||
|
AudioChatState createState() => AudioChatState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class AudioChatState extends State<AudioChatWidget> {
|
||||||
|
_AudioPlaybackState _playState = _AudioPlaybackState.stopped;
|
||||||
|
double? _duration;
|
||||||
|
double? _position;
|
||||||
|
Audio? _audioFile;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_init();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void setState(VoidCallback fn) {
|
||||||
|
if (mounted) {
|
||||||
|
super.setState(fn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _init() async {
|
||||||
|
_audioFile = Audio.loadFromAbsolutePath(
|
||||||
|
widget.message.mediaUrl!,
|
||||||
|
onDuration: (double seconds) {
|
||||||
|
setState(() {
|
||||||
|
_duration = seconds;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onPosition: (double seconds) {
|
||||||
|
setState(() {
|
||||||
|
_position = seconds;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onComplete: () {
|
||||||
|
setState(() {
|
||||||
|
_playState = _AudioPlaybackState.stopped;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_audioFile?.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildUploading() {
|
||||||
|
// TODO(PapaTutuWawa): Fix
|
||||||
|
return FileChatWidget(
|
||||||
|
widget.message,
|
||||||
|
widget.radius,
|
||||||
|
widget.sent,
|
||||||
|
extra: ProgressWidget(id: widget.message.id),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildDownloading() {
|
||||||
|
// TODO(PapaTutuWawa): Fix
|
||||||
|
return FileChatBaseWidget(
|
||||||
|
widget.message,
|
||||||
|
Icons.image,
|
||||||
|
widget.message.isFileUploadNotification ?
|
||||||
|
(widget.message.filename ?? '') :
|
||||||
|
filenameFromUrl(widget.message.srcUrl!),
|
||||||
|
widget.radius,
|
||||||
|
widget.sent,
|
||||||
|
extra: ProgressWidget(id: widget.message.id),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The audio file exists locally
|
||||||
|
Widget _buildAudio() {
|
||||||
|
return MediaBaseChatWidget(
|
||||||
|
SizedBox(
|
||||||
|
width: widget.maxWidth,
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
InkWell(
|
||||||
|
onTap: () {
|
||||||
|
if (_playState != _AudioPlaybackState.playing) {
|
||||||
|
if (_playState == _AudioPlaybackState.paused) {
|
||||||
|
_audioFile?.resume();
|
||||||
|
} else if (_playState == _AudioPlaybackState.stopped) {
|
||||||
|
_audioFile?.play();
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_playState = _AudioPlaybackState.playing;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
_audioFile?.pause();
|
||||||
|
setState(() {
|
||||||
|
_playState = _AudioPlaybackState.paused;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(
|
||||||
|
left: 8,
|
||||||
|
right: 4,
|
||||||
|
),
|
||||||
|
child: _playState == _AudioPlaybackState.playing ?
|
||||||
|
const Icon(Icons.pause) :
|
||||||
|
const Icon(Icons.play_arrow),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Slider(
|
||||||
|
onChanged: (p) {
|
||||||
|
if (_duration == null || _audioFile == null) return;
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_position = p * _duration!;
|
||||||
|
_audioFile!.seek(_position!);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
value: (_position != null && _duration != null) ?
|
||||||
|
(_position! / _duration!).clamp(0, 1) :
|
||||||
|
0,
|
||||||
|
),
|
||||||
|
|
||||||
|
SizedBox(
|
||||||
|
width: widget.maxWidth - 80,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Text(doubleToTimestamp(_position ?? 0)),
|
||||||
|
const Spacer(),
|
||||||
|
Text(doubleToTimestamp(_duration ?? 0)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
MessageBubbleBottom(widget.message, widget.sent),
|
||||||
|
widget.radius,
|
||||||
|
gradient: false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildDownloadable() {
|
||||||
|
return FileChatBaseWidget(
|
||||||
|
widget.message,
|
||||||
|
Icons.image,
|
||||||
|
widget.message.isFileUploadNotification ?
|
||||||
|
(widget.message.filename ?? '') :
|
||||||
|
filenameFromUrl(widget.message.srcUrl!),
|
||||||
|
widget.radius,
|
||||||
|
widget.sent,
|
||||||
|
extra: DownloadButton(
|
||||||
|
onPressed: () {
|
||||||
|
MoxplatformPlugin.handler.getDataSender().sendData(
|
||||||
|
RequestDownloadCommand(message: widget.message),
|
||||||
|
awaitable: false,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (widget.message.isUploading) return _buildUploading();
|
||||||
|
if (widget.message.isFileUploadNotification || widget.message.isDownloading) return _buildDownloading();
|
||||||
|
|
||||||
|
// TODO(PapaTutuWawa): Maybe use an async builder
|
||||||
|
if (widget.message.mediaUrl != null && File(widget.message.mediaUrl!).existsSync()) return _buildAudio();
|
||||||
|
|
||||||
|
return _buildDownloadable();
|
||||||
|
}
|
||||||
|
}
|
@ -26,30 +26,38 @@ class MediaBaseChatWidget extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return IntrinsicWidth(
|
final content = Stack(
|
||||||
child: InkResponse(
|
alignment: Alignment.center,
|
||||||
onTap: onTap,
|
children: [
|
||||||
child: Stack(
|
ClipRRect(
|
||||||
alignment: Alignment.center,
|
borderRadius: radius,
|
||||||
children: [
|
child: background,
|
||||||
ClipRRect(
|
|
||||||
borderRadius: radius,
|
|
||||||
child: background,
|
|
||||||
),
|
|
||||||
...gradient ? [BottomGradient(radius)] : [],
|
|
||||||
...extra != null ? [ extra! ] : [],
|
|
||||||
Positioned(
|
|
||||||
bottom: 0,
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.only(bottom: 3, right: 6),
|
|
||||||
child: bottom,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
...gradient ? [BottomGradient(radius)] : [],
|
||||||
|
...extra != null ? [ extra! ] : [],
|
||||||
|
Positioned(
|
||||||
|
bottom: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 3, right: 6),
|
||||||
|
child: bottom,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (onTap != null) {
|
||||||
|
return IntrinsicWidth(
|
||||||
|
child: InkResponse(
|
||||||
|
onTap: onTap,
|
||||||
|
child: content,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return IntrinsicWidth(
|
||||||
|
child: content,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,11 +4,13 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:moxxyv2/shared/helpers.dart';
|
import 'package:moxxyv2/shared/helpers.dart';
|
||||||
import 'package:moxxyv2/shared/models/media.dart';
|
import 'package:moxxyv2/shared/models/media.dart';
|
||||||
import 'package:moxxyv2/shared/models/message.dart';
|
import 'package:moxxyv2/shared/models/message.dart';
|
||||||
|
import 'package:moxxyv2/ui/widgets/chat/media/audio.dart';
|
||||||
import 'package:moxxyv2/ui/widgets/chat/media/file.dart';
|
import 'package:moxxyv2/ui/widgets/chat/media/file.dart';
|
||||||
import 'package:moxxyv2/ui/widgets/chat/media/image.dart';
|
import 'package:moxxyv2/ui/widgets/chat/media/image.dart';
|
||||||
import 'package:moxxyv2/ui/widgets/chat/media/video.dart';
|
import 'package:moxxyv2/ui/widgets/chat/media/video.dart';
|
||||||
import 'package:moxxyv2/ui/widgets/chat/playbutton.dart';
|
import 'package:moxxyv2/ui/widgets/chat/playbutton.dart';
|
||||||
import 'package:moxxyv2/ui/widgets/chat/quote/base.dart';
|
import 'package:moxxyv2/ui/widgets/chat/quote/base.dart';
|
||||||
|
import 'package:moxxyv2/ui/widgets/chat/shared/audio.dart';
|
||||||
import 'package:moxxyv2/ui/widgets/chat/shared/file.dart';
|
import 'package:moxxyv2/ui/widgets/chat/shared/file.dart';
|
||||||
import 'package:moxxyv2/ui/widgets/chat/shared/image.dart';
|
import 'package:moxxyv2/ui/widgets/chat/shared/image.dart';
|
||||||
import 'package:moxxyv2/ui/widgets/chat/shared/video.dart';
|
import 'package:moxxyv2/ui/widgets/chat/shared/video.dart';
|
||||||
@ -18,7 +20,7 @@ enum MessageType {
|
|||||||
text,
|
text,
|
||||||
image,
|
image,
|
||||||
video,
|
video,
|
||||||
// audio
|
audio,
|
||||||
file
|
file
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -33,9 +35,9 @@ MessageType getMessageType(Message message) {
|
|||||||
return MessageType.image;
|
return MessageType.image;
|
||||||
} else if (mime.startsWith('video/')) {
|
} else if (mime.startsWith('video/')) {
|
||||||
return MessageType.video;
|
return MessageType.video;
|
||||||
|
} else if (mime.startsWith('audio/')) {
|
||||||
|
return MessageType.audio;
|
||||||
}
|
}
|
||||||
// TODO(Unknown): Implement audio
|
|
||||||
//else if (mime.startswith("audio/")) return MessageType.audio;
|
|
||||||
|
|
||||||
return MessageType.file;
|
return MessageType.file;
|
||||||
}
|
}
|
||||||
@ -70,8 +72,8 @@ Widget buildMessageWidget(Message message, double maxWidth, BorderRadius radius,
|
|||||||
case MessageType.video: {
|
case MessageType.video: {
|
||||||
return VideoChatWidget(message, radius, maxWidth, sent);
|
return VideoChatWidget(message, radius, maxWidth, sent);
|
||||||
}
|
}
|
||||||
// TODO(Unknown): Implement audio
|
case MessageType.audio:
|
||||||
//case MessageType.audio: return buildImageMessageWidget(message);
|
return AudioChatWidget(message, radius, maxWidth, sent);
|
||||||
case MessageType.file: {
|
case MessageType.file: {
|
||||||
return FileChatWidget(message, radius, sent);
|
return FileChatWidget(message, radius, sent);
|
||||||
}
|
}
|
||||||
@ -143,7 +145,7 @@ Widget buildQuoteMessageWidget(Message message, bool sent, { void Function()? re
|
|||||||
resetQuotedMessage: resetQuote,
|
resetQuotedMessage: resetQuote,
|
||||||
);
|
);
|
||||||
// TODO(Unknown): Implement audio
|
// TODO(Unknown): Implement audio
|
||||||
//case MessageType.audio: return const SizedBox();
|
case MessageType.audio:
|
||||||
case MessageType.file:
|
case MessageType.file:
|
||||||
return QuoteBaseWidget(
|
return QuoteBaseWidget(
|
||||||
message,
|
message,
|
||||||
@ -197,9 +199,12 @@ Widget buildSharedMediaWidget(SharedMedium medium, String conversationJid) {
|
|||||||
onTap: () => OpenFile.open(medium.path),
|
onTap: () => OpenFile.open(medium.path),
|
||||||
child: const PlayButton(size: 32),
|
child: const PlayButton(size: 32),
|
||||||
);
|
);
|
||||||
|
} else if (medium.mime!.startsWith('audio/')) {
|
||||||
|
return SharedAudioWidget(
|
||||||
|
medium.path,
|
||||||
|
onTap: () => OpenFile.open(medium.path),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
// TODO(Unknown): Audio
|
|
||||||
//if (message.mime!.startsWith("audio/")) return const SizedBox();
|
|
||||||
|
|
||||||
return SharedFileWidget(medium.path);
|
return SharedFileWidget(medium.path);
|
||||||
}
|
}
|
||||||
|
42
lib/ui/widgets/chat/shared/audio.dart
Normal file
42
lib/ui/widgets/chat/shared/audio.dart
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:moxxyv2/ui/widgets/chat/shared/base.dart';
|
||||||
|
|
||||||
|
class SharedAudioWidget extends StatelessWidget {
|
||||||
|
const SharedAudioWidget(
|
||||||
|
this.path, {
|
||||||
|
this.onTap,
|
||||||
|
this.borderColor,
|
||||||
|
this.borderRadius = 10,
|
||||||
|
this.size = sharedMediaContainerDimension,
|
||||||
|
super.key,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
final String path;
|
||||||
|
final Color? borderColor;
|
||||||
|
final void Function()? onTap;
|
||||||
|
final double borderRadius;
|
||||||
|
final double size;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SharedMediaContainer(
|
||||||
|
Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(borderRadius),
|
||||||
|
color: Colors.white60,
|
||||||
|
border: borderColor != null ? Border.all(
|
||||||
|
color: borderColor!,
|
||||||
|
width: 4,
|
||||||
|
) : null,
|
||||||
|
),
|
||||||
|
clipBehavior: Clip.hardEdge,
|
||||||
|
child: const Icon(
|
||||||
|
Icons.music_note,
|
||||||
|
size: 48,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
size: size,
|
||||||
|
onTap: onTap,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -36,6 +36,13 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.9.0"
|
version: "2.9.0"
|
||||||
|
audiofileplayer:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: audiofileplayer
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.1"
|
||||||
awesome_notifications:
|
awesome_notifications:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
@ -11,6 +11,7 @@ environment:
|
|||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
archive: 3.3.0
|
archive: 3.3.0
|
||||||
|
audiofileplayer: 2.1.1
|
||||||
awesome_notifications: 0.7.4+1
|
awesome_notifications: 0.7.4+1
|
||||||
badges: 2.0.3
|
badges: 2.0.3
|
||||||
better_open_file: 3.6.3
|
better_open_file: 3.6.3
|
||||||
|
Loading…
Reference in New Issue
Block a user