Redesign replies, and message state and time

parent 38dc5cd4
......@@ -28,8 +28,22 @@ final PattleTheme pattleLightTheme = PattleTheme(
chat: ChatTheme(
backgroundColor: PattleTheme._primarySwatch[50],
inputColor: Colors.white,
myMessageColor: PattleTheme._primarySwatch[450],
theirMessageColor: Colors.white,
myMessage: MessageTheme(
backgroundColor: PattleTheme._primarySwatch[450],
contentColor: Colors.white,
repliedTo: MessageTheme(
backgroundColor: PattleTheme._primarySwatch[300],
contentColor: Colors.grey[100],
),
),
theirMessage: MessageTheme(
backgroundColor: Colors.white,
contentColor: null,
repliedTo: MessageTheme(
backgroundColor: Colors.grey[250],
contentColor: Colors.grey[600],
),
),
stateMessageColor: PattleTheme._primarySwatch[100],
myRedactedContentColor: Colors.grey[300],
theirRedactedContentColor: Colors.grey[700],
......@@ -51,8 +65,22 @@ final PattleTheme pattleDarkTheme = PattleTheme(
chat: ChatTheme(
backgroundColor: Colors.grey[900],
inputColor: Colors.grey[800],
myMessageColor: PattleTheme._primarySwatch[700],
theirMessageColor: Colors.grey[800],
myMessage: MessageTheme(
backgroundColor: PattleTheme._primarySwatch[700],
contentColor: Colors.white,
repliedTo: MessageTheme(
backgroundColor: PattleTheme._primarySwatch[300],
contentColor: Colors.grey[200],
),
),
theirMessage: MessageTheme(
backgroundColor: Colors.grey[800],
contentColor: null,
repliedTo: MessageTheme(
backgroundColor: null,
contentColor: null,
),
),
stateMessageColor: PattleTheme._primarySwatch[900],
myRedactedContentColor: Colors.white30,
theirRedactedContentColor: Colors.white70,
......@@ -147,8 +175,8 @@ class ChatTheme {
final Color backgroundColor;
final Color inputColor;
final Color myMessageColor;
final Color theirMessageColor;
final MessageTheme myMessage;
final MessageTheme theirMessage;
final Color stateMessageColor;
......@@ -158,14 +186,27 @@ class ChatTheme {
ChatTheme({
@required this.backgroundColor,
@required this.inputColor,
@required this.myMessageColor,
@required this.theirMessageColor,
@required this.myMessage,
@required this.theirMessage,
@required this.stateMessageColor,
@required this.myRedactedContentColor,
@required this.theirRedactedContentColor,
});
}
class MessageTheme {
final Color backgroundColor;
final Color contentColor;
final MessageTheme repliedTo;
MessageTheme({
@required this.backgroundColor,
@required this.contentColor,
this.repliedTo,
});
}
extension PattleThemeContext on BuildContext {
PattleTheme get pattleTheme => PattleTheme.of(this);
}
......@@ -15,7 +15,10 @@
// You should have received a copy of the GNU Affero General Public License
// along with Pattle. If not, see <https://www.gnu.org/licenses/>.
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:matrix_sdk/matrix_sdk.dart';
import 'package:provider/provider.dart';
......@@ -46,10 +49,12 @@ class MessageBubble extends StatelessWidget {
final bool isStartOfGroup;
final bool isEndOfGroup;
/// If this is not null, this bubble is rendered inside another
/// If this is not null, this bubble is rendered above another
/// bubble, because it's replied to by [reply].
final ChatMessage reply;
bool get isRepliedTo => reply != null;
final BorderRadius borderRadius;
final Color color;
......@@ -58,6 +63,8 @@ class MessageBubble extends StatelessWidget {
final EdgeInsets contentPadding = EdgeInsets.all(8);
final double replySlideUnderDistance = 16;
static const _groupTimeLimit = Duration(minutes: 3);
static const _oppositePadding = 48.0;
......@@ -272,61 +279,189 @@ class MessageBubble extends StatelessWidget {
@override
Widget build(BuildContext context) {
final color = this.color ??
(message.isMine
? context.pattleTheme.chat.myMessageColor
: context.pattleTheme.chat.theirMessageColor);
var color = this.color;
if (color == null) {
if (message.isMine) {
if (!isRepliedTo) {
color = context.pattleTheme.chat.myMessage.backgroundColor;
} else {
color = context.pattleTheme.chat.myMessage.repliedTo.backgroundColor;
}
} else {
if (!isRepliedTo) {
color = context.pattleTheme.chat.theirMessage.backgroundColor;
} else {
color =
context.pattleTheme.chat.theirMessage.repliedTo.backgroundColor;
}
}
}
final border = RoundedRectangleBorder(borderRadius: borderRadius);
return Column(
crossAxisAlignment:
message.isMine ? CrossAxisAlignment.end : CrossAxisAlignment.start,
children: <Widget>[
Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment:
message.isMine ? MainAxisAlignment.end : MainAxisAlignment.start,
children: [
Flexible(
child: Padding(
padding: reply == null
? EdgeInsets.only(
left: message.isMine ? _oppositePadding : 0,
right: !message.isMine ? _oppositePadding : 0,
top: previousMessage == null ? _paddingBetween : 0,
bottom: isEndOfGroup
? _paddingBetween
: _paddingBetweenSameGroup,
)
: EdgeInsets.zero,
child: Material(
color: color,
elevation: 1,
shape: border,
child: DefaultTextStyle(
style: Theme.of(context).textTheme.bodyText2.apply(
fontSizeFactor: 1.1,
color: message.isMine ? Colors.white : null,
),
child: Provider<MessageBubble>.value(
value: this,
child: child,
),
),
),
),
Widget widget = Material(
color: color,
elevation: 1,
shape: border,
child: DefaultTextStyle(
style: Theme.of(context).textTheme.bodyText2.apply(
fontSizeFactor: 1.1,
color: message.isMine ? Colors.white : null,
),
],
child: Provider<MessageBubble>.value(
value: this,
child: child,
),
],
),
);
if (message.inReplyTo != null) {
widget = _ReplyLayout(
replySlideUnderDistance: replySlideUnderDistance,
reply: MessageBubble.withContent(
chat: chat,
message: message.inReplyTo,
reply: message,
),
message: widget,
);
}
if (!isRepliedTo) {
return Align(
alignment:
message.isMine ? Alignment.centerRight : Alignment.centerLeft,
child: Padding(
padding: reply == null
? EdgeInsets.only(
left: message.isMine ? _oppositePadding : 0,
right: !message.isMine ? _oppositePadding : 0,
top: previousMessage == null ? _paddingBetween : 0,
bottom:
isEndOfGroup ? _paddingBetween : _paddingBetweenSameGroup,
)
: EdgeInsets.zero,
child: widget,
),
);
} else {
return widget;
}
}
static MessageBubble of(BuildContext context) =>
Provider.of<MessageBubble>(context, listen: false);
}
class _ReplyLayout extends MultiChildRenderObjectWidget {
final Widget reply;
final Widget message;
final double replySlideUnderDistance;
_ReplyLayout({
@required this.reply,
@required this.message,
@required this.replySlideUnderDistance,
}) : super(
children: [
reply,
message,
],
);
@override
_ReplyLayoutRenderBox createRenderObject(BuildContext context) {
return _ReplyLayoutRenderBox()
..replySlideUnderDistance = replySlideUnderDistance;
}
@override
void updateRenderObject(
BuildContext context,
covariant _ReplyLayoutRenderBox renderObject,
) {
renderObject.replySlideUnderDistance = replySlideUnderDistance;
}
}
class _ReplyLayoutRenderBox extends RenderBox
with
ContainerRenderObjectMixin<RenderBox, _ReplyLayoutParentData>,
RenderBoxContainerDefaultsMixin<RenderBox, _ReplyLayoutParentData> {
double replySlideUnderDistance;
@override
void setupParentData(RenderBox child) {
if (child.parentData is! _ReplyLayoutParentData) {
child.parentData = _ReplyLayoutParentData();
}
}
RenderBox get _reply {
return firstChild;
}
RenderBox get _message {
return (firstChild.parentData as _ReplyLayoutParentData).nextSibling;
}
@override
void performLayout() {
assert(childCount == 2);
final reply = _reply;
final message = _message;
reply.layout(
constraints,
parentUsesSize: true,
);
message.layout(
constraints,
parentUsesSize: true,
);
final height =
message.size.height + reply.size.height - replySlideUnderDistance;
final width = max(
reply.size.width,
message.size.width,
);
message.layout(
BoxConstraints.tightFor(width: width),
parentUsesSize: true,
);
size = constraints.constrain(Size(width, height));
final messageParentData = message.parentData as _ReplyLayoutParentData;
messageParentData.offset = Offset(
0,
reply.size.height - replySlideUnderDistance,
);
final replyParentData = reply.parentData as _ReplyLayoutParentData;
replyParentData.offset = Offset(message.size.width - reply.size.width, 0);
}
@override
void paint(PaintingContext context, Offset offset) {
defaultPaint(context, offset);
}
@override
bool hitTestChildren(HitTestResult result, {Offset position}) {
return defaultHitTestChildren(result, position: position);
}
}
class _ReplyLayoutParentData extends ContainerBoxParentData<RenderBox> {}
/// Helper widget to make a [MessageBubble] clickable with a ripple. Defaults
/// to showing a context menu on tap, and being selected on long press.
///
......@@ -377,7 +512,7 @@ class MessageInfo extends StatelessWidget {
static bool necessary(BuildContext context) {
final bubble = MessageBubble.of(context);
return bubble.isEndOfGroup;
return bubble.isEndOfGroup || MessageState.necessaryInBubble(context);
}
@override
......@@ -389,10 +524,10 @@ class MessageInfo extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
if (MessageState.necessary(bubble.message)) ...[
if (MessageState.necessaryInBubble(context)) ...[
MessageState(
message: bubble.message,
color: Colors.white,
color: context.pattleTheme.chat.myMessage.contentColor,
size: 14,
),
SizedBox(width: 4),
......@@ -433,11 +568,13 @@ class Sender extends StatelessWidget {
Widget build(BuildContext context) {
final bubble = MessageBubble.of(context);
var color = personalizedColor ? bubble.message.sender.color(context) : null;
return Text(
bubble.message.sender.name,
style: TextStyle(
fontWeight: FontWeight.bold,
color: personalizedColor ? bubble.message.sender.color(context) : null,
color: bubble.isRepliedTo ? color?.withOpacity(0.70) : color,
),
);
}
......
......@@ -26,6 +26,8 @@ class ChatMessage {
final ChatMember sender;
final ChatMessage inReplyTo;
bool get isReply => inReplyTo != null;
bool get isMine => sender.isYou;
/// Message that redacted this message, if any.
......
......@@ -18,6 +18,8 @@
import 'package:flutter/material.dart';
import 'package:matrix_sdk/matrix_sdk.dart';
import '../chat/widgets/bubble/message.dart';
import '../models/chat_message.dart';
class MessageState extends StatelessWidget {
......@@ -34,6 +36,12 @@ class MessageState extends StatelessWidget {
static bool necessary(ChatMessage message) => message.isMine;
static bool necessaryInBubble(BuildContext context) {
final bubble = MessageBubble.of(context);
return bubble.message.isMine ? bubble.isEndOfGroup : false;
}
@override
Widget build(BuildContext context) {
return Icon(
......
......@@ -191,9 +191,11 @@ packages:
flutter_html:
dependency: "direct main"
description:
name: flutter_html
url: "https://pub.dartlang.org"
source: hosted
path: "."
ref: "71406a09f9e591f9d35635e6a086007ccc678ae3"
resolved-ref: "71406a09f9e591f9d35635e6a086007ccc678ae3"
url: "https://github.com/pattle-org/flutter_html.git"
source: git
version: "0.11.1"
flutter_launcher_icons:
dependency: "direct dev"
......
......@@ -36,7 +36,11 @@ dependencies:
device_info: ^0.4.1+1
package_info: ^0.4.0+5
flutter_html: ^0.11.1
# TODO: Use official package when PR is merged
flutter_html:
git:
url: https://github.com/pattle-org/flutter_html.git
ref: 71406a09f9e591f9d35635e6a086007ccc678ae3
flutter_dotenv: ^2.0.1
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment