736 lines
26 KiB
Dart
736 lines
26 KiB
Dart
import 'dart:io';
|
|
import 'package:emoji_picker_flutter/emoji_picker_flutter.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
import 'package:flutter_vibrate/flutter_vibrate.dart';
|
|
import 'package:get_it/get_it.dart';
|
|
import 'package:moxxyv2/i18n/strings.g.dart';
|
|
import 'package:moxxyv2/shared/error_types.dart';
|
|
import 'package:moxxyv2/shared/helpers.dart';
|
|
import 'package:moxxyv2/shared/models/message.dart';
|
|
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';
|
|
import 'package:moxxyv2/ui/widgets/chat/bubbles/date.dart';
|
|
import 'package:moxxyv2/ui/widgets/chat/bubbles/new_device.dart';
|
|
import 'package:moxxyv2/ui/widgets/chat/chatbubble.dart';
|
|
import 'package:moxxyv2/ui/widgets/overview_menu.dart';
|
|
|
|
class ConversationPage extends StatefulWidget {
|
|
const ConversationPage({ super.key });
|
|
|
|
static MaterialPageRoute<dynamic> get route => MaterialPageRoute<dynamic>(
|
|
builder: (context) => const ConversationPage(),
|
|
settings: const RouteSettings(
|
|
name: conversationRoute,
|
|
),
|
|
);
|
|
|
|
@override
|
|
ConversationPageState createState() => ConversationPageState();
|
|
}
|
|
|
|
class ConversationPageState extends State<ConversationPage> with TickerProviderStateMixin {
|
|
ConversationPageState() :
|
|
_controller = TextEditingController(),
|
|
_scrollController = ScrollController(),
|
|
_scrolledToBottomState = true,
|
|
super();
|
|
final TextEditingController _controller;
|
|
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(
|
|
duration: const Duration(milliseconds: 200),
|
|
vsync: this,
|
|
);
|
|
|
|
// Values taken from here: https://stackoverflow.com/questions/45539395/flutter-float-action-button-hiding-the-visibility-of-items#45598028
|
|
_animationController = AnimationController(
|
|
duration: const Duration(milliseconds: 180),
|
|
vsync: this,
|
|
);
|
|
_scrollToBottom = CurvedAnimation(
|
|
parent: _animationController,
|
|
curve: const Interval(0.5, 1),
|
|
);
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_controller.dispose();
|
|
_scrollController
|
|
..removeListener(_onScroll)
|
|
..dispose();
|
|
_animationController.dispose();
|
|
_overviewAnimationController.dispose();
|
|
_textfieldFocus.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
void _quoteMessage(BuildContext context, Message message) {
|
|
context.read<ConversationBloc>().add(MessageQuotedEvent(message));
|
|
}
|
|
|
|
Future<void> _retractMessage(BuildContext context, String originId) async {
|
|
final result = await showConfirmationDialog(
|
|
t.pages.conversation.retract,
|
|
t.pages.conversation.retractBody,
|
|
context,
|
|
);
|
|
|
|
if (result) {
|
|
// ignore: use_build_context_synchronously
|
|
context.read<ConversationBloc>().add(
|
|
MessageRetractedEvent(originId),
|
|
);
|
|
|
|
// ignore: use_build_context_synchronously
|
|
Navigator.of(context).pop();
|
|
}
|
|
}
|
|
|
|
Widget _renderBubble(ConversationState state, BuildContext context, int _index, double maxWidth, String jid) {
|
|
if (_index.isEven) {
|
|
if (_index == 0) return const SizedBox();
|
|
|
|
final prevIndexRaw = (_index + 2) ~/ 2;
|
|
final prevIndex = state.messages.length - prevIndexRaw;
|
|
final prevMessageDateTime = prevIndex < 0 || prevIndexRaw == 0 ?
|
|
null :
|
|
DateTime.fromMillisecondsSinceEpoch(
|
|
state.messages[prevIndex].timestamp,
|
|
);
|
|
|
|
if (prevMessageDateTime == null) return const SizedBox();
|
|
|
|
final nextIndexRaw = _index ~/ 2;
|
|
final nextIndex = state.messages.length - nextIndexRaw;
|
|
final nextMessageDateTime = nextIndex < 0 || nextIndexRaw == 0 ?
|
|
null :
|
|
DateTime.fromMillisecondsSinceEpoch(
|
|
state.messages[nextIndex].timestamp,
|
|
);
|
|
if (nextMessageDateTime == null) return const SizedBox();
|
|
|
|
// Check if we have to render a date bubble
|
|
if (prevMessageDateTime.day != nextMessageDateTime.day ||
|
|
prevMessageDateTime.month != nextMessageDateTime.month ||
|
|
prevMessageDateTime.year != nextMessageDateTime.year) {
|
|
return Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
DateBubble(
|
|
formatDateBubble(nextMessageDateTime, DateTime.now()),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
return const SizedBox();
|
|
}
|
|
|
|
// TODO(Unknown): Since we reverse the list: Fix start, end and between
|
|
final index = state.messages.length - 1 - (_index - 1) ~/ 2;
|
|
final item = state.messages[index];
|
|
|
|
if (item.isPseudoMessage) {
|
|
return Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
ConstrainedBox(
|
|
constraints: BoxConstraints(
|
|
maxWidth: maxWidth,
|
|
),
|
|
child: NewDeviceBubble(
|
|
data: item.pseudoMessageData!,
|
|
title: state.conversation!.title,
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
final start = index - 1 < 0 ?
|
|
true :
|
|
isSent(state.messages[index - 1], jid) != isSent(item, jid);
|
|
final end = index + 1 >= state.messages.length ?
|
|
true :
|
|
isSent(state.messages[index + 1], jid) != isSent(item, jid);
|
|
final between = !start && !end;
|
|
final sentBySelf = isSent(item, jid);
|
|
|
|
final bubble = RawChatBubble(
|
|
item,
|
|
maxWidth,
|
|
sentBySelf,
|
|
state.conversation!.encrypted,
|
|
start,
|
|
between,
|
|
end,
|
|
);
|
|
|
|
return ChatBubble(
|
|
bubble: bubble,
|
|
message: item,
|
|
sentBySelf: sentBySelf,
|
|
maxWidth: maxWidth,
|
|
onSwipedCallback: (_) => _quoteMessage(context, item),
|
|
onReactionTap: (reaction) {
|
|
final bloc = context.read<ConversationBloc>();
|
|
if (reaction.reactedBySelf) {
|
|
bloc.add(
|
|
ReactionRemovedEvent(
|
|
reaction.emoji,
|
|
index,
|
|
),
|
|
);
|
|
} else {
|
|
bloc.add(
|
|
ReactionAddedEvent(
|
|
reaction.emoji,
|
|
index,
|
|
),
|
|
);
|
|
}
|
|
},
|
|
onLongPressed: (event) async {
|
|
if (!item.isLongpressable) {
|
|
return;
|
|
}
|
|
|
|
Vibrate.feedback(FeedbackType.medium);
|
|
|
|
_overviewMsgAnimation = Tween<double>(
|
|
begin: event.globalPosition.dy - 20,
|
|
end: 200,
|
|
).animate(
|
|
CurvedAnimation(
|
|
parent: _overviewAnimationController,
|
|
curve: Curves.easeInOutCubic,
|
|
),
|
|
);
|
|
// TODO(PapaTutuWawa): Animate the message to the center?
|
|
//_msgX = Tween<double>(
|
|
// begin: 8,
|
|
// end: (MediaQuery.of(context).size.width - obj.paintBounds.width) / 2,
|
|
//).animate(_controller);
|
|
|
|
await _overviewAnimationController.forward();
|
|
await showDialog<void>(
|
|
context: context,
|
|
builder: (context) => OverviewMenu(
|
|
_overviewMsgAnimation,
|
|
rightBorder: sentBySelf,
|
|
left: sentBySelf ? null : 8,
|
|
right: sentBySelf ? 8 : null,
|
|
highlightMaterialBorder: RawChatBubble.getBorderRadius(
|
|
sentBySelf,
|
|
start,
|
|
between,
|
|
end,
|
|
),
|
|
highlight: bubble,
|
|
materialColor: item.isSticker ?
|
|
Colors.transparent :
|
|
null,
|
|
children: [
|
|
...item.isReactable ? [
|
|
OverviewMenuItem(
|
|
icon: Icons.add_reaction,
|
|
text: t.pages.conversation.addReaction,
|
|
onPressed: () async {
|
|
final emoji = await showModalBottomSheet<String>(
|
|
context: context,
|
|
// TODO(PapaTutuWawa): Move this to the theme
|
|
shape: const RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.only(
|
|
topLeft: radiusLarge,
|
|
topRight: radiusLarge,
|
|
),
|
|
),
|
|
builder: (context) => Padding(
|
|
padding: const EdgeInsets.only(top: 12),
|
|
child: EmojiPicker(
|
|
onEmojiSelected: (_, emoji) {
|
|
// ignore: use_build_context_synchronously
|
|
Navigator.of(context).pop(emoji.emoji);
|
|
},
|
|
//height: 250,
|
|
config: Config(
|
|
bgColor: Theme.of(context).scaffoldBackgroundColor,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
if (emoji != null) {
|
|
// ignore: use_build_context_synchronously
|
|
context.read<ConversationBloc>().add(
|
|
ReactionAddedEvent(emoji, index),
|
|
);
|
|
}
|
|
|
|
// ignore: use_build_context_synchronously
|
|
Navigator.of(context).pop();
|
|
},
|
|
),
|
|
] : [],
|
|
...item.canRetract(sentBySelf) ? [
|
|
OverviewMenuItem(
|
|
icon: Icons.delete,
|
|
text: t.pages.conversation.retract,
|
|
onPressed: () => _retractMessage(context, item.originId!),
|
|
),
|
|
] : [],
|
|
// TODO(Unknown): Also allow correcting older messages
|
|
...item.canEdit(sentBySelf) && state.conversation!.lastMessage?.id == item.id ? [
|
|
OverviewMenuItem(
|
|
icon: Icons.edit,
|
|
text: t.pages.conversation.edit,
|
|
onPressed: () {
|
|
context.read<ConversationBloc>().add(
|
|
MessageEditSelectedEvent(item),
|
|
);
|
|
_controller.text = item.body;
|
|
Navigator.of(context).pop();
|
|
},
|
|
),
|
|
] : [],
|
|
...item.errorMenuVisible ? [
|
|
OverviewMenuItem(
|
|
icon: Icons.info_outline,
|
|
text: t.pages.conversation.showError,
|
|
onPressed: () {
|
|
showInfoDialog(
|
|
'Error',
|
|
errorToTranslatableString(item.errorType!),
|
|
context,
|
|
);
|
|
},
|
|
),
|
|
] : [],
|
|
...item.hasWarning ? [
|
|
OverviewMenuItem(
|
|
icon: Icons.warning,
|
|
text: t.pages.conversation.showWarning,
|
|
onPressed: () {
|
|
showInfoDialog(
|
|
'Warning',
|
|
warningToTranslatableString(item.warningType!),
|
|
context,
|
|
);
|
|
},
|
|
),
|
|
] : [],
|
|
...item.isCopyable ? [
|
|
OverviewMenuItem(
|
|
icon: Icons.content_copy,
|
|
text: t.pages.conversation.copy,
|
|
onPressed: () {
|
|
// TODO(Unknown): Show a toast saying the message has been copied
|
|
Clipboard.setData(ClipboardData(text: item.body));
|
|
Navigator.of(context).pop();
|
|
},
|
|
),
|
|
] : [],
|
|
...item.isQuotable ? [
|
|
OverviewMenuItem(
|
|
icon: Icons.forward,
|
|
text: t.pages.conversation.forward,
|
|
onPressed: () {
|
|
showNotImplementedDialog(
|
|
'sharing',
|
|
context,
|
|
);
|
|
},
|
|
),
|
|
] : [],
|
|
OverviewMenuItem(
|
|
icon: Icons.reply,
|
|
text: t.pages.conversation.quote,
|
|
onPressed: () {
|
|
_quoteMessage(context, item);
|
|
Navigator.of(context).pop();
|
|
},
|
|
),
|
|
],
|
|
),
|
|
);
|
|
|
|
await _overviewAnimationController.reverse();
|
|
},
|
|
);
|
|
}
|
|
|
|
/// Render a widget that allows the user to either block the user or add them to their
|
|
/// roster
|
|
Widget _renderNotInRosterWidget(ConversationState state, BuildContext context) {
|
|
return ColoredBox(
|
|
color: Colors.black38,
|
|
child: SizedBox(
|
|
height: 64,
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Expanded(
|
|
child: TextButton(
|
|
child: Text(t.pages.conversation.addToContacts),
|
|
onPressed: () async {
|
|
final jid = state.conversation!.jid;
|
|
final result = await showConfirmationDialog(
|
|
t.pages.conversation.addToContactsTitle(jid: jid),
|
|
t.pages.conversation.addToContactsBody(jid: jid),
|
|
context,
|
|
);
|
|
|
|
if (result) {
|
|
// TODO(Unknown): Maybe show a progress indicator
|
|
// TODO(Unknown): Have the page update its state once the addition is done
|
|
// ignore: use_build_context_synchronously
|
|
context.read<ConversationBloc>().add(
|
|
JidAddedEvent(jid),
|
|
);
|
|
|
|
// ignore: use_build_context_synchronously
|
|
Navigator.of(context).pop();
|
|
}
|
|
},
|
|
),
|
|
),
|
|
Expanded(
|
|
child: TextButton(
|
|
child: Text(t.pages.conversation.blockShort),
|
|
onPressed: () => blockJid(state.conversation!.jid, context),
|
|
),
|
|
)
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
/// Taken from https://bloclibrary.dev/#/flutterinfinitelisttutorial
|
|
bool _isScrolledToBottom() {
|
|
if (!_scrollController.hasClients) return false;
|
|
|
|
return _scrollController.offset <= 10;
|
|
}
|
|
|
|
void _onScroll() {
|
|
final isScrolledToBottom = _isScrolledToBottom();
|
|
if (isScrolledToBottom && !_scrolledToBottomState) {
|
|
_animationController.reverse();
|
|
} else if (!isScrolledToBottom && _scrolledToBottomState) {
|
|
_animationController.forward();
|
|
}
|
|
|
|
_scrolledToBottomState = isScrolledToBottom;
|
|
}
|
|
|
|
@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
|
|
final bloc = GetIt.I.get<ConversationBloc>();
|
|
if (bloc.state.isRecording) {
|
|
// TODO(PapaTutuWawa): Show a dialog
|
|
return true;
|
|
} else if (bloc.state.emojiPickerVisible) {
|
|
bloc.add(EmojiPickerToggledEvent(handleKeyboard: false));
|
|
|
|
return false;
|
|
} else if (bloc.state.stickerPickerVisible) {
|
|
bloc.add(StickerPickerToggledEvent());
|
|
if (_textfieldFocus.hasFocus) {
|
|
_textfieldFocus.unfocus();
|
|
}
|
|
|
|
return false;
|
|
} else {
|
|
bloc.add(CurrentConversationResetEvent());
|
|
|
|
return true;
|
|
}
|
|
},
|
|
child: Stack(
|
|
children: [
|
|
Positioned(
|
|
left: 0,
|
|
right: 0,
|
|
top: 0,
|
|
bottom: 0,
|
|
child: ColoredBox(color: Theme.of(context).scaffoldBackgroundColor),
|
|
),
|
|
Positioned(
|
|
left: 0,
|
|
right: 0,
|
|
bottom: 0,
|
|
child: BlocBuilder<ConversationBloc, ConversationState>(
|
|
buildWhen: (prev, next) => prev.backgroundPath != next.backgroundPath,
|
|
builder: (context, state) {
|
|
final query = MediaQuery.of(context);
|
|
|
|
if (state.backgroundPath.isNotEmpty) {
|
|
return Image.file(
|
|
File(state.backgroundPath),
|
|
fit: BoxFit.cover,
|
|
width: query.size.width,
|
|
height: query.size.height - query.padding.top,
|
|
);
|
|
}
|
|
|
|
return SizedBox(
|
|
width: query.size.width,
|
|
height: query.size.height,
|
|
child: ColoredBox(color: Theme.of(context).scaffoldBackgroundColor),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
|
|
Positioned(
|
|
left: 0,
|
|
right: 0,
|
|
top: 0,
|
|
bottom: 0,
|
|
child: Scaffold(
|
|
// TODO(Unknown): Maybe replace the scaffold itself to prevent transparency
|
|
backgroundColor: const Color.fromRGBO(0, 0, 0, 0),
|
|
appBar: const ConversationTopbar(),
|
|
body: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
BlocBuilder<ConversationBloc, ConversationState>(
|
|
buildWhen: (prev, next) => prev.conversation?.inRoster != next.conversation?.inRoster,
|
|
builder: (context, state) {
|
|
if (state.conversation!.inRoster) return Container();
|
|
|
|
return _renderNotInRosterWidget(state, context);
|
|
},
|
|
),
|
|
|
|
BlocBuilder<ConversationBloc, ConversationState>(
|
|
// NOTE: We don't need to update when the jid changes as it should
|
|
// be static over the entire lifetime of the BLoC.
|
|
buildWhen: (prev, next) => prev.messages != next.messages || prev.conversation!.encrypted != next.conversation!.encrypted,
|
|
builder: (context, state) => Expanded(
|
|
child: ListView.builder(
|
|
itemCount: state.messages.length * 2,
|
|
itemBuilder: (context, index) => _renderBubble(
|
|
state,
|
|
context,
|
|
index,
|
|
maxWidth,
|
|
state.jid,
|
|
),
|
|
shrinkWrap: true,
|
|
reverse: true,
|
|
controller: _scrollController,
|
|
),
|
|
),
|
|
),
|
|
|
|
ColoredBox(
|
|
color: Theme.of(context).scaffoldBackgroundColor,
|
|
child: ConversationBottomRow(
|
|
_controller,
|
|
_textfieldFocus,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
|
|
BlocBuilder<ConversationBloc, ConversationState>(
|
|
buildWhen: (prev, next) => prev.emojiPickerVisible != next.emojiPickerVisible ||
|
|
prev.stickerPickerVisible != next.stickerPickerVisible,
|
|
builder: (context, state) => Positioned(
|
|
right: 8,
|
|
bottom: state.emojiPickerVisible || state.stickerPickerVisible ?
|
|
330 /* 80 + 250 */ :
|
|
80,
|
|
child: Material(
|
|
color: const Color.fromRGBO(0, 0, 0, 0),
|
|
child: ScaleTransition(
|
|
scale: _scrollToBottom,
|
|
alignment: FractionalOffset.center,
|
|
child: SizedBox(
|
|
width: 45,
|
|
height: 45,
|
|
child: FloatingActionButton(
|
|
heroTag: 'fabScrollDown',
|
|
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
|
onPressed: () {
|
|
_scrollController.jumpTo(0);
|
|
},
|
|
child: const Icon(
|
|
Icons.arrow_downward,
|
|
// TODO(Unknown): Theme dependent
|
|
color: Colors.white,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
|
|
// Indicator for the swipe to lock gesture
|
|
Positioned(
|
|
right: 8,
|
|
bottom: 100,
|
|
child: IgnorePointer(
|
|
child: BlocBuilder<ConversationBloc, ConversationState>(
|
|
builder: (context, state) {
|
|
return AnimatedScale(
|
|
scale: state.isRecording && !state.isLocked ? 1 : 0,
|
|
duration: const Duration(milliseconds: 200),
|
|
child: SizedBox(
|
|
height: 24 * 3,
|
|
width: 47,
|
|
child: Stack(
|
|
clipBehavior: Clip.none,
|
|
children: const [
|
|
Positioned(
|
|
bottom: 0,
|
|
child: Icon(
|
|
Icons.keyboard_arrow_up,
|
|
size: 48,
|
|
),
|
|
),
|
|
Positioned(
|
|
bottom: 12,
|
|
child: Icon(
|
|
Icons.keyboard_arrow_up,
|
|
size: 48,
|
|
),
|
|
),
|
|
Positioned(
|
|
bottom: 24,
|
|
child: Icon(
|
|
Icons.keyboard_arrow_up,
|
|
size: 48,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
|
|
Positioned(
|
|
right: 8,
|
|
bottom: 250,
|
|
child: BlocBuilder<ConversationBloc, ConversationState>(
|
|
builder: (context, state) {
|
|
return DragTarget<int>(
|
|
onWillAccept: (data) => state.isDragging,
|
|
onAccept: (_) {
|
|
context.read<ConversationBloc>().add(
|
|
SendButtonLockedEvent(),
|
|
);
|
|
},
|
|
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 ?
|
|
BlinkingIcon(
|
|
icon: Icons.mic,
|
|
duration: const Duration(milliseconds: 600),
|
|
start: Colors.white,
|
|
end: Colors.red.shade600,
|
|
) :
|
|
const Icon(Icons.lock, color: Colors.white),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
},
|
|
),
|
|
),
|
|
|
|
Positioned(
|
|
right: 8,
|
|
bottom: 380,
|
|
child: BlocBuilder<ConversationBloc, ConversationState>(
|
|
builder: (context, state) {
|
|
return DragTarget<int>(
|
|
onWillAccept: (_) => state.isDragging,
|
|
onAccept: (_) {
|
|
context.read<ConversationBloc>().add(
|
|
RecordingCanceledEvent(),
|
|
);
|
|
},
|
|
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: 'fabCancel',
|
|
onPressed: state.isLocked ?
|
|
() {
|
|
context.read<ConversationBloc>().add(
|
|
RecordingCanceledEvent(),
|
|
);
|
|
} :
|
|
null,
|
|
backgroundColor: Colors.grey,
|
|
child: const Icon(Icons.delete, color: Colors.white),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
},
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|