Compare commits

...

2 Commits

5 changed files with 290 additions and 155 deletions

View File

@ -102,7 +102,8 @@
"retract": "Retract message",
"retractBody": "Are you sure you want to retract the message? Keep in mind that this is only a request that the client does not have to honour.",
"forward": "Forward",
"edit": "Edit"
"edit": "Edit",
"quote": "Quote"
},
"addcontact": {
"title": "Add new contact",

View File

@ -102,7 +102,8 @@
"retract": "Nachricht löschen",
"retractBody": "Bist du dir sicher, dass du die Nachricht löschen willst? Bedenke, dass dies nur eine Bitte ist, die dein gegenüber nicht beachten muss.",
"forward": "Weiterleiten",
"edit": "Bearbeiten"
"edit": "Bearbeiten",
"quote": "Zitieren"
},
"addcontact": {
"title": "Neuen Kontakt hinzufügen",

View File

@ -1,6 +1,7 @@
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:get_it/get_it.dart';
import 'package:moxxmpp/moxxmpp.dart';
import 'package:moxxyv2/i18n/strings.g.dart';
@ -11,13 +12,14 @@ import 'package:moxxyv2/ui/constants.dart';
import 'package:moxxyv2/ui/helpers.dart';
import 'package:moxxyv2/ui/widgets/avatar.dart';
import 'package:moxxyv2/ui/widgets/conversation.dart';
import 'package:moxxyv2/ui/widgets/overview_menu.dart';
import 'package:moxxyv2/ui/widgets/topbar.dart';
enum ConversationsOptions {
settings
}
class ConversationsPage extends StatelessWidget {
class ConversationsPage extends StatefulWidget {
const ConversationsPage({ super.key });
static MaterialPageRoute<dynamic> get route => MaterialPageRoute<dynamic>(
@ -26,6 +28,23 @@ class ConversationsPage extends StatelessWidget {
name: conversationsRoute,
),
);
@override
ConversationsPageState createState() => ConversationsPageState();
}
class ConversationsPageState extends State<ConversationsPage> with TickerProviderStateMixin {
late final AnimationController _controller = AnimationController(
duration: const Duration(milliseconds: 200),
vsync: this,
);
late Animation<double> _convY;
@override
void dispose() {
_controller.dispose();
super.dispose();
}
Widget _listWrapper(BuildContext context, ConversationsState state) {
final maxTextWidth = MediaQuery.of(context).size.width * 0.6;
@ -35,7 +54,19 @@ class ConversationsPage extends StatelessWidget {
itemCount: state.conversations.length,
itemBuilder: (_context, index) {
final item = state.conversations[index];
final row = ConversationsListRow(
item.avatarUrl,
item.title,
item.lastMessageBody,
item.unreadCounter,
maxTextWidth,
item.lastChangeTimestamp,
true,
typingIndicator: item.chatState == ChatState.composing,
lastMessageRetracted: item.lastMessageRetracted,
key: ValueKey('conversationRow;${item.jid}'),
);
return Dismissible(
key: ValueKey('conversation;$item'),
onDismissed: (direction) => context.read<ConversationsBloc>().add(
@ -54,23 +85,66 @@ class ConversationsPage extends StatelessWidget {
),
),
),
child: InkWell(
onTap: () => GetIt.I.get<ConversationBloc>().add(
RequestedConversationEvent(item.jid, item.title, item.avatarUrl),
child: GestureDetector(
onLongPressStart: (event) async {
Vibrate.feedback(FeedbackType.medium);
_convY = Tween<double>(
begin: event.globalPosition.dy - 20,
end: 200,
).animate(
CurvedAnimation(
parent: _controller,
curve: Curves.easeInOutCubic,
),
);
await _controller.forward();
await showDialog<void>(
context: context,
builder: (context) => OverviewMenu(
_convY,
highlight: row,
left: 0,
right: 0,
children: [
...item.unreadCounter != 0 ? [
OverviewMenuItem(
icon: Icons.done_all,
text: 'Mark as read',
onPressed: () {
// TODO(PapaTutuWawa): Implement
showNotImplementedDialog(
'marking as read',
context,
);
},
),
] : [],
OverviewMenuItem(
icon: Icons.close,
text: 'Close chat',
onPressed: () {
// TODO(PapaTutuWawa): Implement
showNotImplementedDialog(
'closing the chat from here',
context,
);
},
),
],
),
);
await _controller.reverse();
},
child: InkWell(
onTap: () => GetIt.I.get<ConversationBloc>().add(
RequestedConversationEvent(item.jid, item.title, item.avatarUrl),
),
child: row,
),
child: ConversationsListRow(
item.avatarUrl,
item.title,
item.lastMessageBody,
item.unreadCounter,
maxTextWidth,
item.lastChangeTimestamp,
true,
typingIndicator: item.chatState == ChatState.composing,
lastMessageRetracted: item.lastMessageRetracted,
key: ValueKey('conversationRow;${item.jid}'),
),
),
),
);
},
);

View File

@ -13,6 +13,7 @@ import 'package:moxxyv2/ui/constants.dart';
import 'package:moxxyv2/ui/helpers.dart';
import 'package:moxxyv2/ui/widgets/chat/datebubble.dart';
import 'package:moxxyv2/ui/widgets/chat/media/media.dart';
import 'package:moxxyv2/ui/widgets/overview_menu.dart';
import 'package:swipeable_tile/swipeable_tile.dart';
Widget _buildMessageOption(IconData icon, String text, void Function() callback) {
@ -160,6 +161,28 @@ class ChatBubbleState extends State<ChatBubble>
}
Widget _buildBubble(BuildContext context) {
final message = Container(
constraints: BoxConstraints(
maxWidth: widget.maxWidth,
),
decoration: BoxDecoration(
color: _getBubbleColor(context),
borderRadius: _getBorderRadius(),
),
child: Padding(
// NOTE: Images don't work well with padding here
padding: widget.message.isMedia || widget.message.quotes != null ?
EdgeInsets.zero :
const EdgeInsets.all(8),
child: buildMessageWidget(
widget.message,
widget.maxWidth,
_getBorderRadius(),
widget.sentBySelf,
),
),
);
return SwipeableTile.swipeToTrigger(
direction: _getSwipeDirection(),
swipeThreshold: 0.2,
@ -252,149 +275,86 @@ class ChatBubbleState extends State<ChatBubble>
await _controller.forward();
await showDialog<void>(
context: context,
builder: (context) {
return Stack(
children: [
AnimatedBuilder(
animation: _msgY,
builder: (context, child) {
return Positioned(
top: _msgY.value,
// TODO(PapaTutuWawa): See above
//right: widget.sentBySelf ? _msgX.value : null,
//left: widget.sentBySelf ? null : _msgX.value,
right: widget.sentBySelf ? 8 : null,
left: widget.sentBySelf ? null : 8,
child: Material(
borderRadius: _getBorderRadius(),
child: Container(
constraints: BoxConstraints(
maxWidth: widget.maxWidth,
),
decoration: BoxDecoration(
color: _getBubbleColor(context),
borderRadius: _getBorderRadius(),
),
child: Padding(
// NOTE: Images don't work well with padding here
padding: widget.message.isMedia || widget.message.quotes != null ? EdgeInsets.zero : const EdgeInsets.all(8),
child: buildMessageWidget(
widget.message,
widget.maxWidth,
_getBorderRadius(),
widget.sentBySelf,
),
),
),
),
builder: (context) => OverviewMenu(
_msgY,
rightBorder: widget.sentBySelf,
left: widget.sentBySelf ? null : 8,
right: widget.sentBySelf ? 8 : null,
highlightMaterialBorder: _getBorderRadius(),
highlight: message,
children: [
...widget.message.canRetract(widget.sentBySelf) ? [
_buildMessageOption(
Icons.delete,
t.pages.conversation.retract,
() => _retractMessage(context),
),
] : [],
...widget.message.canEdit(widget.sentBySelf) ? [
_buildMessageOption(
Icons.edit,
t.pages.conversation.edit,
() {
showNotImplementedDialog(
'editing',
context,
);
},
),
Positioned(
bottom: 50,
right: widget.sentBySelf ? 8 : null,
left: widget.sentBySelf ? null : 8,
child: Row(
children: [
Material(
borderRadius: const BorderRadius.all(radiusLarge),
child: Padding(
padding: const EdgeInsets.all(12),
child: IntrinsicHeight(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
...widget.message.canRetract(widget.sentBySelf) ? [
_buildMessageOption(
Icons.delete,
t.pages.conversation.retract,
() => _retractMessage(context),
),
] : [],
...widget.message.canEdit(widget.sentBySelf) ? [
_buildMessageOption(
Icons.edit,
t.pages.conversation.edit,
() {
showNotImplementedDialog(
'editing',
context,
);
},
),
] : [],
...widget.message.errorMenuVisible ? [
_buildMessageOption(
Icons.info_outline,
'Show Error',
() {
showInfoDialog(
'Error',
errorToTranslatableString(widget.message.errorType!),
context,
);
},
),
] : [],
...widget.message.hasWarning ? [
_buildMessageOption(
Icons.warning,
'Show warning',
() {
showInfoDialog(
'Warning',
warningToTranslatableString(widget.message.warningType!),
context,
);
},
),
] : [],
_buildMessageOption(
Icons.forward,
t.pages.conversation.forward,
() {
showNotImplementedDialog(
'sharing',
context,
);
},
),
],
),
),
),
),
],
),
] : [],
...widget.message.errorMenuVisible ? [
_buildMessageOption(
Icons.info_outline,
'Show Error',
() {
showInfoDialog(
'Error',
errorToTranslatableString(widget.message.errorType!),
context,
);
},
),
],
);
},
] : [],
...widget.message.hasWarning ? [
_buildMessageOption(
Icons.warning,
'Show warning',
() {
showInfoDialog(
'Warning',
warningToTranslatableString(widget.message.warningType!),
context,
);
},
),
] : [],
...widget.message.isQuotable ? [
_buildMessageOption(
Icons.forward,
t.pages.conversation.forward,
() {
showNotImplementedDialog(
'sharing',
context,
);
},
),
] : [],
_buildMessageOption(
Icons.reply,
t.pages.conversation.quote,
() {
widget.onSwipedCallback(widget.message);
Navigator.of(context).pop();
},
),
],
),
);
await _controller.reverse();
},
child: Container(
constraints: BoxConstraints(
maxWidth: widget.maxWidth,
),
decoration: BoxDecoration(
color: _getBubbleColor(context),
borderRadius: _getBorderRadius(),
),
child: Padding(
// NOTE: Images don't work well with padding here
padding: widget.message.isMedia || widget.message.quotes != null ?
EdgeInsets.zero :
const EdgeInsets.all(8),
child: buildMessageWidget(
widget.message,
widget.maxWidth,
_getBorderRadius(),
widget.sentBySelf,
),
),
),
child: message,
),
],
),

View File

@ -0,0 +1,99 @@
import 'package:flutter/material.dart';
import 'package:moxxyv2/ui/constants.dart';
class OverviewMenuItem extends StatelessWidget {
const OverviewMenuItem({
required this.icon,
required this.text,
required this.onPressed,
super.key,
});
final IconData icon;
final String text;
final void Function() onPressed;
@override
Widget build(BuildContext context) {
return InkResponse(
onTap: onPressed,
child: Row(
children: [
Padding(
padding: const EdgeInsets.only(
top: 8,
right: 8,
bottom: 8,
),
child: Icon(icon),
),
Text(text),
],
),
);
}
}
class OverviewMenu extends StatelessWidget {
const OverviewMenu(
this._animation, {
required this.highlight,
required this.children,
this.highlightMaterialBorder,
this.rightBorder = true,
this.left,
this.right,
super.key,
}
);
final Animation<double> _animation;
final Widget highlight;
final List<Widget> children;
final bool rightBorder;
final double? left;
final double? right;
final BorderRadius? highlightMaterialBorder;
@override
Widget build(BuildContext context) {
return Stack(
children: [
AnimatedBuilder(
animation: _animation,
builder: (context, child) {
return Positioned(
left: left,
right: right,
top: _animation.value,
child: Material(
borderRadius: highlightMaterialBorder,
child: highlight,
),
);
},
),
Positioned(
bottom: 50,
right: rightBorder ? 8 : null,
left: rightBorder ? null : 8,
child: Row(
children: [
Material(
borderRadius: const BorderRadius.all(radiusLarge),
child: Padding(
padding: const EdgeInsets.all(12),
child: IntrinsicHeight(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: children,
),
),
),
),
],
),
),
],
);
}
}