Compare commits
10 Commits
8b4d7dd569
...
cb6bce0c56
Author | SHA1 | Date | |
---|---|---|---|
cb6bce0c56 | |||
5c1eda72c3 | |||
4542accc33 | |||
8f5470076b | |||
3427c3c761 | |||
0cc8d0947b | |||
c3795450a9 | |||
868d924836 | |||
d8f634d67c | |||
09b97ab4c5 |
@ -356,7 +356,12 @@ Future<Size?> getImageSizeFromData(Uint8List bytes) async {
|
||||
/// to the JID of the conversation the file comes from.
|
||||
/// If the thumbnail already exists, then just its path is returned. If not, then
|
||||
/// it gets generated first.
|
||||
Future<String> getVideoThumbnailPath(String path, String conversationJid) async {
|
||||
Future<String?> getVideoThumbnailPath(String path, String conversationJid, String mime) async {
|
||||
//print('getVideoThumbnailPath: Mime type: $mime');
|
||||
|
||||
// Ignore mime types that may be wacky
|
||||
if (mime == 'video/webm') return null;
|
||||
|
||||
final tempDir = await getTemporaryDirectory();
|
||||
final thumbnailFilenameNoExtension = p.withoutExtension(
|
||||
p.basename(path),
|
||||
|
@ -48,6 +48,10 @@ class ConversationBloc extends Bloc<ConversationEvent, ConversationState> {
|
||||
on<MessageRetractedEvent>(_onMessageRetracted);
|
||||
on<MessageEditSelectedEvent>(_onMessageEditSelected);
|
||||
on<MessageEditCancelledEvent>(_onMessageEditCancelled);
|
||||
on<SendButtonDragStartedEvent>(_onDragStarted);
|
||||
on<SendButtonDragEndedEvent>(_onDragEnded);
|
||||
on<SendButtonLockedEvent>(_onSendButtonLocked);
|
||||
on<SendButtonLockPressedEvent>(_onSendButtonLockPressed);
|
||||
}
|
||||
/// The current chat state with the conversation partner
|
||||
ChatState _currentChatState;
|
||||
@ -119,9 +123,12 @@ class ConversationBloc extends Bloc<ConversationEvent, ConversationState> {
|
||||
quotedMessage: null,
|
||||
messageEditing: false,
|
||||
messageEditingOriginalBody: '',
|
||||
messageText: '',
|
||||
messageEditingId: null,
|
||||
messageEditingSid: null,
|
||||
showSendButton: false,
|
||||
sendButtonState: defaultSendButtonState,
|
||||
isLocked: false,
|
||||
isDragging: false,
|
||||
),
|
||||
);
|
||||
|
||||
@ -167,10 +174,21 @@ class ConversationBloc extends Bloc<ConversationEvent, ConversationState> {
|
||||
_startComposeTimer();
|
||||
_updateChatState(ChatState.composing);
|
||||
|
||||
SendButtonState sendButtonState;
|
||||
if (state.messageEditing) {
|
||||
sendButtonState = event.value == state.messageEditingOriginalBody ?
|
||||
SendButtonState.cancelCorrection :
|
||||
SendButtonState.send;
|
||||
} else {
|
||||
sendButtonState = event.value.isEmpty ?
|
||||
defaultSendButtonState :
|
||||
SendButtonState.send;
|
||||
}
|
||||
|
||||
return emit(
|
||||
state.copyWith(
|
||||
messageText: event.value,
|
||||
showSendButton: event.value.isNotEmpty,
|
||||
sendButtonState: sendButtonState,
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -197,7 +215,7 @@ class ConversationBloc extends Bloc<ConversationEvent, ConversationState> {
|
||||
state.copyWith(
|
||||
messageText: '',
|
||||
quotedMessage: null,
|
||||
showSendButton: false,
|
||||
sendButtonState: defaultSendButtonState,
|
||||
emojiPickerVisible: false,
|
||||
messageEditing: false,
|
||||
messageEditingOriginalBody: '',
|
||||
@ -375,6 +393,7 @@ class ConversationBloc extends Bloc<ConversationEvent, ConversationState> {
|
||||
messageEditingOriginalBody: event.message.body,
|
||||
messageEditingId: event.message.id,
|
||||
messageEditingSid: event.message.sid,
|
||||
sendButtonState: SendButtonState.cancelCorrection,
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -388,7 +407,33 @@ class ConversationBloc extends Bloc<ConversationEvent, ConversationState> {
|
||||
messageEditingOriginalBody: '',
|
||||
messageEditingId: null,
|
||||
messageEditingSid: null,
|
||||
showSendButton: false,
|
||||
sendButtonState: defaultSendButtonState,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onDragStarted(SendButtonDragStartedEvent event, Emitter<ConversationState> emit) async {
|
||||
emit(state.copyWith(isDragging: true));
|
||||
}
|
||||
|
||||
Future<void> _onDragEnded(SendButtonDragEndedEvent event, Emitter<ConversationState> emit) async {
|
||||
emit(
|
||||
state.copyWith(
|
||||
isDragging: false,
|
||||
isLocked: false,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onSendButtonLocked(SendButtonLockedEvent event, Emitter<ConversationState> emit) async {
|
||||
emit(state.copyWith(isLocked: true));
|
||||
}
|
||||
|
||||
Future<void> _onSendButtonLockPressed(SendButtonLockPressedEvent event, Emitter<ConversationState> emit) async {
|
||||
emit(
|
||||
state.copyWith(
|
||||
isLocked: false,
|
||||
isDragging: false,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -131,3 +131,15 @@ class MessageEditSelectedEvent extends ConversationEvent {
|
||||
class MessageEditCancelledEvent extends ConversationEvent {
|
||||
MessageEditCancelledEvent();
|
||||
}
|
||||
|
||||
/// Triggered when the dragging began
|
||||
class SendButtonDragStartedEvent extends ConversationEvent {}
|
||||
|
||||
/// Triggered when the dragging ended
|
||||
class SendButtonDragEndedEvent extends ConversationEvent {}
|
||||
|
||||
/// Triggered when the dragging ended
|
||||
class SendButtonLockedEvent extends ConversationEvent {}
|
||||
|
||||
/// Triggered when the FAB has been locked
|
||||
class SendButtonLockPressedEvent extends ConversationEvent {}
|
||||
|
@ -1,12 +1,19 @@
|
||||
part of 'conversation_bloc.dart';
|
||||
|
||||
enum SendButtonState {
|
||||
audio,
|
||||
send,
|
||||
cancelCorrection,
|
||||
}
|
||||
const defaultSendButtonState = SendButtonState.audio;
|
||||
|
||||
@freezed
|
||||
class ConversationState with _$ConversationState {
|
||||
factory ConversationState({
|
||||
// Our own JID
|
||||
@Default('') String jid,
|
||||
@Default('') String messageText,
|
||||
@Default(false) bool showSendButton,
|
||||
@Default(defaultSendButtonState) SendButtonState sendButtonState,
|
||||
@Default(null) Message? quotedMessage,
|
||||
@Default(<Message>[]) List<Message> messages,
|
||||
@Default(null) Conversation? conversation,
|
||||
@ -16,5 +23,9 @@ class ConversationState with _$ConversationState {
|
||||
@Default('') String messageEditingOriginalBody,
|
||||
@Default(null) String? messageEditingSid,
|
||||
@Default(null) int? messageEditingId,
|
||||
|
||||
// For recording
|
||||
@Default(false) bool isDragging,
|
||||
@Default(false) bool isLocked,
|
||||
}) = _ConversationState;
|
||||
}
|
||||
|
62
lib/ui/pages/conversation/blink.dart
Normal file
62
lib/ui/pages/conversation/blink.dart
Normal file
@ -0,0 +1,62 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class BlinkingMicrophoneIcon extends StatefulWidget {
|
||||
const BlinkingMicrophoneIcon({ super.key });
|
||||
|
||||
@override
|
||||
BlinkingMicrophoneIconState createState() => BlinkingMicrophoneIconState();
|
||||
}
|
||||
|
||||
class BlinkingMicrophoneIconState extends State<BlinkingMicrophoneIcon> with TickerProviderStateMixin {
|
||||
late final AnimationController _recordingBlinkController;
|
||||
late final Animation<Color?> _recordingBlink;
|
||||
bool _blinkForward = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_recordingBlinkController = AnimationController(
|
||||
duration: const Duration(milliseconds: 600),
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
_recordingBlink = ColorTween(
|
||||
begin: Colors.white,
|
||||
end: Colors.red.shade600,
|
||||
).animate(_recordingBlinkController)
|
||||
..addStatusListener((status) {
|
||||
if (status == AnimationStatus.completed ||
|
||||
status == AnimationStatus.dismissed) {
|
||||
if (_blinkForward) {
|
||||
_recordingBlinkController.reverse();
|
||||
} else {
|
||||
_recordingBlinkController.forward();
|
||||
}
|
||||
|
||||
_blinkForward = !_blinkForward;
|
||||
}
|
||||
});
|
||||
|
||||
_recordingBlinkController.forward();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_recordingBlinkController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedBuilder(
|
||||
animation: _recordingBlink,
|
||||
builder: (_, __) {
|
||||
return Icon(
|
||||
Icons.mic,
|
||||
color: _recordingBlink.value,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -2,20 +2,52 @@ import 'dart:math';
|
||||
import 'package:emoji_picker_flutter/emoji_picker_flutter.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_speed_dial/flutter_speed_dial.dart';
|
||||
import 'package:flutter_vibrate/flutter_vibrate.dart';
|
||||
import 'package:moxxyv2/shared/helpers.dart';
|
||||
import 'package:moxxyv2/ui/bloc/conversation_bloc.dart';
|
||||
import 'package:moxxyv2/ui/constants.dart';
|
||||
import 'package:moxxyv2/ui/helpers.dart';
|
||||
import 'package:moxxyv2/ui/pages/conversation/blink.dart';
|
||||
import 'package:moxxyv2/ui/widgets/chat/media/media.dart';
|
||||
import 'package:moxxyv2/ui/widgets/textfield.dart';
|
||||
import 'package:phosphor_flutter/phosphor_flutter.dart';
|
||||
|
||||
class ConversationBottomRow extends StatelessWidget {
|
||||
const ConversationBottomRow(this.controller, this.isSpeedDialOpen, { super.key });
|
||||
final TextEditingController controller;
|
||||
final ValueNotifier<bool> isSpeedDialOpen;
|
||||
class _TextFieldIconButton extends StatelessWidget {
|
||||
const _TextFieldIconButton(this.icon, this.onTap);
|
||||
final void Function() onTap;
|
||||
final IconData icon;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: Icon(
|
||||
icon,
|
||||
size: 20,
|
||||
color: primaryColor,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ConversationBottomRow extends StatefulWidget {
|
||||
const ConversationBottomRow(
|
||||
this.controller,
|
||||
this.focusNode, {
|
||||
super.key,
|
||||
}
|
||||
);
|
||||
final TextEditingController controller;
|
||||
final FocusNode focusNode;
|
||||
|
||||
@override
|
||||
ConversationBottomRowState createState() => ConversationBottomRowState();
|
||||
}
|
||||
|
||||
class ConversationBottomRowState extends State<ConversationBottomRow> {
|
||||
Color _getTextColor(BuildContext context) {
|
||||
// TODO(Unknown): Work on the colors
|
||||
if (MediaQuery.of(context).platformBrightness == Brightness.dark) {
|
||||
@ -25,238 +57,288 @@ class ConversationBottomRow extends StatelessWidget {
|
||||
return Colors.black;
|
||||
}
|
||||
|
||||
bool _shouldCancelEdit(ConversationState state) {
|
||||
return state.messageEditing && controller.text == state.messageEditingOriginalBody;
|
||||
}
|
||||
|
||||
IconData _getSpeeddialIcon(ConversationState state) {
|
||||
if (_shouldCancelEdit(state)) return Icons.clear;
|
||||
|
||||
return state.showSendButton ? Icons.send : Icons.add;
|
||||
IconData _getSendButtonIcon(ConversationState state) {
|
||||
switch (state.sendButtonState) {
|
||||
case SendButtonState.audio: return Icons.mic;
|
||||
case SendButtonState.send: return Icons.send;
|
||||
case SendButtonState.cancelCorrection: return Icons.clear;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ColoredBox(
|
||||
color: const Color.fromRGBO(0, 0, 0, 0),
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: BlocBuilder<ConversationBloc, ConversationState>(
|
||||
buildWhen: (prev, next) => prev.showSendButton != next.showSendButton || prev.quotedMessage != next.quotedMessage || prev.emojiPickerVisible != next.emojiPickerVisible || prev.messageText != next.messageText || prev.messageEditing != next.messageEditing || prev.messageEditingOriginalBody != next.messageEditingOriginalBody,
|
||||
builder: (context, state) => Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: CustomTextField(
|
||||
// TODO(Unknown): Work on the colors
|
||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
textColor: _getTextColor(context),
|
||||
enableBoxShadow: true,
|
||||
maxLines: 5,
|
||||
hintText: 'Send a message...',
|
||||
isDense: true,
|
||||
onChanged: (value) {
|
||||
context.read<ConversationBloc>().add(
|
||||
MessageTextChangedEvent(value),
|
||||
);
|
||||
},
|
||||
contentPadding: textfieldPaddingConversation,
|
||||
cornerRadius: textfieldRadiusConversation,
|
||||
controller: controller,
|
||||
topWidget: state.quotedMessage != null ? buildQuoteMessageWidget(
|
||||
state.quotedMessage!,
|
||||
isSent(state.quotedMessage!, state.jid),
|
||||
resetQuote: () => context.read<ConversationBloc>().add(QuoteRemovedEvent()),
|
||||
) : null,
|
||||
shouldSummonKeyboard: () => !state.emojiPickerVisible,
|
||||
prefixIcon: IntrinsicWidth(
|
||||
child: Row(
|
||||
children: [
|
||||
InkWell(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: Icon(
|
||||
state.emojiPickerVisible ?
|
||||
Icons.keyboard :
|
||||
Icons.insert_emoticon,
|
||||
color: primaryColor,
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
context.read<ConversationBloc>().add(EmojiPickerToggledEvent());
|
||||
},
|
||||
),
|
||||
Visibility(
|
||||
visible: state.messageText.isEmpty,
|
||||
child: InkWell(
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.only(right: 8),
|
||||
child: Icon(
|
||||
PhosphorIcons.stickerBold,
|
||||
size: 24,
|
||||
color: primaryColor,
|
||||
return Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Positioned(
|
||||
child: ColoredBox(
|
||||
color: Colors.transparent,
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: BlocBuilder<ConversationBloc, ConversationState>(
|
||||
buildWhen: (prev, next) => prev.sendButtonState != next.sendButtonState || prev.quotedMessage != next.quotedMessage || prev.emojiPickerVisible != next.emojiPickerVisible || prev.messageText != next.messageText || prev.messageEditing != next.messageEditing || prev.messageEditingOriginalBody != next.messageEditingOriginalBody,
|
||||
builder: (context, state) => Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: CustomTextField(
|
||||
// TODO(Unknown): Work on the colors
|
||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
textColor: _getTextColor(context),
|
||||
enableBoxShadow: true,
|
||||
maxLines: 5,
|
||||
hintText: 'Send a message...',
|
||||
isDense: true,
|
||||
onChanged: (value) {
|
||||
context.read<ConversationBloc>().add(
|
||||
MessageTextChangedEvent(value),
|
||||
);
|
||||
},
|
||||
contentPadding: textfieldPaddingConversation,
|
||||
cornerRadius: textfieldRadiusConversation,
|
||||
controller: widget.controller,
|
||||
topWidget: state.quotedMessage != null ? buildQuoteMessageWidget(
|
||||
state.quotedMessage!,
|
||||
isSent(state.quotedMessage!, state.jid),
|
||||
resetQuote: () => context.read<ConversationBloc>().add(QuoteRemovedEvent()),
|
||||
) : null,
|
||||
focusNode: widget.focusNode,
|
||||
shouldSummonKeyboard: () => !state.emojiPickerVisible,
|
||||
prefixIcon: IntrinsicWidth(
|
||||
child: Row(
|
||||
children: [
|
||||
InkWell(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: Icon(
|
||||
state.emojiPickerVisible ?
|
||||
Icons.keyboard :
|
||||
Icons.insert_emoticon,
|
||||
color: primaryColor,
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
context.read<ConversationBloc>().add(EmojiPickerToggledEvent());
|
||||
},
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
showNotImplementedDialog('stickers', context);
|
||||
},
|
||||
Visibility(
|
||||
visible: state.messageText.isEmpty && state.quotedMessage == null,
|
||||
child: InkWell(
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.only(right: 8),
|
||||
child: Icon(
|
||||
PhosphorIcons.stickerBold,
|
||||
size: 24,
|
||||
color: primaryColor,
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
showNotImplementedDialog('stickers', context);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
prefixIconConstraints: const BoxConstraints(
|
||||
minWidth: 24,
|
||||
minHeight: 24,
|
||||
),
|
||||
suffixIcon: state.messageText.isEmpty ?
|
||||
InkWell(
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 8),
|
||||
child: Icon(
|
||||
Icons.mic_rounded,
|
||||
color: primaryColor,
|
||||
size: 24,
|
||||
prefixIconConstraints: const BoxConstraints(
|
||||
minWidth: 24,
|
||||
minHeight: 24,
|
||||
),
|
||||
suffixIcon: state.messageText.isEmpty && state.quotedMessage == null ?
|
||||
IntrinsicWidth(
|
||||
child: Row(
|
||||
children: [
|
||||
_TextFieldIconButton(
|
||||
Icons.attach_file,
|
||||
() {
|
||||
context.read<ConversationBloc>().add(
|
||||
FilePickerRequestedEvent(),
|
||||
);
|
||||
},
|
||||
),
|
||||
_TextFieldIconButton(
|
||||
Icons.photo_camera,
|
||||
() {
|
||||
showNotImplementedDialog(
|
||||
'taking photos',
|
||||
context,
|
||||
);
|
||||
},
|
||||
),
|
||||
_TextFieldIconButton(
|
||||
Icons.image,
|
||||
() {
|
||||
context.read<ConversationBloc>().add(
|
||||
ImagePickerRequestedEvent(),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
) :
|
||||
null,
|
||||
suffixIconConstraints: const BoxConstraints(
|
||||
minWidth: 24,
|
||||
minHeight: 24,
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
showNotImplementedDialog('audio recording', context);
|
||||
},
|
||||
) :
|
||||
null,
|
||||
suffixIconConstraints: const BoxConstraints(
|
||||
minWidth: 24,
|
||||
minHeight: 24,
|
||||
),
|
||||
),
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(left: 8),
|
||||
child: SizedBox(
|
||||
height: 45,
|
||||
width: 45,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 8),
|
||||
// NOTE: https://stackoverflow.com/a/52786741
|
||||
// Thank you kind sir
|
||||
),
|
||||
BlocBuilder<ConversationBloc, ConversationState>(
|
||||
buildWhen: (prev, next) => prev.emojiPickerVisible != next.emojiPickerVisible,
|
||||
builder: (context, state) => Offstage(
|
||||
offstage: !state.emojiPickerVisible,
|
||||
child: SizedBox(
|
||||
height: 45,
|
||||
width: 45,
|
||||
child: FittedBox(
|
||||
child: SpeedDial(
|
||||
icon: _getSpeeddialIcon(state),
|
||||
curve: Curves.bounceInOut,
|
||||
backgroundColor: primaryColor,
|
||||
// TODO(Unknown): Theme dependent?
|
||||
foregroundColor: Colors.white,
|
||||
openCloseDial: isSpeedDialOpen,
|
||||
onPress: () {
|
||||
if (_shouldCancelEdit(state)) {
|
||||
context.read<ConversationBloc>().add(MessageEditCancelledEvent());
|
||||
controller.text = '';
|
||||
return;
|
||||
}
|
||||
height: 250,
|
||||
child: EmojiPicker(
|
||||
onEmojiSelected: (_, emoji) {
|
||||
final bloc = context.read<ConversationBloc>();
|
||||
final selection = widget.controller.selection;
|
||||
final baseOffset = max(selection.baseOffset, 0);
|
||||
final extentOffset = max(selection.extentOffset, 0);
|
||||
final prefix = bloc.state.messageText.substring(0, baseOffset);
|
||||
final suffix = bloc.state.messageText.substring(extentOffset);
|
||||
final newText = '$prefix${emoji.emoji}$suffix';
|
||||
final newValue = baseOffset + emoji.emoji.codeUnits.length;
|
||||
bloc.add(MessageTextChangedEvent(newText));
|
||||
widget.controller
|
||||
..text = newText
|
||||
..selection = TextSelection(
|
||||
baseOffset: newValue,
|
||||
extentOffset: newValue,
|
||||
);
|
||||
},
|
||||
onBackspacePressed: () {
|
||||
// Taken from https://github.com/Fintasys/emoji_picker_flutter/blob/master/lib/src/emoji_picker.dart#L183
|
||||
final bloc = context.read<ConversationBloc>();
|
||||
final text = bloc.state.messageText;
|
||||
final selection = widget.controller.selection;
|
||||
final cursorPosition = widget.controller.selection.base.offset;
|
||||
|
||||
if (state.showSendButton) {
|
||||
context.read<ConversationBloc>().add(MessageSentEvent());
|
||||
controller.text = '';
|
||||
} else {
|
||||
isSpeedDialOpen.value = true;
|
||||
}
|
||||
},
|
||||
children: [
|
||||
SpeedDialChild(
|
||||
child: const Icon(Icons.image),
|
||||
onTap: () {
|
||||
context.read<ConversationBloc>().add(ImagePickerRequestedEvent());
|
||||
},
|
||||
backgroundColor: primaryColor,
|
||||
// TODO(Unknown): Theme dependent?
|
||||
foregroundColor: Colors.white,
|
||||
label: 'Send Images',
|
||||
),
|
||||
SpeedDialChild(
|
||||
child: const Icon(Icons.photo_camera),
|
||||
onTap: () {
|
||||
showNotImplementedDialog('taking photos', context);
|
||||
},
|
||||
backgroundColor: primaryColor,
|
||||
// TODO(Unknown): Theme dependent?
|
||||
foregroundColor: Colors.white,
|
||||
label: 'Take photo',
|
||||
),
|
||||
SpeedDialChild(
|
||||
child: const Icon(Icons.attach_file),
|
||||
onTap: () {
|
||||
context.read<ConversationBloc>().add(FilePickerRequestedEvent());
|
||||
},
|
||||
backgroundColor: primaryColor,
|
||||
// TODO(Unknown): Theme dependent?
|
||||
foregroundColor: Colors.white,
|
||||
label: 'Send files',
|
||||
)
|
||||
],
|
||||
if (cursorPosition < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
final newTextBeforeCursor = selection
|
||||
.textBefore(text).characters
|
||||
.skipLast(1)
|
||||
.toString();
|
||||
|
||||
bloc.add(MessageTextChangedEvent(newTextBeforeCursor));
|
||||
widget.controller
|
||||
..text = newTextBeforeCursor
|
||||
..selection = TextSelection.fromPosition(
|
||||
TextPosition(offset: newTextBeforeCursor.length),
|
||||
);
|
||||
},
|
||||
config: Config(
|
||||
bgColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
BlocBuilder<ConversationBloc, ConversationState>(
|
||||
buildWhen: (prev, next) => prev.emojiPickerVisible != next.emojiPickerVisible,
|
||||
builder: (context, state) => Offstage(
|
||||
offstage: !state.emojiPickerVisible,
|
||||
child: SizedBox(
|
||||
height: 250,
|
||||
child: EmojiPicker(
|
||||
onEmojiSelected: (_, emoji) {
|
||||
final bloc = context.read<ConversationBloc>();
|
||||
final selection = controller.selection;
|
||||
final baseOffset = max(selection.baseOffset, 0);
|
||||
final extentOffset = max(selection.extentOffset, 0);
|
||||
final prefix = bloc.state.messageText.substring(0, baseOffset);
|
||||
final suffix = bloc.state.messageText.substring(extentOffset);
|
||||
final newText = '$prefix${emoji.emoji}$suffix';
|
||||
final newValue = baseOffset + emoji.emoji.codeUnits.length;
|
||||
bloc.add(MessageTextChangedEvent(newText));
|
||||
controller
|
||||
..text = newText
|
||||
..selection = TextSelection(
|
||||
baseOffset: newValue,
|
||||
extentOffset: newValue,
|
||||
);
|
||||
},
|
||||
onBackspacePressed: () {
|
||||
// Taken from https://github.com/Fintasys/emoji_picker_flutter/blob/master/lib/src/emoji_picker.dart#L183
|
||||
final bloc = context.read<ConversationBloc>();
|
||||
final text = bloc.state.messageText;
|
||||
final selection = controller.selection;
|
||||
final cursorPosition = controller.selection.base.offset;
|
||||
|
||||
if (cursorPosition < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
final newTextBeforeCursor = selection
|
||||
.textBefore(text).characters
|
||||
.skipLast(1)
|
||||
.toString();
|
||||
|
||||
bloc.add(MessageTextChangedEvent(newTextBeforeCursor));
|
||||
controller
|
||||
..text = newTextBeforeCursor
|
||||
..selection = TextSelection.fromPosition(
|
||||
TextPosition(offset: newTextBeforeCursor.length),
|
||||
);
|
||||
},
|
||||
config: Config(
|
||||
bgColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
Positioned(
|
||||
right: 8,
|
||||
bottom: 8,
|
||||
child: BlocBuilder<ConversationBloc, ConversationState>(
|
||||
buildWhen: (prev, next) => prev.sendButtonState != next.sendButtonState ||
|
||||
prev.isDragging != next.isDragging ||
|
||||
prev.isLocked != next.isLocked,
|
||||
builder: (context, state) {
|
||||
return Visibility(
|
||||
visible: !state.isDragging && !state.isLocked,
|
||||
child: LongPressDraggable<int>(
|
||||
data: 1,
|
||||
axis: Axis.vertical,
|
||||
onDragStarted: () {
|
||||
Vibrate.feedback(FeedbackType.heavy);
|
||||
context.read<ConversationBloc>().add(
|
||||
SendButtonDragStartedEvent(),
|
||||
);
|
||||
},
|
||||
onDraggableCanceled: (_, __) {
|
||||
Vibrate.feedback(FeedbackType.heavy);
|
||||
context.read<ConversationBloc>().add(
|
||||
SendButtonDragEndedEvent(),
|
||||
);
|
||||
},
|
||||
onDragCompleted: () {
|
||||
context.read<ConversationBloc>().add(
|
||||
SendButtonLockedEvent(),
|
||||
);
|
||||
},
|
||||
feedback: SizedBox(
|
||||
height: 45,
|
||||
width: 45,
|
||||
child: FloatingActionButton(
|
||||
onPressed: null,
|
||||
heroTag: 'fabDragged',
|
||||
backgroundColor: Colors.red.shade600,
|
||||
child: const BlinkingMicrophoneIcon(),
|
||||
),
|
||||
),
|
||||
childWhenDragging: SizedBox(
|
||||
height: 45,
|
||||
width: 45,
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey,
|
||||
borderRadius: BorderRadius.circular(45),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: SizedBox(
|
||||
height: 45,
|
||||
width: 45,
|
||||
child: FloatingActionButton(
|
||||
heroTag: 'fabRest',
|
||||
onPressed: () {
|
||||
switch (state.sendButtonState) {
|
||||
case SendButtonState.audio: return;
|
||||
case SendButtonState.cancelCorrection:
|
||||
context.read<ConversationBloc>().add(
|
||||
MessageEditCancelledEvent(),
|
||||
);
|
||||
widget.controller.text = '';
|
||||
return;
|
||||
case SendButtonState.send:
|
||||
context.read<ConversationBloc>().add(
|
||||
MessageSentEvent(),
|
||||
);
|
||||
widget.controller.text = '';
|
||||
return;
|
||||
}
|
||||
},
|
||||
child: Icon(
|
||||
_getSendButtonIcon(state),
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ import 'package:moxxyv2/shared/warning_types.dart';
|
||||
import 'package:moxxyv2/ui/bloc/conversation_bloc.dart';
|
||||
import 'package:moxxyv2/ui/constants.dart';
|
||||
import 'package:moxxyv2/ui/helpers.dart';
|
||||
import 'package:moxxyv2/ui/pages/conversation/blink.dart';
|
||||
import 'package:moxxyv2/ui/pages/conversation/bottom.dart';
|
||||
import 'package:moxxyv2/ui/pages/conversation/helpers.dart';
|
||||
import 'package:moxxyv2/ui/pages/conversation/topbar.dart';
|
||||
@ -34,23 +35,23 @@ class ConversationPage extends StatefulWidget {
|
||||
|
||||
class ConversationPageState extends State<ConversationPage> with TickerProviderStateMixin {
|
||||
ConversationPageState() :
|
||||
_isSpeedDialOpen = ValueNotifier(false),
|
||||
_controller = TextEditingController(),
|
||||
_scrollController = ScrollController(),
|
||||
_scrolledToBottomState = true,
|
||||
super();
|
||||
final TextEditingController _controller;
|
||||
final ValueNotifier<bool> _isSpeedDialOpen;
|
||||
final ScrollController _scrollController;
|
||||
late final AnimationController _animationController;
|
||||
late final AnimationController _overviewAnimationController;
|
||||
late Animation<double> _overviewMsgAnimation;
|
||||
late final Animation<double> _scrollToBottom;
|
||||
bool _scrolledToBottomState;
|
||||
late FocusNode _textfieldFocus;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_textfieldFocus = FocusNode();
|
||||
_scrollController.addListener(_onScroll);
|
||||
|
||||
_overviewAnimationController = AnimationController(
|
||||
@ -77,7 +78,7 @@ class ConversationPageState extends State<ConversationPage> with TickerProviderS
|
||||
..dispose();
|
||||
_animationController.dispose();
|
||||
_overviewAnimationController.dispose();
|
||||
|
||||
_textfieldFocus.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@ -327,9 +328,16 @@ class ConversationPageState extends State<ConversationPage> with TickerProviderS
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final maxWidth = MediaQuery.of(context).size.width * 0.6;
|
||||
|
||||
|
||||
return WillPopScope(
|
||||
onWillPop: () async {
|
||||
// TODO(PapaTutuWawa): Check if we are recording an audio message and handle
|
||||
// that accordingly
|
||||
if (_textfieldFocus.hasFocus) {
|
||||
_textfieldFocus.unfocus();
|
||||
return false;
|
||||
}
|
||||
|
||||
final bloc = GetIt.I.get<ConversationBloc>();
|
||||
|
||||
if (bloc.state.emojiPickerVisible) {
|
||||
@ -417,7 +425,10 @@ class ConversationPageState extends State<ConversationPage> with TickerProviderS
|
||||
),
|
||||
),
|
||||
|
||||
ConversationBottomRow(_controller, _isSpeedDialOpen)
|
||||
ConversationBottomRow(
|
||||
_controller,
|
||||
_textfieldFocus,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -446,6 +457,44 @@ class ConversationPageState extends State<ConversationPage> with TickerProviderS
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
Positioned(
|
||||
right: 8,
|
||||
bottom: 300,
|
||||
child: BlocBuilder<ConversationBloc, ConversationState>(
|
||||
builder: (context, state) {
|
||||
return DragTarget<int>(
|
||||
onWillAccept: (data) => true,
|
||||
builder: (context, _, __) {
|
||||
return AnimatedScale(
|
||||
scale: state.isDragging || state.isLocked ? 1 : 0,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
child: SizedBox(
|
||||
height: 45,
|
||||
width: 45,
|
||||
child: FloatingActionButton(
|
||||
heroTag: 'fabLock',
|
||||
onPressed: state.isLocked ?
|
||||
() {
|
||||
context.read<ConversationBloc>().add(
|
||||
SendButtonLockPressedEvent(),
|
||||
);
|
||||
} :
|
||||
null,
|
||||
backgroundColor: state.isLocked ?
|
||||
Colors.red.shade600 :
|
||||
Colors.grey,
|
||||
child: state.isLocked ?
|
||||
const BlinkingMicrophoneIcon() :
|
||||
const Icon(Icons.lock, color: Colors.white),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
@ -34,12 +34,18 @@ class ConversationsPage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class ConversationsPageState extends State<ConversationsPage> with TickerProviderStateMixin {
|
||||
late final AnimationController _controller = AnimationController(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
vsync: this,
|
||||
);
|
||||
late final AnimationController _controller;
|
||||
late Animation<double> _convY;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
vsync: this,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
|
@ -65,6 +65,7 @@ class SendFilesPage extends StatelessWidget {
|
||||
path,
|
||||
// TODO(PapaTutuWawa): Fix
|
||||
'sendfiles',
|
||||
mime,
|
||||
onTap: () {
|
||||
if (selected) {
|
||||
// The trash can icon has been tapped
|
||||
|
@ -28,6 +28,85 @@ enum _AudioPlaybackState {
|
||||
stopped
|
||||
}
|
||||
|
||||
class _AudioWidget extends StatelessWidget {
|
||||
const _AudioWidget(
|
||||
this.maxWidth,
|
||||
this.isDownloading,
|
||||
this.onTap,
|
||||
this.icon,
|
||||
this.onChanged,
|
||||
this.duration,
|
||||
this.position,
|
||||
this.messageId,
|
||||
);
|
||||
final double maxWidth;
|
||||
final bool isDownloading;
|
||||
final void Function() onTap;
|
||||
final void Function(double) onChanged;
|
||||
final double? duration;
|
||||
final double? position;
|
||||
final Widget? icon;
|
||||
final int messageId;
|
||||
|
||||
Widget _getLeftWidget() {
|
||||
if (isDownloading) {
|
||||
return SizedBox(
|
||||
width: 48,
|
||||
height: 48,
|
||||
child: ProgressWidget(id: messageId),
|
||||
);
|
||||
}
|
||||
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 8,
|
||||
right: 4,
|
||||
),
|
||||
child: icon,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
width: maxWidth,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_getLeftWidget(),
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Slider(
|
||||
onChanged: onChanged,
|
||||
value: (position != null && duration != null) ?
|
||||
(position! / duration!).clamp(0, 1) :
|
||||
0,
|
||||
),
|
||||
|
||||
SizedBox(
|
||||
width: maxWidth - 80,
|
||||
child: Row(
|
||||
children: [
|
||||
Text(doubleToTimestamp(position ?? 0)),
|
||||
const Spacer(),
|
||||
Text(doubleToTimestamp(duration ?? 0)),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AudioChatWidget extends StatefulWidget {
|
||||
const AudioChatWidget(
|
||||
this.message,
|
||||
@ -94,100 +173,79 @@ class AudioChatState extends State<AudioChatWidget> {
|
||||
}
|
||||
|
||||
Widget _buildUploading() {
|
||||
// TODO(PapaTutuWawa): Fix
|
||||
return FileChatWidget(
|
||||
widget.message,
|
||||
return MediaBaseChatWidget(
|
||||
_AudioWidget(
|
||||
widget.maxWidth,
|
||||
true,
|
||||
() {},
|
||||
null,
|
||||
(_) {},
|
||||
null,
|
||||
null,
|
||||
widget.message.id,
|
||||
),
|
||||
MessageBubbleBottom(widget.message, widget.sent),
|
||||
widget.radius,
|
||||
widget.sent,
|
||||
extra: ProgressWidget(id: widget.message.id),
|
||||
gradient: false,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDownloading() {
|
||||
// TODO(PapaTutuWawa): Fix
|
||||
return FileChatBaseWidget(
|
||||
widget.message,
|
||||
Icons.image,
|
||||
widget.message.isFileUploadNotification ?
|
||||
(widget.message.filename ?? '') :
|
||||
filenameFromUrl(widget.message.srcUrl!),
|
||||
return MediaBaseChatWidget(
|
||||
_AudioWidget(
|
||||
widget.maxWidth,
|
||||
true,
|
||||
() {},
|
||||
null,
|
||||
(_) {},
|
||||
null,
|
||||
null,
|
||||
widget.message.id,
|
||||
),
|
||||
MessageBubbleBottom(widget.message, widget.sent),
|
||||
widget.radius,
|
||||
widget.sent,
|
||||
extra: ProgressWidget(id: widget.message.id),
|
||||
gradient: false,
|
||||
);
|
||||
}
|
||||
|
||||
/// 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();
|
||||
}
|
||||
_AudioWidget(
|
||||
widget.maxWidth,
|
||||
false,
|
||||
() {
|
||||
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;
|
||||
});
|
||||
}
|
||||
setState(() {
|
||||
_playState = _AudioPlaybackState.playing;
|
||||
});
|
||||
} else {
|
||||
_audioFile?.pause();
|
||||
setState(() {
|
||||
_playState = _AudioPlaybackState.paused;
|
||||
});
|
||||
}
|
||||
},
|
||||
_playState == _AudioPlaybackState.playing ?
|
||||
const Icon(Icons.pause) :
|
||||
const Icon(Icons.play_arrow),
|
||||
(p) {
|
||||
if (_duration == null || _audioFile == null) return;
|
||||
|
||||
},
|
||||
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),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
setState(() {
|
||||
_position = p * _duration!;
|
||||
_audioFile!.seek(_position!);
|
||||
});
|
||||
},
|
||||
_duration,
|
||||
_position,
|
||||
widget.message.id,
|
||||
),
|
||||
MessageBubbleBottom(widget.message, widget.sent),
|
||||
widget.radius,
|
||||
@ -196,6 +254,7 @@ class AudioChatState extends State<AudioChatWidget> {
|
||||
}
|
||||
|
||||
Widget _buildDownloadable() {
|
||||
// TODO(Unknown): Implement
|
||||
return FileChatBaseWidget(
|
||||
widget.message,
|
||||
Icons.image,
|
||||
@ -222,7 +281,7 @@ class AudioChatState extends State<AudioChatWidget> {
|
||||
|
||||
// TODO(PapaTutuWawa): Maybe use an async builder
|
||||
if (widget.message.mediaUrl != null && File(widget.message.mediaUrl!).existsSync()) return _buildAudio();
|
||||
|
||||
|
||||
return _buildDownloadable();
|
||||
}
|
||||
}
|
||||
|
@ -104,11 +104,7 @@ Widget buildQuoteMessageWidget(Message message, bool sent, { void Function()? re
|
||||
}
|
||||
|
||||
Widget buildSharedMediaWidget(SharedMedium medium, String conversationJid) {
|
||||
if (medium.mime == null) {
|
||||
return SharedFileWidget(
|
||||
medium.path,
|
||||
);
|
||||
} else if (medium.mime!.startsWith('image/')) {
|
||||
if (medium.mime!.startsWith('image/')) {
|
||||
return SharedImageWidget(
|
||||
medium.path,
|
||||
onTap: () => OpenFile.open(medium.path),
|
||||
@ -117,6 +113,7 @@ Widget buildSharedMediaWidget(SharedMedium medium, String conversationJid) {
|
||||
return SharedVideoWidget(
|
||||
medium.path,
|
||||
conversationJid,
|
||||
medium.mime!,
|
||||
onTap: () => OpenFile.open(medium.path),
|
||||
child: const PlayButton(size: 32),
|
||||
);
|
||||
@ -127,5 +124,8 @@ Widget buildSharedMediaWidget(SharedMedium medium, String conversationJid) {
|
||||
);
|
||||
}
|
||||
|
||||
return SharedFileWidget(medium.path);
|
||||
return SharedFileWidget(
|
||||
medium.path,
|
||||
onTap: () => OpenFile.open(medium.path),
|
||||
);
|
||||
}
|
||||
|
@ -35,6 +35,7 @@ class VideoChatWidget extends StatelessWidget {
|
||||
VideoThumbnail(
|
||||
path: message.mediaUrl!,
|
||||
conversationJid: message.conversationJid,
|
||||
mime: message.mediaType!,
|
||||
size: Size(
|
||||
maxWidth,
|
||||
0.6 * maxWidth,
|
||||
@ -83,6 +84,7 @@ class VideoChatWidget extends StatelessWidget {
|
||||
VideoThumbnail(
|
||||
path: message.mediaUrl!,
|
||||
conversationJid: message.conversationJid,
|
||||
mime: message.mediaType!,
|
||||
size: Size(
|
||||
maxWidth,
|
||||
0.6 * maxWidth,
|
||||
|
@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:moxxyv2/i18n/strings.g.dart';
|
||||
import 'package:moxxyv2/shared/models/message.dart';
|
||||
import 'package:moxxyv2/ui/widgets/chat/quote/media.dart';
|
||||
import 'package:moxxyv2/ui/widgets/chat/shared/audio.dart';
|
||||
@ -25,7 +26,7 @@ class QuotedAudioWidget extends StatelessWidget {
|
||||
borderRadius: 8,
|
||||
),
|
||||
// TODO(Unknown): Include the audio messages duration here
|
||||
'Audio',
|
||||
t.messages.audio,
|
||||
sent,
|
||||
resetQuote: resetQuote,
|
||||
);
|
||||
|
@ -22,7 +22,6 @@ class QuotedFileWidget extends StatelessWidget {
|
||||
message,
|
||||
SharedFileWidget(
|
||||
message.mediaUrl!,
|
||||
enableOnTap: false,
|
||||
size: 48,
|
||||
borderRadius: 8,
|
||||
),
|
||||
|
@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:moxxyv2/i18n/strings.g.dart';
|
||||
import 'package:moxxyv2/shared/models/message.dart';
|
||||
import 'package:moxxyv2/ui/widgets/chat/quote/media.dart';
|
||||
import 'package:moxxyv2/ui/widgets/chat/shared/image.dart';
|
||||
@ -24,7 +25,7 @@ class QuotedImageWidget extends StatelessWidget {
|
||||
size: 48,
|
||||
borderRadius: 8,
|
||||
),
|
||||
'Image',
|
||||
t.messages.image,
|
||||
sent,
|
||||
resetQuote: resetQuote,
|
||||
);
|
||||
|
@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:moxxyv2/i18n/strings.g.dart';
|
||||
import 'package:moxxyv2/shared/models/message.dart';
|
||||
import 'package:moxxyv2/ui/widgets/chat/quote/media.dart';
|
||||
import 'package:moxxyv2/ui/widgets/chat/shared/video.dart';
|
||||
@ -22,10 +23,11 @@ class QuotedVideoWidget extends StatelessWidget {
|
||||
SharedVideoWidget(
|
||||
message.mediaUrl!,
|
||||
message.conversationJid,
|
||||
message.mediaType!,
|
||||
size: 48,
|
||||
borderRadius: 8,
|
||||
),
|
||||
'Video',
|
||||
t.messages.video,
|
||||
sent,
|
||||
resetQuote: resetQuote,
|
||||
);
|
||||
|
@ -1,18 +1,17 @@
|
||||
import 'package:better_open_file/better_open_file.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:moxxyv2/ui/widgets/chat/shared/base.dart';
|
||||
|
||||
class SharedFileWidget extends StatelessWidget {
|
||||
const SharedFileWidget(
|
||||
this.path, {
|
||||
this.enableOnTap = true,
|
||||
this.onTap,
|
||||
this.borderRadius = 10,
|
||||
this.size = sharedMediaContainerDimension,
|
||||
super.key,
|
||||
}
|
||||
);
|
||||
final String path;
|
||||
final bool enableOnTap;
|
||||
final void Function()? onTap;
|
||||
final double borderRadius;
|
||||
final double size;
|
||||
|
||||
@ -30,9 +29,7 @@ class SharedFileWidget extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
size: size,
|
||||
onTap: enableOnTap ?
|
||||
() => OpenFile.open(path) :
|
||||
null,
|
||||
onTap: onTap,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,8 @@ import 'package:moxxyv2/ui/widgets/chat/video_thumbnail.dart';
|
||||
class SharedVideoWidget extends StatelessWidget {
|
||||
const SharedVideoWidget(
|
||||
this.path,
|
||||
this.conversationJid, {
|
||||
this.conversationJid,
|
||||
this.mime, {
|
||||
this.onTap,
|
||||
this.borderColor,
|
||||
this.child,
|
||||
@ -16,6 +17,7 @@ class SharedVideoWidget extends StatelessWidget {
|
||||
);
|
||||
final String path;
|
||||
final String conversationJid;
|
||||
final String mime;
|
||||
final Color? borderColor;
|
||||
final void Function()? onTap;
|
||||
final Widget? child;
|
||||
@ -28,6 +30,7 @@ class SharedVideoWidget extends StatelessWidget {
|
||||
VideoThumbnail(
|
||||
path: path,
|
||||
conversationJid: conversationJid,
|
||||
mime: mime,
|
||||
size: Size(
|
||||
size,
|
||||
size,
|
||||
|
@ -9,25 +9,27 @@ class VideoThumbnail extends StatelessWidget {
|
||||
required this.conversationJid,
|
||||
required this.size,
|
||||
required this.borderRadius,
|
||||
required this.mime,
|
||||
super.key,
|
||||
});
|
||||
final String path;
|
||||
final String conversationJid;
|
||||
final Size size;
|
||||
final BorderRadius borderRadius;
|
||||
final String mime;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FutureBuilder<String>(
|
||||
future: getVideoThumbnailPath(path, conversationJid),
|
||||
return FutureBuilder<String?>(
|
||||
future: getVideoThumbnailPath(path, conversationJid, mime),
|
||||
builder: (context, snapshot) {
|
||||
Widget widget;
|
||||
if (snapshot.hasData) {
|
||||
if (snapshot.hasData && snapshot.data != null) {
|
||||
widget = Image.file(
|
||||
File(snapshot.data!),
|
||||
fit: BoxFit.cover,
|
||||
);
|
||||
} else if (snapshot.hasError) {
|
||||
} else if (snapshot.hasError || snapshot.hasData && snapshot.data == null) {
|
||||
widget = SizedBox(
|
||||
width: size.width,
|
||||
height: size.height,
|
||||
|
@ -112,6 +112,7 @@ class ConversationsListRowState extends State<ConversationsListRow> {
|
||||
preview = SharedVideoWidget(
|
||||
widget.conversation.lastMessage!.mediaUrl!,
|
||||
widget.conversation.jid,
|
||||
widget.conversation.lastMessage!.mediaType!,
|
||||
borderRadius: 5,
|
||||
size: 30,
|
||||
);
|
||||
|
@ -30,6 +30,7 @@ class CustomTextField extends StatelessWidget {
|
||||
this.suffixIconConstraints,
|
||||
this.onTap,
|
||||
this.shouldSummonKeyboard,
|
||||
this.focusNode,
|
||||
super.key,
|
||||
});
|
||||
final double cornerRadius;
|
||||
@ -59,6 +60,7 @@ class CustomTextField extends StatelessWidget {
|
||||
final ValueChanged<String>? onChanged;
|
||||
final void Function()? onTap;
|
||||
final bool Function()? shouldSummonKeyboard;
|
||||
final FocusNode? focusNode;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -91,6 +93,7 @@ class CustomTextField extends StatelessWidget {
|
||||
style: style,
|
||||
onTap: onTap,
|
||||
shouldSummonKeyboard: shouldSummonKeyboard,
|
||||
focusNode: focusNode,
|
||||
decoration: InputDecoration(
|
||||
labelText: labelText,
|
||||
hintText: hintText,
|
||||
|
Loading…
Reference in New Issue
Block a user