Compare commits

...

4 Commits

10 changed files with 339 additions and 51 deletions

View File

@ -96,7 +96,7 @@ class Conversation with _$Conversation {
'open': boolToInt(open),
'muted': boolToInt(muted),
'encrypted': boolToInt(encrypted),
'lastMessage': lastMessage?.id,
'lastMessageId': lastMessage?.id,
};
}

View File

@ -181,7 +181,7 @@ class ConversationBloc extends Bloc<ConversationEvent, ConversationState> {
_stopComposeTimer();
// ignore: cast_nullable_to_non_nullable
final r = await MoxplatformPlugin.handler.getDataSender().sendData(
await MoxplatformPlugin.handler.getDataSender().sendData(
SendMessageCommand(
recipients: [state.conversation!.jid],
body: state.messageText,
@ -190,24 +190,21 @@ class ConversationBloc extends Bloc<ConversationEvent, ConversationState> {
editId: state.messageEditingId,
editSid: state.messageEditingSid,
),
awaitable: false,
);
if (!state.messageEditing) {
final result = r! as events.MessageAddedEvent;
emit(
state.copyWith(
messages: List<Message>.from(<Message>[ ...state.messages, result.message ]),
messageText: '',
quotedMessage: null,
showSendButton: false,
emojiPickerVisible: false,
messageEditing: false,
messageEditingOriginalBody: '',
messageEditingId: null,
messageEditingSid: null,
),
);
}
emit(
state.copyWith(
messageText: '',
quotedMessage: null,
showSendButton: false,
emojiPickerVisible: false,
messageEditing: false,
messageEditingOriginalBody: '',
messageEditingId: null,
messageEditingSid: null,
),
);
}
Future<void> _onMessageQuoted(MessageQuotedEvent event, Emitter<ConversationState> emit) async {

View File

@ -83,7 +83,7 @@ class SharedMediaPage extends StatelessWidget {
spacing: 5,
runSpacing: 5,
children: row.map((medium) {
return buildSharedMediaWidget(medium, state.jid);
return buildSharedMediaWidget(medium, state.jid);
}).toList(),
),
),

View File

@ -216,7 +216,7 @@ class ChatBubbleState extends State<ChatBubble>
GestureDetector(
onLongPressStart: widget.onLongPressed,
child: widget.bubble,
),
),
],
),
),

View 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();
}
}

View File

@ -26,30 +26,38 @@ class MediaBaseChatWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return IntrinsicWidth(
child: InkResponse(
onTap: onTap,
child: Stack(
alignment: Alignment.center,
children: [
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,
),
)
],
final content = Stack(
alignment: Alignment.center,
children: [
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,
),
),
],
);
if (onTap != null) {
return IntrinsicWidth(
child: InkResponse(
onTap: onTap,
child: content,
),
);
} else {
return IntrinsicWidth(
child: content,
);
}
}
}

View File

@ -4,11 +4,13 @@ import 'package:flutter/material.dart';
import 'package:moxxyv2/shared/helpers.dart';
import 'package:moxxyv2/shared/models/media.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/image.dart';
import 'package:moxxyv2/ui/widgets/chat/media/video.dart';
import 'package:moxxyv2/ui/widgets/chat/playbutton.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/image.dart';
import 'package:moxxyv2/ui/widgets/chat/shared/video.dart';
@ -18,7 +20,7 @@ enum MessageType {
text,
image,
video,
// audio
audio,
file
}
@ -33,9 +35,9 @@ MessageType getMessageType(Message message) {
return MessageType.image;
} else if (mime.startsWith('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;
}
@ -70,8 +72,8 @@ Widget buildMessageWidget(Message message, double maxWidth, BorderRadius radius,
case MessageType.video: {
return VideoChatWidget(message, radius, maxWidth, sent);
}
// TODO(Unknown): Implement audio
//case MessageType.audio: return buildImageMessageWidget(message);
case MessageType.audio:
return AudioChatWidget(message, radius, maxWidth, sent);
case MessageType.file: {
return FileChatWidget(message, radius, sent);
}
@ -143,7 +145,7 @@ Widget buildQuoteMessageWidget(Message message, bool sent, { void Function()? re
resetQuotedMessage: resetQuote,
);
// TODO(Unknown): Implement audio
//case MessageType.audio: return const SizedBox();
case MessageType.audio:
case MessageType.file:
return QuoteBaseWidget(
message,
@ -197,9 +199,12 @@ Widget buildSharedMediaWidget(SharedMedium medium, String conversationJid) {
onTap: () => OpenFile.open(medium.path),
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);
}

View 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,
);
}
}

View File

@ -36,6 +36,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.9.0"
audiofileplayer:
dependency: "direct main"
description:
name: audiofileplayer
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.1"
awesome_notifications:
dependency: "direct main"
description:

View File

@ -11,6 +11,7 @@ environment:
dependencies:
archive: 3.3.0
audiofileplayer: 2.1.1
awesome_notifications: 0.7.4+1
badges: 2.0.3
better_open_file: 3.6.3