@@ -470,6 +470,7 @@ class ThreadPage extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Color.fromRGBO(252, 252, 252, 1),
|
||||
appBar: ThreadHeader(
|
||||
parent: parent,
|
||||
),
|
||||
|
||||
@@ -113,11 +113,12 @@ class MessageActionsModal extends StatelessWidget {
|
||||
messageTheme: messageTheme,
|
||||
showReactions: false,
|
||||
showUsername: false,
|
||||
showReplyIndicator: false,
|
||||
showThreadReplyIndicator: false,
|
||||
showUserAvatar: showUserAvatar,
|
||||
showTimestamp: false,
|
||||
translateUserAvatar: false,
|
||||
showReactionPickerIndicator: true,
|
||||
showInChannelIndicator: false,
|
||||
showSendingIndicator: DisplayWidget.gone,
|
||||
shape: messageShape,
|
||||
),
|
||||
|
||||
@@ -301,7 +301,7 @@ class MessageInputState extends State<MessageInput> {
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: Text('Send also as direct message'),
|
||||
child: Text('Also send as direct message'),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -241,7 +241,7 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData) {
|
||||
return Center(
|
||||
child: CircularProgressIndicator(),
|
||||
child: const CircularProgressIndicator(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -336,18 +336,27 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: <Widget>[
|
||||
buildParentMessage(widget.parentMessage),
|
||||
Padding(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 32),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
Color(0XFFF7F7F7),
|
||||
Color(0XFFFCFCFC),
|
||||
],
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text(
|
||||
'Start of thread',
|
||||
'${widget.parentMessage.replyCount} ${widget.parentMessage.replyCount == 1 ? 'Reply' : 'Replies'}',
|
||||
textAlign: TextAlign.center,
|
||||
style: StreamChatTheme.of(context)
|
||||
.channelTheme
|
||||
.channelHeaderTheme
|
||||
.lastMessageAt,
|
||||
),
|
||||
color: Theme.of(context)
|
||||
.accentColor
|
||||
.withAlpha(50),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -585,7 +594,7 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
if (!snapshot.data) {
|
||||
if (direction == QueryDirection.top) {
|
||||
return Container(
|
||||
height: 50,
|
||||
height: 52,
|
||||
width: double.infinity,
|
||||
);
|
||||
}
|
||||
@@ -679,7 +688,8 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
final isMyMessage = message.user.id == StreamChat.of(context).user.id;
|
||||
|
||||
return MessageWidget(
|
||||
showReplyIndicator: false,
|
||||
showThreadReplyIndicator: false,
|
||||
showInChannelIndicator: false,
|
||||
message: message,
|
||||
reverse: isMyMessage,
|
||||
showUsername: !isMyMessage,
|
||||
@@ -748,6 +758,8 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
bottom: index == 0 ? 30 : (isNextUser ? 2 : 7),
|
||||
top: 3,
|
||||
),
|
||||
showInChannelIndicator: widget.parentMessage == null,
|
||||
showThreadReplyIndicator: widget.parentMessage == null,
|
||||
showUsername: !isMyMessage && !isNextUser,
|
||||
showSendingIndicator: isMyMessage &&
|
||||
(index == 0 || message.status != MessageSendingStatus.SENT)
|
||||
|
||||
@@ -105,7 +105,7 @@ class MessageReactionsModal extends StatelessWidget {
|
||||
showReactions: false,
|
||||
showUsername: false,
|
||||
showUserAvatar: showUserAvatar,
|
||||
showReplyIndicator: false,
|
||||
showThreadReplyIndicator: false,
|
||||
showTimestamp: false,
|
||||
translateUserAvatar: false,
|
||||
showSendingIndicator: DisplayWidget.gone,
|
||||
|
||||
+303
-238
@@ -99,8 +99,11 @@ class MessageWidget extends StatefulWidget {
|
||||
|
||||
final bool allRead;
|
||||
|
||||
/// If true the widget will show the reply indicator
|
||||
final bool showReplyIndicator;
|
||||
/// If true the widget will show the thread reply indicator
|
||||
final bool showThreadReplyIndicator;
|
||||
|
||||
/// If true the widget will show the show in channel indicator
|
||||
final bool showInChannelIndicator;
|
||||
|
||||
/// The function called when tapping on UserAvatar
|
||||
final void Function(User) onUserAvatarTap;
|
||||
@@ -123,6 +126,7 @@ class MessageWidget extends StatefulWidget {
|
||||
/// Center user avatar with bottom of the message
|
||||
final bool translateUserAvatar;
|
||||
|
||||
///
|
||||
MessageWidget({
|
||||
Key key,
|
||||
@required this.message,
|
||||
@@ -139,7 +143,8 @@ class MessageWidget extends StatefulWidget {
|
||||
this.showReactionPickerIndicator = false,
|
||||
this.showUserAvatar = DisplayWidget.show,
|
||||
this.showSendingIndicator = DisplayWidget.show,
|
||||
this.showReplyIndicator = true,
|
||||
this.showThreadReplyIndicator = true,
|
||||
this.showInChannelIndicator = true,
|
||||
this.onThreadTap,
|
||||
this.showUsername = true,
|
||||
this.showTimestamp = true,
|
||||
@@ -211,10 +216,23 @@ class MessageWidget extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _MessageWidgetState extends State<MessageWidget> {
|
||||
bool get showThreadReplyIndicator =>
|
||||
widget.showThreadReplyIndicator && widget.message.replyCount > 0;
|
||||
|
||||
bool get showUsername => widget.showUsername;
|
||||
|
||||
bool get showTimeStamp =>
|
||||
widget.message.createdAt != null && widget.showTimestamp;
|
||||
|
||||
bool get showReadList => widget.readList?.isNotEmpty == true;
|
||||
|
||||
bool get showInChannel =>
|
||||
widget.showInChannelIndicator && widget.message?.showInChannel == true;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var leftPadding = widget.showUserAvatar != DisplayWidget.gone
|
||||
? widget.messageTheme.avatarTheme.constraints.maxWidth + 16.0
|
||||
? widget.messageTheme.avatarTheme.constraints.maxWidth + 14.5
|
||||
: 6.0;
|
||||
|
||||
final hasFiles =
|
||||
@@ -234,140 +252,151 @@ class _MessageWidgetState extends State<MessageWidget> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
Stack(
|
||||
alignment: AlignmentDirectional.bottomStart,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
if (widget.showUserAvatar == DisplayWidget.show)
|
||||
_buildUserAvatar(),
|
||||
SizedBox(
|
||||
width: 6,
|
||||
),
|
||||
if (widget.showUserAvatar == DisplayWidget.hide)
|
||||
SizedBox(
|
||||
width: widget.messageTheme.avatarTheme.constraints
|
||||
.maxWidth +
|
||||
8,
|
||||
),
|
||||
Flexible(
|
||||
child: PortalEntry(
|
||||
portal: Container(
|
||||
transform: Matrix4.translationValues(-16, 2, 0),
|
||||
child: _buildReactionIndicator(context),
|
||||
constraints: BoxConstraints(maxWidth: 22 * 6.0),
|
||||
),
|
||||
portalAnchor: Alignment(-1.0, -1.0),
|
||||
childAnchor: Alignment(1, -1.0),
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Padding(
|
||||
padding: widget.showReactions
|
||||
? EdgeInsets.only(
|
||||
top: widget.message.reactionCounts
|
||||
?.isNotEmpty ==
|
||||
true
|
||||
? 18
|
||||
: 0,
|
||||
)
|
||||
: EdgeInsets.zero,
|
||||
child: (widget.message.isDeleted &&
|
||||
widget.message.status !=
|
||||
MessageSendingStatus
|
||||
.FAILED_DELETE)
|
||||
? Transform(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
if (widget.showUserAvatar == DisplayWidget.show)
|
||||
_buildUserAvatar(),
|
||||
SizedBox(width: 6),
|
||||
if (widget.showUserAvatar == DisplayWidget.hide)
|
||||
SizedBox(
|
||||
width: widget.messageTheme.avatarTheme
|
||||
.constraints.maxWidth +
|
||||
8,
|
||||
),
|
||||
Flexible(
|
||||
child: PortalEntry(
|
||||
portal: Container(
|
||||
transform:
|
||||
Matrix4.translationValues(-16, 2, 0),
|
||||
child: _buildReactionIndicator(context),
|
||||
constraints:
|
||||
BoxConstraints(maxWidth: 22 * 6.0),
|
||||
),
|
||||
portalAnchor: Alignment(-1.0, -1.0),
|
||||
childAnchor: Alignment(1, -1.0),
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Padding(
|
||||
padding: widget.showReactions
|
||||
? EdgeInsets.only(
|
||||
top: widget.message.reactionCounts
|
||||
?.isNotEmpty ==
|
||||
true
|
||||
? 18
|
||||
: 0,
|
||||
)
|
||||
: EdgeInsets.zero,
|
||||
child: (widget.message.isDeleted &&
|
||||
widget.message.status !=
|
||||
MessageSendingStatus
|
||||
.FAILED_DELETE)
|
||||
? Transform(
|
||||
alignment: Alignment.center,
|
||||
transform: Matrix4.rotationY(
|
||||
widget.reverse ? pi : 0),
|
||||
child: DeletedMessage(
|
||||
reverse: widget.reverse,
|
||||
borderRadiusGeometry:
|
||||
widget.borderRadiusGeometry,
|
||||
borderSide: widget.borderSide,
|
||||
shape: widget.shape,
|
||||
messageTheme:
|
||||
widget.messageTheme,
|
||||
),
|
||||
)
|
||||
: Material(
|
||||
clipBehavior: Clip.antiAlias,
|
||||
shape: widget.shape ??
|
||||
RoundedRectangleBorder(
|
||||
side: isOnlyEmoji
|
||||
? BorderSide.none
|
||||
: widget.borderSide ??
|
||||
BorderSide(
|
||||
color: Theme.of(context)
|
||||
.brightness ==
|
||||
Brightness
|
||||
.dark
|
||||
? Colors.white
|
||||
.withAlpha(
|
||||
24)
|
||||
: Colors.black
|
||||
.withAlpha(
|
||||
24),
|
||||
),
|
||||
borderRadius: widget
|
||||
.borderRadiusGeometry ??
|
||||
BorderRadius.zero,
|
||||
),
|
||||
color: _getBackgroundColor(),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(
|
||||
hasFiles ? 2.0 : 0.0),
|
||||
child: Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
mainAxisSize:
|
||||
MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
..._parseAttachments(
|
||||
context),
|
||||
if (widget.message.text
|
||||
.trim()
|
||||
.isNotEmpty &&
|
||||
!isGiphy)
|
||||
_buildTextBubble(context),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (widget.showReactionPickerIndicator)
|
||||
Positioned(
|
||||
right: 0,
|
||||
top: -6,
|
||||
child: Transform(
|
||||
transform: Matrix4.rotationY(
|
||||
widget.reverse ? pi : 0),
|
||||
child: DeletedMessage(
|
||||
reverse: widget.reverse,
|
||||
borderRadiusGeometry:
|
||||
widget.borderRadiusGeometry,
|
||||
borderSide: widget.borderSide,
|
||||
shape: widget.shape,
|
||||
messageTheme: widget.messageTheme,
|
||||
),
|
||||
)
|
||||
: Material(
|
||||
clipBehavior: Clip.antiAlias,
|
||||
shape: widget.shape ??
|
||||
RoundedRectangleBorder(
|
||||
side: isOnlyEmoji
|
||||
? BorderSide.none
|
||||
: widget.borderSide ??
|
||||
BorderSide(
|
||||
color: Theme.of(context)
|
||||
.brightness ==
|
||||
Brightness
|
||||
.dark
|
||||
? Colors.white
|
||||
.withAlpha(24)
|
||||
: Colors.black
|
||||
.withAlpha(
|
||||
24),
|
||||
),
|
||||
borderRadius: widget
|
||||
.borderRadiusGeometry ??
|
||||
BorderRadius.zero,
|
||||
),
|
||||
color: _getBackgroundColor(),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(
|
||||
hasFiles ? 2.0 : 0.0),
|
||||
child: Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
..._parseAttachments(context),
|
||||
if (widget.message.text
|
||||
.trim()
|
||||
.isNotEmpty &&
|
||||
!isGiphy)
|
||||
_buildTextBubble(context),
|
||||
],
|
||||
child: CustomPaint(
|
||||
painter: ReactionBubblePainter(
|
||||
widget.messageTheme
|
||||
.reactionsBackgroundColor,
|
||||
widget.messageTheme
|
||||
.reactionsBorderColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (widget.showReactionPickerIndicator)
|
||||
Positioned(
|
||||
right: 0,
|
||||
top: -6,
|
||||
child: Transform(
|
||||
transform: Matrix4.rotationY(
|
||||
widget.reverse ? pi : 0),
|
||||
child: CustomPaint(
|
||||
painter: ReactionBubblePainter(
|
||||
widget.messageTheme
|
||||
.reactionsBackgroundColor,
|
||||
widget.messageTheme
|
||||
.reactionsBorderColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (showThreadReplyIndicator ||
|
||||
showUsername ||
|
||||
showTimeStamp ||
|
||||
showInChannel)
|
||||
SizedBox(height: 20.0),
|
||||
],
|
||||
),
|
||||
if (widget.showReplyIndicator &&
|
||||
widget.message.replyCount > 0)
|
||||
_buildReplyIndicator(leftPadding),
|
||||
if (showThreadReplyIndicator ||
|
||||
showUsername ||
|
||||
showTimeStamp ||
|
||||
showInChannel)
|
||||
_buildBottomRow(leftPadding)
|
||||
],
|
||||
),
|
||||
if ((widget.message.createdAt != null &&
|
||||
widget.showTimestamp) ||
|
||||
widget.showUsername ||
|
||||
widget.readList?.isNotEmpty == true)
|
||||
_buildBottomRow(leftPadding),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -376,6 +405,119 @@ class _MessageWidgetState extends State<MessageWidget> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBottomRow(double leftPadding) {
|
||||
final deleted = widget.message.isDeleted;
|
||||
var children = <Widget>[];
|
||||
if (deleted) {
|
||||
children.add(
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
StreamSvgIcon.eye(
|
||||
color: Colors.black.withOpacity(0.5),
|
||||
size: 16.0,
|
||||
),
|
||||
SizedBox(width: 8.0),
|
||||
Text(
|
||||
'Only visible to you',
|
||||
style: TextStyle(
|
||||
color: Colors.black.withOpacity(0.5),
|
||||
fontSize: 12.0,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
} else if (showInChannel) {
|
||||
final onThreadTap = () async {
|
||||
try {
|
||||
final channel = StreamChannel.of(context);
|
||||
final message = await channel.getMessage(widget.message.parentId);
|
||||
return widget.onThreadTap(message);
|
||||
} catch (e, stk) {
|
||||
print(e);
|
||||
print(stk);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
children.add(
|
||||
InkWell(
|
||||
onTap: widget.onThreadTap != null ? onThreadTap : null,
|
||||
child: Text('Thread Reply', style: widget.messageTheme?.replies),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
final showSendingIndicator =
|
||||
widget.showSendingIndicator == DisplayWidget.show;
|
||||
final replyCount = widget.message.replyCount;
|
||||
final msg = replyCount != 0
|
||||
? '$replyCount ${replyCount > 1 ? 'Thread Replies' : 'Thread Reply'}'
|
||||
: 'Thread Reply';
|
||||
|
||||
final onThreadTap = () async {
|
||||
var message = widget.message;
|
||||
return widget.onThreadTap(message);
|
||||
};
|
||||
|
||||
children.addAll([
|
||||
if (showSendingIndicator) _buildSendingIndicator(),
|
||||
if (showReadList)
|
||||
SizedBox.fromSize(
|
||||
size: Size((widget.readList.length * 10.0) + 10, 17),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 4.0),
|
||||
child: _buildReadIndicator(),
|
||||
),
|
||||
),
|
||||
if (showThreadReplyIndicator)
|
||||
InkWell(
|
||||
onTap: widget.onThreadTap != null ? onThreadTap : null,
|
||||
child: Text(msg, style: widget.messageTheme?.replies),
|
||||
),
|
||||
if (showUsername)
|
||||
Text(
|
||||
widget.message.user.name,
|
||||
style: widget.messageTheme.replies.copyWith(
|
||||
color: widget.messageTheme.createdAt.color,
|
||||
),
|
||||
),
|
||||
if (showTimeStamp)
|
||||
Text(
|
||||
Jiffy(widget.message.createdAt.toLocal()).jm,
|
||||
style: widget.messageTheme.createdAt,
|
||||
),
|
||||
]);
|
||||
}
|
||||
if (widget.reverse) children = children.reversed.toList();
|
||||
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(left: leftPadding),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
if (!deleted && (showThreadReplyIndicator || showInChannel))
|
||||
Container(
|
||||
margin: EdgeInsets.only(
|
||||
bottom: widget.messageTheme.replies.fontSize / 2),
|
||||
child: CustomPaint(
|
||||
size: const Size(16, 32),
|
||||
painter: _ThreadReplyPainter(
|
||||
color: widget.messageTheme.replyThreadColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
...children.map(
|
||||
(child) => Transform(
|
||||
transform: Matrix4.rotationY(widget.reverse ? pi : 0),
|
||||
alignment: Alignment.center,
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
].insertBetween(const SizedBox(width: 8.0)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildUrlAttachment() {
|
||||
var urlAttachment = widget.message.attachments
|
||||
.firstWhere((element) => element.ogScrapeUrl != null);
|
||||
@@ -394,87 +536,6 @@ class _MessageWidgetState extends State<MessageWidget> {
|
||||
);
|
||||
}
|
||||
|
||||
Padding _buildBottomRow(double leftPadding) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: leftPadding,
|
||||
top: 2,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Transform(
|
||||
alignment: Alignment.center,
|
||||
transform: Matrix4.rotationY(widget.reverse ? pi : 0),
|
||||
child: RichText(
|
||||
text: TextSpan(
|
||||
style: widget.messageTheme.createdAt,
|
||||
children: <TextSpan>[
|
||||
if (widget.showUsername)
|
||||
TextSpan(
|
||||
text: widget.message.user.name,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: widget.messageTheme.createdAt.color
|
||||
.withOpacity(1)),
|
||||
),
|
||||
if (widget.message.createdAt != null && widget.showTimestamp)
|
||||
TextSpan(
|
||||
text: Jiffy(widget.message.createdAt.toLocal())
|
||||
.format(' HH:mm'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (widget.showSendingIndicator == DisplayWidget.show)
|
||||
_buildSendingIndicator(),
|
||||
if (widget.readList?.isNotEmpty == true)
|
||||
SizedBox.fromSize(
|
||||
size: Size((widget.readList.length * 10.0) + 10, 17),
|
||||
child: Transform(
|
||||
alignment: Alignment.center,
|
||||
transform: Matrix4.rotationY(widget.reverse ? pi : 0),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 4.0),
|
||||
child: _buildReadIndicator(),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (widget.message.isDeleted)
|
||||
Transform(
|
||||
transform: Matrix4.rotationY(widget.reverse ? pi : 0),
|
||||
alignment: Alignment.center,
|
||||
child: Padding(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
StreamSvgIcon.eye(
|
||||
color: Colors.black.withOpacity(0.5),
|
||||
size: 16.0,
|
||||
),
|
||||
SizedBox(
|
||||
width: 8.0,
|
||||
),
|
||||
Text(
|
||||
'Only visible to you',
|
||||
style: TextStyle(
|
||||
color: Colors.black.withOpacity(0.5),
|
||||
fontSize: 12.0,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
bool get isGiphy =>
|
||||
widget.message.attachments?.any((element) => element.type == 'giphy') ==
|
||||
true;
|
||||
@@ -565,7 +626,7 @@ class _MessageWidgetState extends State<MessageWidget> {
|
||||
true,
|
||||
showReactions: widget.showReactions,
|
||||
showReply:
|
||||
widget.showReplyIndicator && widget.onThreadTap != null,
|
||||
widget.showThreadReplyIndicator && widget.onThreadTap != null,
|
||||
),
|
||||
);
|
||||
});
|
||||
@@ -715,38 +776,13 @@ class _MessageWidgetState extends State<MessageWidget> {
|
||||
return;
|
||||
}
|
||||
|
||||
Widget _buildReplyIndicator(double leftPadding) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: leftPadding,
|
||||
),
|
||||
child: Transform(
|
||||
transform: Matrix4.rotationY(widget.reverse ? pi : 0),
|
||||
alignment: Alignment.center,
|
||||
child: ReplyIndicator(
|
||||
message: widget.message,
|
||||
reversed: widget.reverse,
|
||||
messageTheme: widget.messageTheme,
|
||||
onTap: widget.onThreadTap != null
|
||||
? () {
|
||||
widget.onThreadTap(widget.message);
|
||||
}
|
||||
: null,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSendingIndicator() {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(right: 4.0),
|
||||
child: Transform(
|
||||
transform: Matrix4.rotationY(widget.reverse ? pi : 0),
|
||||
alignment: Alignment.center,
|
||||
child: SendingIndicator(
|
||||
message: widget.message,
|
||||
allRead: widget.allRead,
|
||||
),
|
||||
return Container(
|
||||
height: widget.messageTheme.createdAt.fontSize + 2,
|
||||
width: widget.messageTheme.createdAt.fontSize + 2,
|
||||
child: SendingIndicator(
|
||||
message: widget.message,
|
||||
allRead: widget.allRead,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -958,3 +994,32 @@ class _MessageWidgetState extends State<MessageWidget> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _ThreadReplyPainter extends CustomPainter {
|
||||
final Color color;
|
||||
|
||||
const _ThreadReplyPainter({@required this.color});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final paint = Paint()
|
||||
..color = color ?? Color(0XFFDBDBDB)
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 1
|
||||
..strokeCap = StrokeCap.round;
|
||||
|
||||
final path = Path()
|
||||
..moveTo(0, 0)
|
||||
..quadraticBezierTo(0, size.height * 0.38, 0, size.height * 0.50)
|
||||
..quadraticBezierTo(
|
||||
0,
|
||||
size.height,
|
||||
size.width,
|
||||
size.height,
|
||||
);
|
||||
canvas.drawPath(path, paint);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
|
||||
}
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat/stream_chat.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
/// A reply button indicator
|
||||
class ReplyIndicator extends StatelessWidget {
|
||||
final Message message;
|
||||
final VoidCallback onTap;
|
||||
final bool reversed;
|
||||
final MessageTheme messageTheme;
|
||||
|
||||
const ReplyIndicator({
|
||||
Key key,
|
||||
this.message,
|
||||
this.onTap,
|
||||
this.reversed = false,
|
||||
this.messageTheme,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var row = [
|
||||
Text(
|
||||
'Replies: ${message.replyCount}',
|
||||
style: messageTheme?.replies,
|
||||
),
|
||||
Transform(
|
||||
transform: Matrix4.rotationY(reversed ? 0 : pi),
|
||||
alignment: Alignment.center,
|
||||
child: Icon(
|
||||
Icons.subdirectory_arrow_left,
|
||||
color: Theme.of(context).brightness == Brightness.dark
|
||||
? Colors.white12
|
||||
: Colors.black12,
|
||||
),
|
||||
),
|
||||
];
|
||||
|
||||
if (!reversed) {
|
||||
row = row.reversed.toList();
|
||||
}
|
||||
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 2.0),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: row,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+26
-12
@@ -143,23 +143,24 @@ class StreamChannelState extends State<StreamChannel> {
|
||||
if (_topPaginationEnded || _queryTopMessagesController.value) return;
|
||||
_queryTopMessagesController.add(true);
|
||||
|
||||
if (!channel.state.threads.containsKey(parentId)) {
|
||||
return _queryTopMessagesController.add(false);
|
||||
Message message;
|
||||
if (channel.state.threads.containsKey(parentId)) {
|
||||
final thread = channel.state.threads[parentId];
|
||||
if (thread.isNotEmpty) {
|
||||
message = thread.first;
|
||||
}
|
||||
}
|
||||
|
||||
final thread = channel.state.threads[parentId];
|
||||
|
||||
if (thread.isEmpty) return _queryTopMessagesController.add(false);
|
||||
|
||||
final message = thread.first;
|
||||
|
||||
try {
|
||||
final state = await queryBeforeMessage(
|
||||
message.id,
|
||||
limit: limit,
|
||||
final response = await channel.getReplies(
|
||||
parentId,
|
||||
PaginationParams(
|
||||
lessThan: message?.id,
|
||||
limit: limit,
|
||||
),
|
||||
preferOffline: preferOffline,
|
||||
);
|
||||
if (state.messages.isEmpty || state.messages.length < limit) {
|
||||
if (response.messages.isEmpty || response.messages.length < limit) {
|
||||
_topPaginationEnded = true;
|
||||
}
|
||||
_queryTopMessagesController.add(false);
|
||||
@@ -267,6 +268,19 @@ class StreamChannelState extends State<StreamChannel> {
|
||||
return state;
|
||||
}
|
||||
|
||||
///
|
||||
Future<Message> getMessage(String messageId) async {
|
||||
var message = channel.state.messages.firstWhere(
|
||||
(it) => it.id == messageId,
|
||||
orElse: () => null,
|
||||
);
|
||||
if (message == null) {
|
||||
final response = await channel.getMessagesById([messageId]);
|
||||
message = response.messages.first;
|
||||
}
|
||||
return message;
|
||||
}
|
||||
|
||||
/// Reloads the channel with latest message
|
||||
Future<void> reloadChannel() => queryAtMessage(before: 30);
|
||||
|
||||
|
||||
@@ -191,6 +191,8 @@ class StreamChatThemeData {
|
||||
this.ownMessageTheme.messageBackgroundColor,
|
||||
avatarTheme: ownMessageTheme?.avatarTheme ??
|
||||
this.ownMessageTheme.avatarTheme,
|
||||
replyThreadColor: ownMessageTheme?.replyThreadColor ??
|
||||
this.ownMessageTheme.replyThreadColor,
|
||||
) ??
|
||||
this.ownMessageTheme,
|
||||
otherMessageTheme: otherMessageTheme?.copyWith(
|
||||
@@ -209,6 +211,8 @@ class StreamChatThemeData {
|
||||
this.otherMessageTheme.messageBackgroundColor,
|
||||
avatarTheme: otherMessageTheme?.avatarTheme ??
|
||||
this.otherMessageTheme.avatarTheme,
|
||||
replyThreadColor: ownMessageTheme?.replyThreadColor ??
|
||||
this.ownMessageTheme.replyThreadColor,
|
||||
) ??
|
||||
this.otherMessageTheme,
|
||||
reactionIcons: reactionIcons ?? this.reactionIcons,
|
||||
@@ -297,16 +301,17 @@ class StreamChatThemeData {
|
||||
color: isDark
|
||||
? Colors.white.withOpacity(.5)
|
||||
: Colors.black.withOpacity(.5),
|
||||
fontSize: 11,
|
||||
fontSize: 12,
|
||||
),
|
||||
replies: TextStyle(
|
||||
color: accentColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 12,
|
||||
),
|
||||
messageBackgroundColor: isDark ? Color(0xff191919) : Color(0xffEAEAEA),
|
||||
reactionsBackgroundColor: isDark ? Colors.black : Colors.white,
|
||||
reactionsBorderColor: isDark ? Color(0xff191919) : Color(0xffEAEAEA),
|
||||
replyThreadColor: isDark ? Color(0xff191919) : Color(0xffEAEAEA),
|
||||
avatarTheme: AvatarTheme(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
constraints: BoxConstraints.tightFor(
|
||||
@@ -330,17 +335,19 @@ class StreamChatThemeData {
|
||||
color: isDark
|
||||
? Colors.white.withOpacity(.5)
|
||||
: Colors.black.withOpacity(.5),
|
||||
fontSize: 11,
|
||||
fontSize: 12,
|
||||
),
|
||||
replies: TextStyle(
|
||||
color: accentColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 12,
|
||||
),
|
||||
messageLinks: TextStyle(
|
||||
color: accentColor,
|
||||
),
|
||||
messageBackgroundColor: isDark ? Colors.black : Colors.white,
|
||||
replyThreadColor:
|
||||
isDark ? Colors.white.withAlpha(24) : Colors.black.withAlpha(24),
|
||||
avatarTheme: AvatarTheme(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
constraints: BoxConstraints.tightFor(
|
||||
@@ -455,6 +462,7 @@ class MessageTheme {
|
||||
final Color messageBackgroundColor;
|
||||
final Color reactionsBackgroundColor;
|
||||
final Color reactionsBorderColor;
|
||||
final Color replyThreadColor;
|
||||
final AvatarTheme avatarTheme;
|
||||
|
||||
const MessageTheme({
|
||||
@@ -465,6 +473,7 @@ class MessageTheme {
|
||||
this.messageBackgroundColor,
|
||||
this.reactionsBackgroundColor,
|
||||
this.reactionsBorderColor,
|
||||
this.replyThreadColor,
|
||||
this.avatarTheme,
|
||||
this.createdAt,
|
||||
});
|
||||
@@ -479,6 +488,7 @@ class MessageTheme {
|
||||
AvatarTheme avatarTheme,
|
||||
Color reactionsBackgroundColor,
|
||||
Color reactionsBorderColor,
|
||||
Color replyThreadColor,
|
||||
}) =>
|
||||
MessageTheme(
|
||||
messageText: messageText ?? this.messageText,
|
||||
@@ -492,6 +502,7 @@ class MessageTheme {
|
||||
reactionsBackgroundColor:
|
||||
reactionsBackgroundColor ?? this.reactionsBackgroundColor,
|
||||
reactionsBorderColor: reactionsBorderColor ?? this.reactionsBorderColor,
|
||||
replyThreadColor: replyThreadColor ?? this.replyThreadColor,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+45
-36
@@ -1,7 +1,9 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat/stream_chat.dart';
|
||||
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
|
||||
import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
|
||||
|
||||
import 'back_button.dart';
|
||||
import 'channel_name.dart';
|
||||
|
||||
/// 
|
||||
/// 
|
||||
@@ -77,43 +79,50 @@ class ThreadHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||
return AppBar(
|
||||
automaticallyImplyLeading: false,
|
||||
elevation: 1,
|
||||
leading: showBackButton
|
||||
? StreamBackButton(
|
||||
onPressed: onBackPressed,
|
||||
showUnreads: true,
|
||||
)
|
||||
: SizedBox(),
|
||||
backgroundColor:
|
||||
StreamChatTheme.of(context).channelTheme.channelHeaderTheme.color,
|
||||
actions: <Widget>[
|
||||
Container(
|
||||
child: showBackButton
|
||||
? AspectRatio(
|
||||
aspectRatio: 1,
|
||||
child: IconButton(
|
||||
onPressed: onBackPressed ?? () => Navigator.pop(context),
|
||||
icon: StreamSvgIcon.close(
|
||||
size: 24,
|
||||
color: Theme.of(context).brightness == Brightness.dark
|
||||
? Colors.white
|
||||
: Colors.black,
|
||||
),
|
||||
),
|
||||
)
|
||||
: SizedBox(),
|
||||
),
|
||||
],
|
||||
centerTitle: false,
|
||||
title: Text.rich(
|
||||
TextSpan(
|
||||
text: 'Thread',
|
||||
children: [
|
||||
TextSpan(
|
||||
text:
|
||||
' ${parent.replyCount} ${parent.replyCount == 1 ? 'reply' : 'replies'}',
|
||||
style: StreamChatTheme.of(context)
|
||||
.channelTheme
|
||||
.channelHeaderTheme
|
||||
.lastMessageAt,
|
||||
),
|
||||
],
|
||||
),
|
||||
style:
|
||||
StreamChatTheme.of(context).channelTheme.channelHeaderTheme.title,
|
||||
centerTitle: true,
|
||||
title: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
'Thread Reply',
|
||||
style: StreamChatTheme.of(context)
|
||||
.channelTheme
|
||||
.channelHeaderTheme
|
||||
.title,
|
||||
),
|
||||
SizedBox(height: 2),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
'with ',
|
||||
style: StreamChatTheme.of(context)
|
||||
.channelTheme
|
||||
.channelHeaderTheme
|
||||
.lastMessageAt,
|
||||
),
|
||||
Flexible(
|
||||
child: ChannelName(
|
||||
textStyle: StreamChatTheme.of(context)
|
||||
.channelTheme
|
||||
.channelHeaderTheme
|
||||
.lastMessageAt,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -92,3 +92,12 @@ Future<bool> showConfirmationDialog(
|
||||
/// Get random png with initials
|
||||
String getRandomPicUrl(User user) =>
|
||||
'https://getstream.io/random_png/?id=${user.id}&name=${user.name}';
|
||||
|
||||
/// List extension
|
||||
extension ListX<T> on List<T> {
|
||||
/// Insert any item<T> inBetween the list items
|
||||
List<T> insertBetween(T item) => expand((e) sync* {
|
||||
yield item;
|
||||
yield e;
|
||||
}).skip(1).toList(growable: false);
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@ export 'src/message_list_view.dart';
|
||||
export 'src/message_text.dart';
|
||||
export 'src/message_widget.dart';
|
||||
export 'src/reaction_picker.dart';
|
||||
export 'src/reply_indicator.dart';
|
||||
export 'src/sending_indicator.dart';
|
||||
export 'src/stream_channel.dart';
|
||||
export 'src/stream_chat.dart';
|
||||
|
||||
Reference in New Issue
Block a user