Commit 9ae031eb authored by Wilko Manger's avatar Wilko Manger

Implement replies

parent 4c16008b
Pipeline #392 failed with stages
in 14 seconds
A new version is available on F-droid!
Lots of changes again, including:
- Render HTML formatting in messages!
- Replies are now rendered!
- Show date headers between messages of different days!
- Render usernames with a color in chat timeline
- Add loading indicators (when logging in, loading chats, etc.)
- Show error banner at the top if syncing failed
- Syncing now resumes after a failed attempt (no more restarting)
- Fix messages not being sent if connection was lost and the app restarted
![Preview image](/CHANGELOG/0.4.0.png)
To install this release, add the following repo in F-droid:
https://fdroid.pattle.im/?fingerprint=E91F63CA6AE04F8E7EA53E52242EAF8779559209B8A342F152F9E7265E3EA729
......@@ -40,8 +40,8 @@ abstract class Bubble extends Item {
Bubble({
@required this.item,
@required ChatItem previousItem,
@required ChatItem nextItem,
ChatItem previousItem,
ChatItem nextItem,
@required this.isMine
}) :
event = item.event,
......@@ -53,8 +53,8 @@ abstract class Bubble extends Item {
factory Bubble.fromItem({
@required ChatEvent item,
@required ChatItem previousItem,
@required ChatItem nextItem,
ChatItem previousItem,
ChatItem nextItem,
@required bool isMine
}) {
if (item.event is TextMessageEvent) {
......@@ -83,6 +83,28 @@ abstract class Bubble extends Item {
}
}
factory Bubble.asReply({
@required RoomEvent replyTo,
@required bool isMine
}) {
final item = ChatEvent(replyTo);
if (replyTo is TextMessageEvent) {
return TextBubble(
item: item,
isMine: isMine,
isRepliedTo: true,
);
} else if (replyTo is ImageMessageEvent) {
return ImageBubble(
item: item,
isMine: isMine,
isRepliedTo: true,
);
} else {
return null;
}
}
@override
Widget build(BuildContext context);
......
......@@ -40,16 +40,18 @@ class ImageBubble extends MessageBubble {
ImageBubble({
@required ChatEvent item,
@required ChatItem previousItem,
@required ChatItem nextItem,
@required bool isMine
ChatItem previousItem,
ChatItem nextItem,
@required bool isMine,
bool isRepliedTo = false,
}) :
event = item.event,
super(
item: item,
previousItem: previousItem,
nextItem: nextItem,
isMine: isMine
isMine: isMine,
isRepliedTo: isRepliedTo
);
void _onTap(BuildContext context) {
......
......@@ -36,11 +36,14 @@ abstract class MessageBubble extends Bubble {
static const _betweenGroupMargin = 4.0;
static const _oppositeMargin = 64.0;
final bool isRepliedTo;
MessageBubble({
@required ChatEvent item,
@required ChatItem previousItem,
@required ChatItem nextItem,
@required bool isMine
ChatItem previousItem,
ChatItem nextItem,
@required bool isMine,
this.isRepliedTo = false
}) :super(
item: item,
previousItem: previousItem,
......@@ -96,7 +99,7 @@ abstract class MessageBubble extends Bubble {
color = colorOf(event.sender);
}
if (isStartOfGroup) {
if (isStartOfGroup || (isRepliedTo && !isMine)) {
return Text(displayNameOf(event.sender),
style: textStyle(context, color: color).copyWith(
fontWeight: FontWeight.bold
......@@ -110,6 +113,11 @@ abstract class MessageBubble extends Bubble {
bool _isStartOfGroup;
@protected
bool get isStartOfGroup {
if (isRepliedTo) {
_isStartOfGroup = false;
return _isStartOfGroup;
}
if (_isStartOfGroup == null) {
if (previousItem is! ChatEvent
|| (previousItem is ChatEvent
......@@ -147,6 +155,11 @@ abstract class MessageBubble extends Bubble {
bool _isEndOfGroup;
@protected
bool get isEndOfGroup {
if (isRepliedTo) {
_isEndOfGroup = false;
return _isEndOfGroup;
}
if (_isEndOfGroup == null) {
if (nextItem is! ChatEvent
|| (nextItem is ChatEvent
......@@ -252,12 +265,13 @@ abstract class MessageBubble extends Bubble {
children: [
Flexible(
child: Padding(
padding: EdgeInsets.only(
left: _oppositeMargin,
right: Item.sideMargin,
bottom: marginBottom(),
top: marginTop(),
),
padding: !isRepliedTo ?
EdgeInsets.only(
left: _oppositeMargin,
right: Item.sideMargin,
bottom: marginBottom(),
top: marginTop(),
) : EdgeInsets.only(),
child: Material(
color: LightColors.red[450],
elevation: 1,
......@@ -284,17 +298,18 @@ abstract class MessageBubble extends Bubble {
children: [
Flexible(
child: Padding(
padding: EdgeInsets.only(
left: Item.sideMargin,
right: _oppositeMargin,
bottom: marginBottom(),
top: marginTop()
),
padding: !isRepliedTo ?
EdgeInsets.only(
left: Item.sideMargin,
right: _oppositeMargin,
bottom: marginBottom(),
top: marginTop()
) : EdgeInsets.only(),
child: Material(
color: Colors.white,
elevation: 1,
shape: border(),
child: buildTheirs(context)
child: buildTheirs(context),
)
)
),
......
......@@ -18,32 +18,40 @@
import 'package:flutter/material.dart';
import 'package:matrix_sdk/matrix_sdk.dart';
import 'package:pattle/src/ui/main/models/chat_item.dart';
import 'package:flutter_html/flutter_html.dart';
import 'package:pattle/src/ui/util/user.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:pattle/src/di.dart' as di;
import 'package:flutter_html/flutter_html.dart';
import 'bubble.dart';
import 'message_bubble.dart';
class TextBubble extends MessageBubble {
static const _replyMargin = 8.0;
@override
final TextMessageEvent event;
final LocalUser me = di.getLocalUser();
TextBubble({
@required ChatEvent item,
@required ChatItem previousItem,
@required ChatItem nextItem,
@required bool isMine
ChatItem previousItem,
ChatItem nextItem,
@required bool isMine,
bool isRepliedTo = false
}) :
event = item.event,
super(
item: item,
previousItem: previousItem,
nextItem: nextItem,
isMine: isMine
isMine: isMine,
isRepliedTo: isRepliedTo
);
TextStyle linkStyle(BuildContext context) {
TextStyle _linkStyle(BuildContext context) {
if (isMine) {
return textStyle(context).copyWith(
decoration: TextDecoration.underline
......@@ -56,19 +64,52 @@ class TextBubble extends MessageBubble {
}
}
Widget buildContent(BuildContext context) =>
Html(
Widget _buildRepliedTo(BuildContext context) {
final repliedId = event.content.inReplyToId;
if (repliedId != null) {
return FutureBuilder<Event>(
future: event.room.events[repliedId],
builder: (BuildContext context, AsyncSnapshot<Event> snapshot) {
final repliedTo = snapshot.data;
if (repliedTo != null && repliedTo is TextMessageEvent) {
return !isRepliedTo ? Padding(
padding: EdgeInsets.only(
top: !isMine ? 4 : 0,
bottom: _replyMargin,
),
// Only build the replied to message if this itself
// is not a replied to message (to prevent very long
// reply chains)
child: Bubble.asReply(
replyTo: repliedTo,
isMine: repliedTo.sender.id == di.getLocalUser().id
),
) : Container();
} else {
return Container(height: 0, width: 0);
}
},
);
} else {
return Container(height: 0, width: 0);
}
}
Widget buildContent(BuildContext context) {
return Html(
data: event.content.formattedBody ?? '',
useRichText: true,
defaultTextStyle: textStyle(context),
fillWidth: false,
linkStyle: linkStyle(context),
linkStyle: _linkStyle(context),
onLinkTap: (url) async {
if (await canLaunch(url)) {
await launch(url);
}
},
);
}
@protected
Widget buildMine(BuildContext context) {
......@@ -95,6 +136,7 @@ class TextBubble extends MessageBubble {
child: Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: <Widget>[
_buildRepliedTo(context),
buildContent(context),
SizedBox(height: 4),
bottom
......@@ -117,6 +159,7 @@ class TextBubble extends MessageBubble {
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
buildSender(context),
_buildRepliedTo(context),
SizedBox(height: 4),
buildContent(context),
SizedBox(height: 4),
......
......@@ -215,7 +215,7 @@ packages:
name: matrix_sdk
url: "https://pub.dartlang.org"
source: hosted
version: "0.4.10"
version: "0.4.12"
matrix_sdk_sqflite:
dependency: "direct main"
description:
......
......@@ -12,7 +12,7 @@ dependencies:
injector: ^1.0.6
matrix_sdk: ^0.4.10
matrix_sdk: ^0.4.12
matrix_sdk_sqflite: ^0.1.17
rxdart: ^0.21.0
......
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