ui: Allow quoting messages

This commit is contained in:
PapaTutuWawa 2022-03-07 20:32:29 +01:00
parent 7f8cc962b9
commit 76118d0cc5
8 changed files with 237 additions and 42 deletions

View File

@ -4,6 +4,7 @@ import "package:moxxyv2/ui/widgets/topbar.dart";
import "package:moxxyv2/ui/widgets/chatbubble.dart";
import "package:moxxyv2/ui/widgets/avatar.dart";
import "package:moxxyv2/ui/widgets/textfield.dart";
import "package:moxxyv2/ui/widgets/quotedmessage.dart";
import "package:moxxyv2/ui/pages/profile/profile.dart";
import "package:moxxyv2/ui/pages/conversation/arguments.dart";
import "package:moxxyv2/ui/constants.dart";
@ -16,6 +17,8 @@ import "package:moxxyv2/ui/redux/conversation/actions.dart";
import "package:flutter/material.dart";
import "package:flutter_speed_dial/flutter_speed_dial.dart";
import "package:flutter_redux/flutter_redux.dart";
import "package:swipeable_tile/swipeable_tile.dart";
import "package:flutter_vibrate/flutter_vibrate.dart";
typedef SendMessageFunction = void Function(String body);
@ -58,8 +61,23 @@ class _MessageListViewModel {
final void Function() closeChat;
final void Function() resetCurrentConversation;
final String backgroundPath;
final Message? quotedMessage;
final void Function(Message?) setQuotedMessage;
_MessageListViewModel({ required this.conversation, required this.showSendButton, required this.sendMessage, required this.setShowSendButton, required this.showScrollToEndButton, required this.setShowScrollToEndButton, required this.closeChat, required this.messages, required this.resetCurrentConversation, required this.backgroundPath });
_MessageListViewModel({
required this.conversation,
required this.showSendButton,
required this.sendMessage,
required this.setShowSendButton,
required this.showScrollToEndButton,
required this.setShowScrollToEndButton,
required this.closeChat,
required this.messages,
required this.resetCurrentConversation,
required this.backgroundPath,
required this.setQuotedMessage,
required this.quotedMessage
});
}
class ConversationPage extends StatefulWidget {
@ -100,23 +118,82 @@ class _ConversationPageState extends State<ConversationPage> {
}
}
Widget _renderBubble(List<Message> messages, int _index, double maxWidth) {
Widget _renderBubble(_MessageListViewModel viewModel, int _index, double maxWidth) {
// TODO: Since we reverse the list: Fix start, end and between
final index = messages.length - 1 - _index;
Message item = messages[index];
bool start = index - 1 < 0 ? true : messages[index - 1].sent != item.sent;
bool end = index + 1 >= messages.length ? true : messages[index + 1].sent != item.sent;
final index = viewModel.messages.length - 1 - _index;
Message item = viewModel.messages[index];
bool start = index - 1 < 0 ? true : viewModel.messages[index - 1].sent != item.sent;
bool end = index + 1 >= viewModel.messages.length ? true : viewModel.messages[index + 1].sent != item.sent;
bool between = !start && !end;
return ChatBubble(
message: item,
sentBySelf: item.sent,
start: start,
end: end,
between: between,
closerTogether: !end,
maxWidth: maxWidth,
key: ValueKey("message;" + item.toString())
return SwipeableTile.swipeToTrigger(
direction: SwipeDirection.horizontal,
swipeThreshold: 0.2,
onSwiped: (_) => viewModel.setQuotedMessage(item),
backgroundBuilder: (_, direction, progress) {
// NOTE: Taken from https://github.com/watery-desert/swipeable_tile/blob/main/example/lib/main.dart#L240
// and modified.
bool vibrated = false;
return AnimatedBuilder(
animation: progress,
builder: (_, __) {
if (progress.value > 0.9999 && !vibrated) {
Vibrate.feedback(FeedbackType.light);
vibrated = true;
} else if (progress.value < 0.9999) {
vibrated = false;
}
return Container(
alignment: direction == SwipeDirection.endToStart ? Alignment.centerRight : Alignment.centerLeft,
child: Padding(
padding: EdgeInsets.only(
right: direction == SwipeDirection.endToStart ? 24.0 : 0.0,
left: direction == SwipeDirection.startToEnd ? 24.0 : 0.0
),
child: Transform.scale(
scale: Tween<double>(
begin: 0.0,
end: 1.2,
)
.animate(
CurvedAnimation(
parent: progress,
curve: const Interval(0.5, 1.0,
curve: Curves.linear),
),
)
.value,
child: Container(
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.3),
shape: BoxShape.circle
),
child: const Padding(
padding: EdgeInsets.all(8.0),
child: Icon(
Icons.reply,
color: Colors.white,
)
)
),
),
),
);
},
);
},
isEelevated: false,
key: ValueKey("message;" + item.toString()),
child: ChatBubble(
message: item,
sentBySelf: item.sent,
start: start,
end: end,
between: between,
closerTogether: !end,
maxWidth: maxWidth,
)
);
}
@ -148,7 +225,9 @@ class _ConversationPageState extends State<ConversationPage> {
)
),
resetCurrentConversation: () => store.dispatch(SetOpenConversationAction(jid: null)),
backgroundPath: store.state.preferencesState.backgroundPath
backgroundPath: store.state.preferencesState.backgroundPath,
setQuotedMessage: (msg) => store.dispatch(QuoteMessageUIAction(msg)),
quotedMessage: store.state.conversationPageState.quotedMessage
);
},
builder: (context, viewModel) {
@ -224,7 +303,7 @@ class _ConversationPageState extends State<ConversationPage> {
child: ListView.builder(
reverse: true,
itemCount: viewModel.messages.length,
itemBuilder: (context, index) => _renderBubble(viewModel.messages, index, maxWidth)
itemBuilder: (context, index) => _renderBubble(viewModel, index, maxWidth)
)
),
Positioned(
@ -261,6 +340,10 @@ class _ConversationPageState extends State<ConversationPage> {
onChanged: (value) => _onMessageTextChanged(value, viewModel),
contentPadding: textfieldPaddingConversation,
cornerRadius: textfieldRadiusConversation,
topWidget: viewModel.quotedMessage != null ? QuotedMessageWidget(
message: viewModel.quotedMessage!,
resetQuotedMessage: () => viewModel.setQuotedMessage(null)
) : null
)
),
Padding(

View File

@ -106,3 +106,10 @@ class UpdateConversationAction {
UpdateConversationAction({ required this.conversation });
}
/// Triggered when a message is to be quoted
class QuoteMessageUIAction {
final Message? message;
QuoteMessageUIAction(this.message);
}

View File

@ -51,9 +51,13 @@ HashMap<String, List<Message>> messageReducer(HashMap<String, List<Message>> sta
ConversationPageState conversationPageReducer(ConversationPageState state, dynamic action) {
if (action is SetShowSendButtonAction) {
return state.copyWith(showSendButton: action.show);
return state.copyWith(state.quotedMessage, showSendButton: action.show);
} else if (action is SetShowScrollToEndButtonAction) {
return state.copyWith(showScrollToEndButton: action.show);
return state.copyWith(state.quotedMessage, showScrollToEndButton: action.show);
} else if (action is QuoteMessageUIAction) {
return state.copyWith(action.message);
} else if (action is SendMessageAction) {
return state.copyWith(null);
}
return state;

View File

@ -1,14 +1,22 @@
import "package:moxxyv2/shared/models/message.dart";
class ConversationPageState {
final bool showSendButton;
final bool showScrollToEndButton;
final Message? quotedMessage;
ConversationPageState({ required this.showSendButton, required this.showScrollToEndButton });
ConversationPageState.initialState() : showSendButton = false, showScrollToEndButton = false;
ConversationPageState({
required this.showSendButton,
required this.showScrollToEndButton,
this.quotedMessage
});
ConversationPageState.initialState() : showSendButton = false, showScrollToEndButton = false, quotedMessage = null;
ConversationPageState copyWith({ bool? showSendButton, bool? showScrollToEndButton }) {
ConversationPageState copyWith(Message? quotedMessage, { bool? showSendButton, bool? showScrollToEndButton }) {
return ConversationPageState(
showSendButton: showSendButton ?? this.showSendButton,
showScrollToEndButton: showScrollToEndButton ?? this.showScrollToEndButton
showScrollToEndButton: showScrollToEndButton ?? this.showScrollToEndButton,
quotedMessage: quotedMessage
);
}
}

View File

@ -0,0 +1,62 @@
import "package:moxxyv2/shared/models/message.dart";
import "package:moxxyv2/ui/constants.dart";
import "package:flutter/material.dart";
/// This Widget is used to show that a message has been quoted.
class QuotedMessageWidget extends StatelessWidget {
final Message message;
final void Function() resetQuotedMessage;
/// [message]: The message used to quote
/// [resetQuotedMessage]: Function to reset the quoted message
const QuotedMessageWidget({
required this.message,
required this.resetQuotedMessage,
Key? key
}) : super(key: key);
@override
Widget build(BuildContext context) {
const quoteLeftBorderWidth = 7.0;
return Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
decoration: const BoxDecoration(
color: bubbleColorReceived,
borderRadius: BorderRadius.all(radiusLarge)
),
clipBehavior: Clip.antiAlias,
child: Stack(
children: [
Positioned(
left: 0.0,
top: 0.0,
bottom: 0.0,
child: Container(
color: Colors.white,
width: quoteLeftBorderWidth,
)
),
Positioned(
right: 3.0,
top: 3.0,
child: InkWell(
onTap: resetQuotedMessage,
child: Icon(
Icons.close,
size: 24.0
)
)
),
Padding(
padding: const EdgeInsets.all(8.0).add(const EdgeInsets.only(left: quoteLeftBorderWidth, right: 26.0)),
child: Text(message.body)
),
]
)
)
);
}
}

View File

@ -10,6 +10,7 @@ class CustomTextField extends StatelessWidget {
final Widget? suffix;
final String? suffixText;
final Widget? suffixIcon;
final Widget? topWidget;
final EdgeInsetsGeometry contentPadding;
final bool enabled;
final bool obscureText;
@ -28,6 +29,7 @@ class CustomTextField extends StatelessWidget {
this.suffix,
this.suffixText,
this.suffixIcon,
this.topWidget,
this.enabled = true,
this.obscureText = false,
this.maxLines = 1,
@ -42,6 +44,8 @@ class CustomTextField extends StatelessWidget {
@override
Widget build(BuildContext context) {
const quoteLeftBorderWidth = 7.0;
return Column(
children: [
Container(
@ -52,25 +56,31 @@ class CustomTextField extends StatelessWidget {
color: Colors.purple
)
),
child: TextField(
maxLines: maxLines,
minLines: minLines,
obscureText: obscureText,
enabled: enabled,
controller: controller,
onChanged: onChanged,
enableSuggestions: enableIMEFeatures,
autocorrect: enableIMEFeatures,
decoration: InputDecoration(
labelText: labelText,
hintText: hintText,
border: InputBorder.none,
contentPadding: contentPadding,
suffixIcon: suffixIcon,
suffix: suffix,
suffixText: suffixText,
isDense: isDense
)
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
...(topWidget != null ? [ topWidget! ] : []),
TextField(
maxLines: maxLines,
minLines: minLines,
obscureText: obscureText,
enabled: enabled,
controller: controller,
onChanged: onChanged,
enableSuggestions: enableIMEFeatures,
autocorrect: enableIMEFeatures,
decoration: InputDecoration(
labelText: labelText,
hintText: hintText,
border: InputBorder.none,
contentPadding: contentPadding,
suffixIcon: suffixIcon,
suffix: suffix,
suffixText: suffixText,
isDense: isDense
)
)
]
)
),
Visibility(

View File

@ -456,6 +456,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "5.0.0+1"
flutter_vibrate:
dependency: "direct main"
description:
name: flutter_vibrate
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.0"
flutter_web_plugins:
dependency: transitive
description: flutter
@ -958,6 +965,15 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0"
swipeable_tile:
dependency: "direct main"
description:
path: "."
ref: bfab5e28f1f1ea624232002f0d05481cb2bd9997
resolved-ref: bfab5e28f1f1ea624232002f0d05481cb2bd9997
url: "https://github.com/PapaTutuWawa/swipeable_tile.git"
source: git
version: "1.0.0+6"
term_glyph:
dependency: transitive
description:

View File

@ -72,6 +72,11 @@ dependencies:
open_file: ^3.2.1
hex: ^0.2.0
drop_down_list: ^0.0.2
swipeable_tile:
git:
url: https://github.com/PapaTutuWawa/swipeable_tile.git
ref: bfab5e28f1f1ea624232002f0d05481cb2bd9997
flutter_vibrate: ^1.3.0
dev_dependencies:
#flutter_test: