Compare commits

...

2 Commits

Author SHA1 Message Date
dee479a918 fix(ui): Move the DragTargets more to the left 2023-01-13 23:05:15 +01:00
6895ef1e32 feat(ui): Move the send button back to a speed dial
This makes the voice message UX more like what Signal and co. do.
Also makes the message TextField less crowded. Kind of fixes #207.
2023-01-13 23:03:02 +01:00
6 changed files with 199 additions and 173 deletions

View File

@ -160,7 +160,10 @@
"stickerPickerNoStickersLine2": "They can be installed in the sticker settings.",
"stickerSettings": "Sticker settings",
"newDeviceMessage": "${title} added a new encryption device",
"messageHint": "Send a message..."
"messageHint": "Send a message...",
"sendImages": "Send images",
"sendFiles": "Send files",
"takePhotos": "Take photos"
},
"addcontact": {
"title": "Add new contact",

View File

@ -160,7 +160,10 @@
"stickerPickerNoStickersLine2": "Diese können in den Stickereinstellungen installiert werden.",
"stickerSettings": "Stickereinstellungen",
"newDeviceMessage": "${title} hat ein neues Verschlüsselungsgerät hinzugefügt",
"messageHint": "Nachricht senden..."
"messageHint": "Nachricht senden...",
"sendImages": "Bilder senden",
"sendFiles": "Dateien senden",
"takePhotos": "Bilder aufnehmen"
},
"addcontact": {
"title": "Neuen Kontakt hinzufügen",

View File

@ -1,11 +1,11 @@
part of 'conversation_bloc.dart';
enum SendButtonState {
audio,
multi,
send,
cancelCorrection,
}
const defaultSendButtonState = SendButtonState.audio;
const defaultSendButtonState = SendButtonState.multi;
@freezed
class ConversationState with _$ConversationState {

View File

@ -3,8 +3,8 @@ import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart';
import 'package:flutter_speed_dial/flutter_speed_dial.dart';
import 'package:flutter_vibrate/flutter_vibrate.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:get_it/get_it.dart';
import 'package:moxxyv2/i18n/strings.g.dart';
import 'package:moxxyv2/shared/helpers.dart';
@ -32,7 +32,55 @@ class _TextFieldIconButton extends StatelessWidget {
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Icon(
icon,
size: 20,
size: 24,
color: primaryColor,
),
),
);
}
}
class _TextFieldRecordButton extends StatelessWidget {
const _TextFieldRecordButton();
@override
Widget build(BuildContext context) {
return 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(),
);
},
childWhenDragging: const SizedBox(),
feedback: SizedBox(
width: 45,
height: 45,
child: FloatingActionButton(
onPressed: null,
heroTag: 'fabDragged',
backgroundColor: Colors.red.shade600,
child: BlinkingIcon(
icon: Icons.mic,
duration: const Duration(milliseconds: 600),
start: Colors.white,
end: Colors.red.shade600,
),
),
),
child: const Padding(
padding: EdgeInsets.symmetric(horizontal: 8),
child: Icon(
Icons.mic,
size: 24,
color: primaryColor,
),
),
@ -44,13 +92,15 @@ class ConversationBottomRow extends StatefulWidget {
const ConversationBottomRow(
this.controller,
this.tabController,
this.focusNode, {
this.focusNode,
this.speedDialValueNotifier, {
super.key,
}
);
final TextEditingController controller;
final TabController tabController;
final FocusNode focusNode;
final ValueNotifier<bool> speedDialValueNotifier;
@override
ConversationBottomRowState createState() => ConversationBottomRowState();
@ -58,7 +108,7 @@ class ConversationBottomRow extends StatefulWidget {
class ConversationBottomRowState extends State<ConversationBottomRow> {
late StreamSubscription<bool> _keyboardVisibilitySubscription;
@override
void initState() {
super.initState();
@ -82,7 +132,7 @@ class ConversationBottomRowState extends State<ConversationBottomRow> {
IconData _getSendButtonIcon(ConversationState state) {
switch (state.sendButtonState) {
case SendButtonState.audio: return Icons.mic;
case SendButtonState.multi: return Icons.add;
case SendButtonState.send: return Icons.send;
case SendButtonState.cancelCorrection: return Icons.clear;
}
@ -114,11 +164,20 @@ class ConversationBottomRowState extends State<ConversationBottomRow> {
children: [
Expanded(
child: CustomTextField(
backgroundColor: Theme.of(context).extension<MoxxyThemeData>()!.conversationTextFieldColor,
textColor: Theme.of(context).extension<MoxxyThemeData>()!.conversationTextFieldTextColor,
backgroundColor: Theme
.of(context)
.extension<MoxxyThemeData>()!
.conversationTextFieldColor,
textColor: Theme
.of(context)
.extension<MoxxyThemeData>()!
.conversationTextFieldTextColor,
maxLines: 5,
hintText: t.pages.conversation.messageHint,
hintTextColor: Theme.of(context).extension<MoxxyThemeData>()!.conversationTextFieldHintTextColor,
hintTextColor: Theme
.of(context)
.extension<MoxxyThemeData>()!
.conversationTextFieldHintTextColor,
isDense: true,
onChanged: (value) {
context.read<ConversationBloc>().add(
@ -129,33 +188,31 @@ class ConversationBottomRowState extends State<ConversationBottomRow> {
fontSize: textFieldFontSizeConversation,
cornerRadius: textfieldRadiusConversation,
controller: widget.controller,
topWidget: state.quotedMessage != null ? buildQuoteMessageWidget(
state.quotedMessage!,
isSent(state.quotedMessage!, state.jid),
resetQuote: () => context.read<ConversationBloc>().add(QuoteRemovedEvent()),
) : null,
topWidget: state.quotedMessage != null ?
buildQuoteMessageWidget(
state.quotedMessage!,
isSent(state.quotedMessage!, state.jid),
resetQuote: () => context.read<ConversationBloc>().add(QuoteRemovedEvent()),
) :
null,
focusNode: widget.focusNode,
shouldSummonKeyboard: () => !state.pickerVisible,
prefixIcon: IntrinsicWidth(
child: Row(
children: [
InkWell(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Icon(
state.pickerVisible ?
Icons.keyboard :
_getPickerIcon(),
color: primaryColor,
size: 24,
),
Padding(
padding: const EdgeInsets.only(left: 8),
child: _TextFieldIconButton(
state.pickerVisible ?
Icons.keyboard :
_getPickerIcon(),
() {
context.read<ConversationBloc>().add(
PickerToggledEvent(),
);
},
),
onTap: () {
context.read<ConversationBloc>().add(
PickerToggledEvent(),
);
},
),
),
],
),
),
@ -166,31 +223,10 @@ class ConversationBottomRowState extends State<ConversationBottomRow> {
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(),
);
},
children: const [
Padding(
padding: EdgeInsets.only(right: 8),
child: _TextFieldRecordButton(),
),
],
),
@ -202,11 +238,69 @@ class ConversationBottomRowState extends State<ConversationBottomRow> {
),
),
),
const Padding(
padding: EdgeInsets.only(left: 8),
Padding(
padding: const EdgeInsets.only(left: 8),
child: SizedBox(
height: 45,
width: 45,
child: SpeedDial(
icon: _getSendButtonIcon(state),
backgroundColor: primaryColor,
foregroundColor: Colors.white,
children: [
SpeedDialChild(
child: const Icon(Icons.image),
onTap: () {
context.read<ConversationBloc>().add(
ImagePickerRequestedEvent(),
);
},
backgroundColor: primaryColor,
foregroundColor: Colors.white,
label: t.pages.conversation.sendImages,
),
SpeedDialChild(
child: const Icon(Icons.file_present),
onTap: () {
context.read<ConversationBloc>().add(
FilePickerRequestedEvent(),
);
},
backgroundColor: primaryColor,
foregroundColor: Colors.white,
label: t.pages.conversation.sendFiles,
),
SpeedDialChild(
child: const Icon(Icons.photo_camera),
onTap: () {
showNotImplementedDialog('taking photos', context);
},
backgroundColor: primaryColor,
foregroundColor: Colors.white,
label: t.pages.conversation.takePhotos,
),
],
openCloseDial: widget.speedDialValueNotifier,
onPress: () {
switch (state.sendButtonState) {
case SendButtonState.cancelCorrection:
context.read<ConversationBloc>().add(
MessageEditCancelledEvent(),
);
widget.controller.text = '';
return;
case SendButtonState.send:
context.read<ConversationBloc>().add(
MessageSentEvent(),
);
widget.controller.text = '';
return;
case SendButtonState.multi:
widget.speedDialValueNotifier.value = !widget.speedDialValueNotifier.value;
return;
}
},
),
),
),
],
@ -275,104 +369,10 @@ class ConversationBottomRowState extends State<ConversationBottomRow> {
),
),
),
BlocBuilder<ConversationBloc, ConversationState>(
buildWhen: (prev, next) => prev.sendButtonState != next.sendButtonState ||
prev.isDragging != next.isDragging ||
prev.isLocked != next.isLocked ||
prev.pickerVisible != next.pickerVisible,
builder: (context, state) {
return Positioned(
right: 8,
bottom: state.pickerVisible ?
pickerHeight + 8 :
8,
child: 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(),
);
},
feedback: SizedBox(
height: 45,
width: 45,
child: FloatingActionButton(
onPressed: null,
heroTag: 'fabDragged',
backgroundColor: Colors.red.shade600,
child: BlinkingIcon(
icon: Icons.mic,
duration: const Duration(milliseconds: 600),
start: Colors.white,
end: Colors.red.shade600,
),
),
),
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:
Vibrate.feedback(FeedbackType.heavy);
Fluttertoast.showToast(
msg: t.warnings.conversation.holdForLonger,
gravity: ToastGravity.SNACKBAR,
toastLength: Toast.LENGTH_SHORT,
);
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,
),
),
),
),
),
);
},
),
Positioned(
left: 8,
bottom: 11,
bottom: 8,
right: 61,
child: BlocBuilder<ConversationBloc, ConversationState>(
buildWhen: (prev, next) => prev.isRecording != next.isRecording,
@ -383,7 +383,7 @@ class ConversationBottomRowState extends State<ConversationBottomRow> {
child: IgnorePointer(
ignoring: !state.isRecording,
child: SizedBox(
height: 38,
height: textFieldFontSizeConversation + 2 * 12 + 2,
child: DecoratedBox(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(textfieldRadiusConversation),
@ -393,14 +393,14 @@ class ConversationBottomRowState extends State<ConversationBottomRow> {
// created and destroyed to prevent the timer from running
// until the user closes the page.
child: state.isRecording ?
const Align(
alignment: Alignment.centerLeft,
child: Padding(
padding: EdgeInsets.only(left: 16),
child: TimerWidget(),
),
) :
null,
const Align(
alignment: Alignment.centerLeft,
child: Padding(
padding: EdgeInsets.only(left: 16),
child: TimerWidget(),
),
) :
null,
),
),
),

View File

@ -17,6 +17,7 @@ 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/theme.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';
@ -46,6 +47,7 @@ class ConversationPageState extends State<ConversationPage> with TickerProviderS
late final Animation<double> _scrollToBottom;
bool _scrolledToBottomState = true;
late FocusNode _textfieldFocus;
final ValueNotifier<bool> _isSpeedDialOpen = ValueNotifier(false);
@override
void initState() {
@ -552,6 +554,7 @@ class ConversationPageState extends State<ConversationPage> with TickerProviderS
_controller,
_tabController,
_textfieldFocus,
_isSpeedDialOpen,
),
),
],
@ -594,7 +597,7 @@ class ConversationPageState extends State<ConversationPage> with TickerProviderS
// Indicator for the swipe to lock gesture
Positioned(
right: 8,
right: 53,
bottom: 100,
child: IgnorePointer(
child: BlocBuilder<ConversationBloc, ConversationState>(
@ -639,7 +642,7 @@ class ConversationPageState extends State<ConversationPage> with TickerProviderS
),
Positioned(
right: 8,
right: 61,
bottom: pickerHeight,
child: BlocBuilder<ConversationBloc, ConversationState>(
builder: (context, state) {
@ -668,7 +671,10 @@ class ConversationPageState extends State<ConversationPage> with TickerProviderS
null,
backgroundColor: state.isLocked ?
Colors.red.shade600 :
Colors.grey,
Theme
.of(context)
.extension<MoxxyThemeData>()!
.conversationTextFieldColor,
child: state.isLocked ?
BlinkingIcon(
icon: Icons.mic,
@ -676,7 +682,13 @@ class ConversationPageState extends State<ConversationPage> with TickerProviderS
start: Colors.white,
end: Colors.red.shade600,
) :
const Icon(Icons.lock, color: Colors.white),
Icon(
Icons.lock,
color: Theme
.of(context)
.extension<MoxxyThemeData>()!
.conversationTextFieldTextColor,
),
),
),
);
@ -687,7 +699,7 @@ class ConversationPageState extends State<ConversationPage> with TickerProviderS
),
Positioned(
right: 8,
right: 61,
bottom: 380,
child: BlocBuilder<ConversationBloc, ConversationState>(
builder: (context, state) {
@ -714,8 +726,17 @@ class ConversationPageState extends State<ConversationPage> with TickerProviderS
);
} :
null,
backgroundColor: Colors.grey,
child: const Icon(Icons.delete, color: Colors.white),
backgroundColor: Theme
.of(context)
.extension<MoxxyThemeData>()!
.conversationTextFieldColor,
child: Icon(
Icons.delete,
color: Theme
.of(context)
.extension<MoxxyThemeData>()!
.conversationTextFieldTextColor,
),
),
),
);

View File

@ -276,7 +276,6 @@ class ConversationsPageState extends State<ConversationsPage> with TickerProvide
icon: Icons.chat,
curve: Curves.bounceInOut,
backgroundColor: primaryColor,
// TODO(Unknown): Theme dependent?
foregroundColor: Colors.white,
children: [
SpeedDialChild(