swith to event aggregations and render message edits and reactions
This commit is contained in:
parent
761fd91e2e
commit
6fa9c182f2
11 changed files with 512 additions and 150 deletions
|
|
@ -12,6 +12,7 @@ import 'package:flutter/material.dart';
|
|||
import '../adaptive_page_layout.dart';
|
||||
import '../avatar.dart';
|
||||
import '../matrix.dart';
|
||||
import '../message_reactions.dart';
|
||||
import 'state_message.dart';
|
||||
|
||||
class Message extends StatelessWidget {
|
||||
|
|
@ -64,11 +65,13 @@ class Message extends StatelessWidget {
|
|||
var rowMainAxisAlignment =
|
||||
ownMessage ? MainAxisAlignment.end : MainAxisAlignment.start;
|
||||
|
||||
final displayEvent = event.getDisplayEvent(timeline);
|
||||
|
||||
if (event.showThumbnail) {
|
||||
color = Theme.of(context).scaffoldBackgroundColor.withOpacity(0.66);
|
||||
textColor = Theme.of(context).textTheme.bodyText2.color;
|
||||
} else if (ownMessage) {
|
||||
color = event.status == -1
|
||||
color = displayEvent.status == -1
|
||||
? Colors.redAccent
|
||||
: Theme.of(context).primaryColor;
|
||||
}
|
||||
|
|
@ -91,15 +94,14 @@ class Message extends StatelessWidget {
|
|||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
if (event.isReply)
|
||||
if (event.relationshipType == RelationshipTypes.Reply)
|
||||
FutureBuilder<Event>(
|
||||
future: event.getReplyEvent(timeline),
|
||||
builder: (BuildContext context, snapshot) {
|
||||
final replyEvent = snapshot.hasData
|
||||
? snapshot.data
|
||||
: Event(
|
||||
eventId: event.content['m.relates_to']
|
||||
['m.in_reply_to']['event_id'],
|
||||
eventId: event.relationshipEventId,
|
||||
content: {'msgtype': 'm.text', 'body': '...'},
|
||||
senderId: event.senderId,
|
||||
type: 'm.room.message',
|
||||
|
|
@ -110,18 +112,18 @@ class Message extends StatelessWidget {
|
|||
);
|
||||
return Container(
|
||||
margin: EdgeInsets.symmetric(vertical: 4.0),
|
||||
child:
|
||||
ReplyContent(replyEvent, lightText: ownMessage),
|
||||
child: ReplyContent(replyEvent,
|
||||
lightText: ownMessage, timeline: timeline),
|
||||
);
|
||||
},
|
||||
),
|
||||
MessageContent(
|
||||
event,
|
||||
displayEvent,
|
||||
textColor: textColor,
|
||||
),
|
||||
if (event.type == EventTypes.Encrypted &&
|
||||
event.messageType == MessageTypes.BadEncrypted &&
|
||||
event.content['can_request_session'] == true)
|
||||
if (displayEvent.type == EventTypes.Encrypted &&
|
||||
displayEvent.messageType == MessageTypes.BadEncrypted &&
|
||||
displayEvent.content['can_request_session'] == true)
|
||||
RaisedButton(
|
||||
color: color.withAlpha(100),
|
||||
child: Text(
|
||||
|
|
@ -129,15 +131,18 @@ class Message extends StatelessWidget {
|
|||
style: TextStyle(color: textColor),
|
||||
),
|
||||
onPressed: () => SimpleDialogs(context)
|
||||
.tryRequestWithLoadingDialog(event.requestKey()),
|
||||
.tryRequestWithLoadingDialog(
|
||||
displayEvent.requestKey()),
|
||||
),
|
||||
SizedBox(height: 4),
|
||||
Opacity(
|
||||
opacity: 0,
|
||||
child: _MetaRow(
|
||||
event,
|
||||
event, // meta information should be from the unedited event
|
||||
ownMessage,
|
||||
textColor,
|
||||
timeline,
|
||||
displayEvent,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
@ -150,6 +155,8 @@ class Message extends StatelessWidget {
|
|||
event,
|
||||
ownMessage,
|
||||
textColor,
|
||||
timeline,
|
||||
displayEvent,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
@ -170,6 +177,32 @@ class Message extends StatelessWidget {
|
|||
} else {
|
||||
rowChildren.insert(0, avatarOrSizedBox);
|
||||
}
|
||||
final row = Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
mainAxisAlignment: rowMainAxisAlignment,
|
||||
children: rowChildren,
|
||||
);
|
||||
Widget container;
|
||||
if (event.hasAggregatedEvents(timeline, RelationshipTypes.Reaction)) {
|
||||
container = Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment:
|
||||
ownMessage ? CrossAxisAlignment.end : CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
row,
|
||||
Padding(
|
||||
padding: EdgeInsets.only(
|
||||
top: 4.0,
|
||||
left: (ownMessage ? 0 : Avatar.defaultSize) + 12.0,
|
||||
right: (ownMessage ? Avatar.defaultSize : 0) + 12.0,
|
||||
),
|
||||
child: MessageReactions(event, timeline),
|
||||
),
|
||||
],
|
||||
);
|
||||
} else {
|
||||
container = row;
|
||||
}
|
||||
|
||||
return InkWell(
|
||||
onHover: (b) => useMouse = true,
|
||||
|
|
@ -185,11 +218,7 @@ class Message extends StatelessWidget {
|
|||
child: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: 8.0, right: 8.0, bottom: sameSender ? 4.0 : 8.0),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
mainAxisAlignment: rowMainAxisAlignment,
|
||||
children: rowChildren,
|
||||
),
|
||||
child: container,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
@ -200,8 +229,12 @@ class _MetaRow extends StatelessWidget {
|
|||
final Event event;
|
||||
final bool ownMessage;
|
||||
final Color color;
|
||||
final Timeline timeline;
|
||||
final Event displayEvent;
|
||||
|
||||
const _MetaRow(this.event, this.ownMessage, this.color, {Key key})
|
||||
const _MetaRow(
|
||||
this.event, this.ownMessage, this.color, this.timeline, this.displayEvent,
|
||||
{Key key})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
|
|
@ -229,10 +262,16 @@ class _MetaRow extends StatelessWidget {
|
|||
fontSize: 11,
|
||||
),
|
||||
),
|
||||
if (event.hasAggregatedEvents(timeline, RelationshipTypes.Edit))
|
||||
Icon(
|
||||
Icons.edit,
|
||||
size: 12,
|
||||
color: color,
|
||||
),
|
||||
if (ownMessage) SizedBox(width: 2),
|
||||
if (ownMessage)
|
||||
Icon(
|
||||
event.statusIcon,
|
||||
displayEvent.statusIcon,
|
||||
size: 12,
|
||||
color: color,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -194,7 +194,6 @@ class MatrixState extends State<Matrix> {
|
|||
verificationMethods.add(KeyVerificationMethod.emoji);
|
||||
}
|
||||
client = Client(widget.clientName,
|
||||
debug: false,
|
||||
enableE2eeRecovery: true,
|
||||
verificationMethods: verificationMethods,
|
||||
importantStateEvents: <String>{
|
||||
|
|
|
|||
|
|
@ -40,7 +40,6 @@ class MessageContent extends StatelessWidget {
|
|||
case MessageTypes.Text:
|
||||
case MessageTypes.Notice:
|
||||
case MessageTypes.Emote:
|
||||
case MessageTypes.Reply:
|
||||
if (Matrix.of(context).renderHtml &&
|
||||
!event.redacted &&
|
||||
event.content['format'] == 'org.matrix.custom.html' &&
|
||||
|
|
|
|||
140
lib/components/message_reactions.dart
Normal file
140
lib/components/message_reactions.dart
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_advanced_networkimage/provider.dart';
|
||||
|
||||
import 'matrix.dart';
|
||||
|
||||
class MessageReactions extends StatelessWidget {
|
||||
final Event event;
|
||||
final Timeline timeline;
|
||||
|
||||
const MessageReactions(this.event, this.timeline);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final allReactionEvents =
|
||||
event.aggregatedEvents(timeline, RelationshipTypes.Reaction);
|
||||
final reactionMap = <String, _ReactionEntry>{};
|
||||
for (final e in allReactionEvents) {
|
||||
if (e.content['m.relates_to'].containsKey('key')) {
|
||||
final key = e.content['m.relates_to']['key'];
|
||||
if (!reactionMap.containsKey(key)) {
|
||||
reactionMap[key] = _ReactionEntry(
|
||||
key: key,
|
||||
count: 0,
|
||||
reacted: false,
|
||||
);
|
||||
}
|
||||
reactionMap[key].count++;
|
||||
reactionMap[key].reacted |= e.senderId == e.room.client.userID;
|
||||
}
|
||||
}
|
||||
final reactionList = reactionMap.values.toList();
|
||||
reactionList.sort((a, b) => b.count - a.count > 0 ? 1 : -1);
|
||||
return Wrap(
|
||||
spacing: 4.0,
|
||||
runSpacing: 4.0,
|
||||
children: reactionList
|
||||
.map((r) => _Reaction(
|
||||
reactionKey: r.key,
|
||||
count: r.count,
|
||||
reacted: r.reacted,
|
||||
onTap: () {
|
||||
if (r.reacted) {
|
||||
final evt = allReactionEvents.firstWhere(
|
||||
(e) =>
|
||||
e.senderId == e.room.client.userID &&
|
||||
e.content['m.relates_to']['key'] == r.key,
|
||||
orElse: () => null);
|
||||
if (evt != null) {
|
||||
evt.redact();
|
||||
}
|
||||
} else {
|
||||
event.room.sendReaction(event.eventId, r.key);
|
||||
}
|
||||
},
|
||||
))
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Reaction extends StatelessWidget {
|
||||
final String reactionKey;
|
||||
final int count;
|
||||
final bool reacted;
|
||||
final void Function() onTap;
|
||||
|
||||
const _Reaction({this.reactionKey, this.count, this.reacted, this.onTap});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final borderColor = reacted ? Colors.red : Theme.of(context).primaryColor;
|
||||
final textColor = Theme.of(context).brightness == Brightness.dark
|
||||
? Colors.white
|
||||
: Colors.black;
|
||||
final color = Theme.of(context).secondaryHeaderColor;
|
||||
final fontSize = DefaultTextStyle.of(context).style.fontSize;
|
||||
final padding = fontSize / 5;
|
||||
Widget content;
|
||||
if (reactionKey.startsWith('mxc://')) {
|
||||
final src = Uri.parse(reactionKey)?.getThumbnail(
|
||||
Matrix.of(context).client,
|
||||
width: 9999,
|
||||
height: fontSize * MediaQuery.of(context).devicePixelRatio,
|
||||
method: ThumbnailMethod.scale,
|
||||
);
|
||||
content = Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Image(
|
||||
image: AdvancedNetworkImage(
|
||||
src,
|
||||
useDiskCache: !kIsWeb,
|
||||
),
|
||||
height: fontSize,
|
||||
),
|
||||
Text(count.toString(),
|
||||
style: TextStyle(
|
||||
color: textColor,
|
||||
fontSize: DefaultTextStyle.of(context).style.fontSize,
|
||||
)),
|
||||
],
|
||||
);
|
||||
} else {
|
||||
var renderKey = reactionKey;
|
||||
if (renderKey.length > 10) {
|
||||
renderKey = renderKey.substring(0, 7) + '...';
|
||||
}
|
||||
content = Text('$renderKey $count',
|
||||
style: TextStyle(
|
||||
color: textColor,
|
||||
fontSize: DefaultTextStyle.of(context).style.fontSize,
|
||||
));
|
||||
}
|
||||
return InkWell(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
border: Border.all(
|
||||
width: fontSize / 20,
|
||||
color: borderColor,
|
||||
),
|
||||
borderRadius: BorderRadius.all(Radius.circular(padding * 2)),
|
||||
),
|
||||
padding: EdgeInsets.all(padding),
|
||||
child: content,
|
||||
),
|
||||
onTap: () => onTap != null ? onTap() : null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ReactionEntry {
|
||||
String key;
|
||||
int count;
|
||||
bool reacted;
|
||||
|
||||
_ReactionEntry({this.key, this.count, this.reacted});
|
||||
}
|
||||
|
|
@ -8,23 +8,29 @@ import 'matrix.dart';
|
|||
class ReplyContent extends StatelessWidget {
|
||||
final Event replyEvent;
|
||||
final bool lightText;
|
||||
final Timeline timeline;
|
||||
|
||||
const ReplyContent(this.replyEvent, {this.lightText = false, Key key})
|
||||
const ReplyContent(this.replyEvent,
|
||||
{this.lightText = false, Key key, this.timeline})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget replyBody;
|
||||
if (replyEvent != null &&
|
||||
final displayEvent = replyEvent != null && timeline != null
|
||||
? replyEvent.getDisplayEvent(timeline)
|
||||
: replyEvent;
|
||||
if (displayEvent != null &&
|
||||
Matrix.of(context).renderHtml &&
|
||||
[EventTypes.Message, EventTypes.Encrypted].contains(replyEvent.type) &&
|
||||
[EventTypes.Message, EventTypes.Encrypted]
|
||||
.contains(displayEvent.type) &&
|
||||
[MessageTypes.Text, MessageTypes.Notice, MessageTypes.Emote]
|
||||
.contains(replyEvent.messageType) &&
|
||||
!replyEvent.redacted &&
|
||||
replyEvent.content['format'] == 'org.matrix.custom.html' &&
|
||||
replyEvent.content['formatted_body'] is String) {
|
||||
String html = replyEvent.content['formatted_body'];
|
||||
if (replyEvent.messageType == MessageTypes.Emote) {
|
||||
.contains(displayEvent.messageType) &&
|
||||
!displayEvent.redacted &&
|
||||
displayEvent.content['format'] == 'org.matrix.custom.html' &&
|
||||
displayEvent.content['formatted_body'] is String) {
|
||||
String html = displayEvent.content['formatted_body'];
|
||||
if (displayEvent.messageType == MessageTypes.Emote) {
|
||||
html = '* $html';
|
||||
}
|
||||
replyBody = HtmlMessage(
|
||||
|
|
@ -36,11 +42,11 @@ class ReplyContent extends StatelessWidget {
|
|||
fontSize: DefaultTextStyle.of(context).style.fontSize,
|
||||
),
|
||||
maxLines: 1,
|
||||
room: replyEvent.room,
|
||||
room: displayEvent.room,
|
||||
);
|
||||
} else {
|
||||
replyBody = Text(
|
||||
replyEvent?.getLocalizedBody(
|
||||
displayEvent?.getLocalizedBody(
|
||||
L10n.of(context),
|
||||
withSenderNamePrefix: false,
|
||||
hideReply: true,
|
||||
|
|
@ -71,7 +77,7 @@ class ReplyContent extends StatelessWidget {
|
|||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
(replyEvent?.sender?.calcDisplayname() ?? '') + ':',
|
||||
(displayEvent?.sender?.calcDisplayname() ?? '') + ':',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
|
|
|
|||
|
|
@ -8,6 +8,18 @@ import 'package:moor/moor.dart';
|
|||
import 'package:moor/isolate.dart';
|
||||
import 'cipher_db.dart' as cipher;
|
||||
|
||||
class DatabaseNoTransactions extends Database {
|
||||
DatabaseNoTransactions.connect(DatabaseConnection connection)
|
||||
: super.connect(connection);
|
||||
|
||||
// moor transactions are sometimes rather weird and freeze. Until there is a
|
||||
// proper fix in moor we override that there aren't actually using transactions
|
||||
@override
|
||||
Future<T> transaction<T>(Future<T> Function() action) async {
|
||||
return action();
|
||||
}
|
||||
}
|
||||
|
||||
bool _inited = false;
|
||||
|
||||
// see https://moor.simonbinder.eu/docs/advanced-features/isolates/
|
||||
|
|
@ -57,7 +69,7 @@ Future<Database> constructDb(
|
|||
receivePort.sendPort, targetPath, password, logStatements),
|
||||
);
|
||||
final isolate = (await receivePort.first as MoorIsolate);
|
||||
return Database.connect(await isolate.connect());
|
||||
return DatabaseNoTransactions.connect(await isolate.connect());
|
||||
}
|
||||
|
||||
Future<String> getLocalstorage(String key) async {
|
||||
|
|
|
|||
|
|
@ -161,7 +161,7 @@ abstract class FirebaseController {
|
|||
} else {
|
||||
final platform = kIsWeb ? 'Web' : Platform.operatingSystem;
|
||||
final clientName = 'FluffyChat $platform';
|
||||
client = Client(clientName, debug: false);
|
||||
client = Client(clientName);
|
||||
client.database = await getDatabase(client);
|
||||
client.connect();
|
||||
await client.onLoginStateChanged.stream
|
||||
|
|
|
|||
|
|
@ -76,6 +76,8 @@ class _ChatState extends State<_Chat> {
|
|||
|
||||
Event replyEvent;
|
||||
|
||||
Event editEvent;
|
||||
|
||||
bool showScrollDownButton = false;
|
||||
|
||||
bool get selectMode => selectedEvents.isNotEmpty;
|
||||
|
|
@ -174,13 +176,15 @@ class _ChatState extends State<_Chat> {
|
|||
|
||||
void send() {
|
||||
if (sendController.text.isEmpty) return;
|
||||
room.sendTextEvent(sendController.text, inReplyTo: replyEvent);
|
||||
room.sendTextEvent(sendController.text,
|
||||
inReplyTo: replyEvent, editEventId: editEvent?.eventId);
|
||||
sendController.text = '';
|
||||
if (replyEvent != null) {
|
||||
setState(() => replyEvent = null);
|
||||
}
|
||||
|
||||
setState(() => inputText = '');
|
||||
setState(() {
|
||||
inputText = '';
|
||||
replyEvent = null;
|
||||
editEvent = null;
|
||||
});
|
||||
}
|
||||
|
||||
void sendFileAction(BuildContext context) async {
|
||||
|
|
@ -289,8 +293,17 @@ class _ChatState extends State<_Chat> {
|
|||
Navigator.of(context).popUntil((r) => r.isFirst);
|
||||
}
|
||||
|
||||
void sendAgainAction() {
|
||||
selectedEvents.first.sendAgain();
|
||||
void sendAgainAction(Timeline timeline) {
|
||||
final event = selectedEvents.first;
|
||||
if (event.status == -1) {
|
||||
event.sendAgain();
|
||||
}
|
||||
final allEditEvents = event
|
||||
.aggregatedEvents(timeline, RelationshipTypes.Edit)
|
||||
.where((e) => e.status == -1);
|
||||
for (final e in allEditEvents) {
|
||||
e.sendAgain();
|
||||
}
|
||||
setState(() => selectedEvents.clear());
|
||||
}
|
||||
|
||||
|
|
@ -411,6 +424,23 @@ class _ChatState extends State<_Chat> {
|
|||
.numberSelected(selectedEvents.length.toString())),
|
||||
actions: selectMode
|
||||
? <Widget>[
|
||||
if (selectedEvents.length == 1 &&
|
||||
selectedEvents.first.status > 0 &&
|
||||
selectedEvents.first.senderId == client.userID)
|
||||
IconButton(
|
||||
icon: Icon(Icons.edit),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
editEvent = selectedEvents.first;
|
||||
sendController.text = editEvent
|
||||
.getDisplayEvent(timeline)
|
||||
.getLocalizedBody(L10n.of(context),
|
||||
withSenderNamePrefix: false, hideReply: true);
|
||||
selectedEvents.clear();
|
||||
});
|
||||
inputFocus.requestFocus();
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(Icons.content_copy),
|
||||
onPressed: () => copyEventsAction(context),
|
||||
|
|
@ -467,7 +497,16 @@ class _ChatState extends State<_Chat> {
|
|||
room.sendReadReceipt(timeline.events.first.eventId);
|
||||
}
|
||||
|
||||
if (timeline.events.isEmpty) return Container();
|
||||
final filteredEvents = timeline.events
|
||||
.where((e) =>
|
||||
![
|
||||
RelationshipTypes.Edit,
|
||||
RelationshipTypes.Reaction
|
||||
].contains(e.relationshipType) &&
|
||||
e.type != 'm.reaction')
|
||||
.toList();
|
||||
|
||||
if (filteredEvents.isEmpty) return Container();
|
||||
|
||||
return ListView.builder(
|
||||
padding: EdgeInsets.symmetric(
|
||||
|
|
@ -479,10 +518,10 @@ class _ChatState extends State<_Chat> {
|
|||
2),
|
||||
),
|
||||
reverse: true,
|
||||
itemCount: timeline.events.length + 2,
|
||||
itemCount: filteredEvents.length + 2,
|
||||
controller: _scrollController,
|
||||
itemBuilder: (BuildContext context, int i) {
|
||||
return i == timeline.events.length + 1
|
||||
return i == filteredEvents.length + 1
|
||||
? _loadingHistory
|
||||
? Container(
|
||||
height: 50,
|
||||
|
|
@ -512,7 +551,7 @@ class _ChatState extends State<_Chat> {
|
|||
? Duration(milliseconds: 0)
|
||||
: Duration(milliseconds: 500),
|
||||
alignment:
|
||||
timeline.events.first.senderId ==
|
||||
filteredEvents.first.senderId ==
|
||||
client.userID
|
||||
? Alignment.topRight
|
||||
: Alignment.topLeft,
|
||||
|
|
@ -530,7 +569,7 @@ class _ChatState extends State<_Chat> {
|
|||
bottom: 8,
|
||||
),
|
||||
)
|
||||
: Message(timeline.events[i - 1],
|
||||
: Message(filteredEvents[i - 1],
|
||||
onAvatarTab: (Event event) {
|
||||
sendController.text +=
|
||||
' ${event.senderId}';
|
||||
|
|
@ -553,10 +592,10 @@ class _ChatState extends State<_Chat> {
|
|||
},
|
||||
longPressSelect: selectedEvents.isEmpty,
|
||||
selected: selectedEvents
|
||||
.contains(timeline.events[i - 1]),
|
||||
.contains(filteredEvents[i - 1]),
|
||||
timeline: timeline,
|
||||
nextEvent: i >= 2
|
||||
? timeline.events[i - 2]
|
||||
? filteredEvents[i - 2]
|
||||
: null);
|
||||
});
|
||||
},
|
||||
|
|
@ -565,17 +604,23 @@ class _ChatState extends State<_Chat> {
|
|||
ConnectionStatusHeader(),
|
||||
AnimatedContainer(
|
||||
duration: Duration(milliseconds: 300),
|
||||
height: replyEvent != null ? 56 : 0,
|
||||
height: editEvent != null || replyEvent != null ? 56 : 0,
|
||||
child: Material(
|
||||
color: Theme.of(context).secondaryHeaderColor,
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
IconButton(
|
||||
icon: Icon(Icons.close),
|
||||
onPressed: () => setState(() => replyEvent = null),
|
||||
onPressed: () => setState(() {
|
||||
replyEvent = null;
|
||||
editEvent = null;
|
||||
}),
|
||||
),
|
||||
Expanded(
|
||||
child: ReplyContent(replyEvent),
|
||||
child: replyEvent != null
|
||||
? ReplyContent(replyEvent, timeline: timeline)
|
||||
: _EditContent(
|
||||
editEvent?.getDisplayEvent(timeline)),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
@ -611,7 +656,10 @@ class _ChatState extends State<_Chat> {
|
|||
),
|
||||
),
|
||||
selectedEvents.length == 1
|
||||
? selectedEvents.first.status > 0
|
||||
? selectedEvents.first
|
||||
.getDisplayEvent(timeline)
|
||||
.status >
|
||||
0
|
||||
? Container(
|
||||
height: 56,
|
||||
child: FlatButton(
|
||||
|
|
@ -629,7 +677,7 @@ class _ChatState extends State<_Chat> {
|
|||
height: 56,
|
||||
child: FlatButton(
|
||||
onPressed: () =>
|
||||
sendAgainAction(),
|
||||
sendAgainAction(timeline),
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
Text(L10n.of(context)
|
||||
|
|
@ -804,3 +852,38 @@ class _ChatState extends State<_Chat> {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _EditContent extends StatelessWidget {
|
||||
final Event event;
|
||||
|
||||
_EditContent(this.event);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (event == null) {
|
||||
return Container();
|
||||
}
|
||||
return Row(
|
||||
children: <Widget>[
|
||||
Icon(
|
||||
Icons.edit,
|
||||
color: Theme.of(context).primaryColor,
|
||||
),
|
||||
Container(width: 15.0),
|
||||
Text(
|
||||
event?.getLocalizedBody(
|
||||
L10n.of(context),
|
||||
withSenderNamePrefix: false,
|
||||
hideReply: true,
|
||||
) ??
|
||||
'',
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).textTheme.bodyText2.color,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue