Compare commits
2 Commits
85023299d2
...
eac8592536
Author | SHA1 | Date | |
---|---|---|---|
eac8592536 | |||
3e0feaa3e8 |
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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}'),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
99
lib/ui/widgets/overview_menu.dart
Normal file
99
lib/ui/widgets/overview_menu.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user