ui: Allow quoting messages
This commit is contained in:
parent
7f8cc962b9
commit
76118d0cc5
@ -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(
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
62
lib/ui/widgets/quotedmessage.dart
Normal file
62
lib/ui/widgets/quotedmessage.dart
Normal 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)
|
||||
),
|
||||
]
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
@ -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(
|
||||
|
16
pubspec.lock
16
pubspec.lock
@ -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:
|
||||
|
@ -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:
|
||||
|
Loading…
Reference in New Issue
Block a user