@@ -470,6 +470,7 @@ class ThreadPage extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
|
backgroundColor: Color.fromRGBO(252, 252, 252, 1),
|
||||||
appBar: ThreadHeader(
|
appBar: ThreadHeader(
|
||||||
parent: parent,
|
parent: parent,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ class MessageActionsModal extends StatelessWidget {
|
|||||||
messageTheme: messageTheme,
|
messageTheme: messageTheme,
|
||||||
showReactions: false,
|
showReactions: false,
|
||||||
showUsername: false,
|
showUsername: false,
|
||||||
showReplyIndicator: false,
|
showThreadReplyIndicator: false,
|
||||||
showUserAvatar: showUserAvatar,
|
showUserAvatar: showUserAvatar,
|
||||||
showTimestamp: false,
|
showTimestamp: false,
|
||||||
translateUserAvatar: false,
|
translateUserAvatar: false,
|
||||||
|
|||||||
@@ -241,7 +241,7 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
if (!snapshot.hasData) {
|
if (!snapshot.hasData) {
|
||||||
return Center(
|
return Center(
|
||||||
child: CircularProgressIndicator(),
|
child: const CircularProgressIndicator(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -336,18 +336,25 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
buildParentMessage(widget.parentMessage),
|
buildParentMessage(widget.parentMessage),
|
||||||
Padding(
|
Container(
|
||||||
padding:
|
decoration: BoxDecoration(
|
||||||
const EdgeInsets.symmetric(horizontal: 32),
|
gradient: LinearGradient(
|
||||||
child: Container(
|
colors: [
|
||||||
padding: const EdgeInsets.all(8),
|
Color(0XFFF7F7F7),
|
||||||
|
Color(0XFFFCFCFC),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
child: Text(
|
child: Text(
|
||||||
'Start of thread',
|
'${widget.parentMessage.replyCount} ${widget.parentMessage.replyCount == 1 ? 'Reply' : 'Replies'}',
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
|
style: StreamChatTheme.of(context)
|
||||||
|
.channelTheme
|
||||||
|
.channelHeaderTheme
|
||||||
|
.lastMessageAt,
|
||||||
),
|
),
|
||||||
color: Theme.of(context)
|
|
||||||
.accentColor
|
|
||||||
.withAlpha(50),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -585,7 +592,7 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
if (!snapshot.data) {
|
if (!snapshot.data) {
|
||||||
if (direction == QueryDirection.top) {
|
if (direction == QueryDirection.top) {
|
||||||
return Container(
|
return Container(
|
||||||
height: 50,
|
height: 52,
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -679,7 +686,8 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
final isMyMessage = message.user.id == StreamChat.of(context).user.id;
|
final isMyMessage = message.user.id == StreamChat.of(context).user.id;
|
||||||
|
|
||||||
return MessageWidget(
|
return MessageWidget(
|
||||||
showReplyIndicator: false,
|
showThreadReplyIndicator: false,
|
||||||
|
showInChannelIndicator: false,
|
||||||
message: message,
|
message: message,
|
||||||
reverse: isMyMessage,
|
reverse: isMyMessage,
|
||||||
showUsername: !isMyMessage,
|
showUsername: !isMyMessage,
|
||||||
@@ -748,6 +756,8 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
bottom: index == 0 ? 30 : (isNextUser ? 2 : 7),
|
bottom: index == 0 ? 30 : (isNextUser ? 2 : 7),
|
||||||
top: 3,
|
top: 3,
|
||||||
),
|
),
|
||||||
|
showInChannelIndicator: widget.parentMessage == null,
|
||||||
|
showThreadReplyIndicator: widget.parentMessage == null,
|
||||||
showUsername: !isMyMessage && !isNextUser,
|
showUsername: !isMyMessage && !isNextUser,
|
||||||
showSendingIndicator: isMyMessage &&
|
showSendingIndicator: isMyMessage &&
|
||||||
(index == 0 || message.status != MessageSendingStatus.SENT)
|
(index == 0 || message.status != MessageSendingStatus.SENT)
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ class MessageReactionsModal extends StatelessWidget {
|
|||||||
showReactions: false,
|
showReactions: false,
|
||||||
showUsername: false,
|
showUsername: false,
|
||||||
showUserAvatar: showUserAvatar,
|
showUserAvatar: showUserAvatar,
|
||||||
showReplyIndicator: false,
|
showThreadReplyIndicator: false,
|
||||||
showTimestamp: false,
|
showTimestamp: false,
|
||||||
translateUserAvatar: false,
|
translateUserAvatar: false,
|
||||||
showSendingIndicator: DisplayWidget.gone,
|
showSendingIndicator: DisplayWidget.gone,
|
||||||
|
|||||||
+297
-238
@@ -99,8 +99,11 @@ class MessageWidget extends StatefulWidget {
|
|||||||
|
|
||||||
final bool allRead;
|
final bool allRead;
|
||||||
|
|
||||||
/// If true the widget will show the reply indicator
|
/// If true the widget will show the thread reply indicator
|
||||||
final bool showReplyIndicator;
|
final bool showThreadReplyIndicator;
|
||||||
|
|
||||||
|
/// If true the widget will show the show in channel indicator
|
||||||
|
final bool showInChannelIndicator;
|
||||||
|
|
||||||
/// The function called when tapping on UserAvatar
|
/// The function called when tapping on UserAvatar
|
||||||
final void Function(User) onUserAvatarTap;
|
final void Function(User) onUserAvatarTap;
|
||||||
@@ -123,6 +126,7 @@ class MessageWidget extends StatefulWidget {
|
|||||||
/// Center user avatar with bottom of the message
|
/// Center user avatar with bottom of the message
|
||||||
final bool translateUserAvatar;
|
final bool translateUserAvatar;
|
||||||
|
|
||||||
|
///
|
||||||
MessageWidget({
|
MessageWidget({
|
||||||
Key key,
|
Key key,
|
||||||
@required this.message,
|
@required this.message,
|
||||||
@@ -139,7 +143,8 @@ class MessageWidget extends StatefulWidget {
|
|||||||
this.showReactionPickerIndicator = false,
|
this.showReactionPickerIndicator = false,
|
||||||
this.showUserAvatar = DisplayWidget.show,
|
this.showUserAvatar = DisplayWidget.show,
|
||||||
this.showSendingIndicator = DisplayWidget.show,
|
this.showSendingIndicator = DisplayWidget.show,
|
||||||
this.showReplyIndicator = true,
|
this.showThreadReplyIndicator = true,
|
||||||
|
this.showInChannelIndicator = true,
|
||||||
this.onThreadTap,
|
this.onThreadTap,
|
||||||
this.showUsername = true,
|
this.showUsername = true,
|
||||||
this.showTimestamp = true,
|
this.showTimestamp = true,
|
||||||
@@ -211,10 +216,23 @@ class MessageWidget extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _MessageWidgetState extends State<MessageWidget> {
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
var leftPadding = widget.showUserAvatar != DisplayWidget.gone
|
var leftPadding = widget.showUserAvatar != DisplayWidget.gone
|
||||||
? widget.messageTheme.avatarTheme.constraints.maxWidth + 16.0
|
? widget.messageTheme.avatarTheme.constraints.maxWidth + 14.5
|
||||||
: 6.0;
|
: 6.0;
|
||||||
|
|
||||||
final hasFiles =
|
final hasFiles =
|
||||||
@@ -234,140 +252,151 @@ class _MessageWidgetState extends State<MessageWidget> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Column(
|
Stack(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
alignment: AlignmentDirectional.bottomStart,
|
||||||
mainAxisSize: MainAxisSize.min,
|
children: [
|
||||||
children: <Widget>[
|
Column(
|
||||||
Row(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: <Widget>[
|
children: [
|
||||||
if (widget.showUserAvatar == DisplayWidget.show)
|
Row(
|
||||||
_buildUserAvatar(),
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
SizedBox(
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
width: 6,
|
mainAxisSize: MainAxisSize.min,
|
||||||
),
|
children: <Widget>[
|
||||||
if (widget.showUserAvatar == DisplayWidget.hide)
|
if (widget.showUserAvatar == DisplayWidget.show)
|
||||||
SizedBox(
|
_buildUserAvatar(),
|
||||||
width: widget.messageTheme.avatarTheme.constraints
|
SizedBox(width: 6),
|
||||||
.maxWidth +
|
if (widget.showUserAvatar == DisplayWidget.hide)
|
||||||
8,
|
SizedBox(
|
||||||
),
|
width: widget.messageTheme.avatarTheme
|
||||||
Flexible(
|
.constraints.maxWidth +
|
||||||
child: PortalEntry(
|
8,
|
||||||
portal: Container(
|
),
|
||||||
transform: Matrix4.translationValues(-16, 2, 0),
|
Flexible(
|
||||||
child: _buildReactionIndicator(context),
|
child: PortalEntry(
|
||||||
constraints: BoxConstraints(maxWidth: 22 * 6.0),
|
portal: Container(
|
||||||
),
|
transform:
|
||||||
portalAnchor: Alignment(-1.0, -1.0),
|
Matrix4.translationValues(-16, 2, 0),
|
||||||
childAnchor: Alignment(1, -1.0),
|
child: _buildReactionIndicator(context),
|
||||||
child: Stack(
|
constraints:
|
||||||
clipBehavior: Clip.none,
|
BoxConstraints(maxWidth: 22 * 6.0),
|
||||||
children: [
|
),
|
||||||
Padding(
|
portalAnchor: Alignment(-1.0, -1.0),
|
||||||
padding: widget.showReactions
|
childAnchor: Alignment(1, -1.0),
|
||||||
? EdgeInsets.only(
|
child: Stack(
|
||||||
top: widget.message.reactionCounts
|
clipBehavior: Clip.none,
|
||||||
?.isNotEmpty ==
|
children: [
|
||||||
true
|
Padding(
|
||||||
? 18
|
padding: widget.showReactions
|
||||||
: 0,
|
? EdgeInsets.only(
|
||||||
)
|
top: widget.message.reactionCounts
|
||||||
: EdgeInsets.zero,
|
?.isNotEmpty ==
|
||||||
child: (widget.message.isDeleted &&
|
true
|
||||||
widget.message.status !=
|
? 18
|
||||||
MessageSendingStatus
|
: 0,
|
||||||
.FAILED_DELETE)
|
)
|
||||||
? Transform(
|
: EdgeInsets.zero,
|
||||||
alignment: Alignment.center,
|
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(
|
transform: Matrix4.rotationY(
|
||||||
widget.reverse ? pi : 0),
|
widget.reverse ? pi : 0),
|
||||||
child: DeletedMessage(
|
child: CustomPaint(
|
||||||
reverse: widget.reverse,
|
painter: ReactionBubblePainter(
|
||||||
borderRadiusGeometry:
|
widget.messageTheme
|
||||||
widget.borderRadiusGeometry,
|
.reactionsBackgroundColor,
|
||||||
borderSide: widget.borderSide,
|
widget.messageTheme
|
||||||
shape: widget.shape,
|
.reactionsBorderColor,
|
||||||
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: CustomPaint(
|
|
||||||
painter: ReactionBubblePainter(
|
|
||||||
widget.messageTheme
|
|
||||||
.reactionsBackgroundColor,
|
|
||||||
widget.messageTheme
|
|
||||||
.reactionsBorderColor,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
),
|
),
|
||||||
|
if (showThreadReplyIndicator ||
|
||||||
|
showUsername ||
|
||||||
|
showTimeStamp ||
|
||||||
|
showInChannel)
|
||||||
|
SizedBox(height: 20.0),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
if (widget.showReplyIndicator &&
|
if (showThreadReplyIndicator ||
|
||||||
widget.message.replyCount > 0)
|
showUsername ||
|
||||||
_buildReplyIndicator(leftPadding),
|
showTimeStamp ||
|
||||||
|
showInChannel)
|
||||||
|
_buildBottomRows(leftPadding)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
if ((widget.message.createdAt != null &&
|
|
||||||
widget.showTimestamp) ||
|
|
||||||
widget.showUsername ||
|
|
||||||
widget.readList?.isNotEmpty == true)
|
|
||||||
_buildBottomRow(leftPadding),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -376,6 +405,113 @@ class _MessageWidgetState extends State<MessageWidget> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildBottomRows(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 {
|
||||||
|
final showSendingIndicator =
|
||||||
|
widget.showSendingIndicator == DisplayWidget.show;
|
||||||
|
final replyCount = widget.message.replyCount;
|
||||||
|
final msg = showInChannel
|
||||||
|
? 'Thread Reply'
|
||||||
|
: replyCount != 0
|
||||||
|
? '$replyCount ${replyCount > 1 ? 'Thread Replies' : 'Thread Reply'}'
|
||||||
|
: 'Thread Reply';
|
||||||
|
|
||||||
|
final onThreadTap = () async {
|
||||||
|
try {
|
||||||
|
var message = widget.message;
|
||||||
|
if (showInChannel && message.parentId != null) {
|
||||||
|
final channel = StreamChannel.of(context);
|
||||||
|
message = await channel.getMessage(widget.message.parentId);
|
||||||
|
}
|
||||||
|
return widget.onThreadTap(message);
|
||||||
|
} catch (e, stk) {
|
||||||
|
print(e);
|
||||||
|
print(stk);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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 || showInChannel)
|
||||||
|
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() {
|
Widget _buildUrlAttachment() {
|
||||||
var urlAttachment = widget.message.attachments
|
var urlAttachment = widget.message.attachments
|
||||||
.firstWhere((element) => element.ogScrapeUrl != null);
|
.firstWhere((element) => element.ogScrapeUrl != null);
|
||||||
@@ -394,87 +530,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 =>
|
bool get isGiphy =>
|
||||||
widget.message.attachments?.any((element) => element.type == 'giphy') ==
|
widget.message.attachments?.any((element) => element.type == 'giphy') ==
|
||||||
true;
|
true;
|
||||||
@@ -565,7 +620,7 @@ class _MessageWidgetState extends State<MessageWidget> {
|
|||||||
true,
|
true,
|
||||||
showReactions: widget.showReactions,
|
showReactions: widget.showReactions,
|
||||||
showReply:
|
showReply:
|
||||||
widget.showReplyIndicator && widget.onThreadTap != null,
|
widget.showThreadReplyIndicator && widget.onThreadTap != null,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -715,38 +770,13 @@ class _MessageWidgetState extends State<MessageWidget> {
|
|||||||
return;
|
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() {
|
Widget _buildSendingIndicator() {
|
||||||
return Padding(
|
return Container(
|
||||||
padding: const EdgeInsets.only(right: 4.0),
|
height: widget.messageTheme.createdAt.fontSize + 2,
|
||||||
child: Transform(
|
width: widget.messageTheme.createdAt.fontSize + 2,
|
||||||
transform: Matrix4.rotationY(widget.reverse ? pi : 0),
|
child: SendingIndicator(
|
||||||
alignment: Alignment.center,
|
message: widget.message,
|
||||||
child: SendingIndicator(
|
allRead: widget.allRead,
|
||||||
message: widget.message,
|
|
||||||
allRead: widget.allRead,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -958,3 +988,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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -267,6 +267,19 @@ class StreamChannelState extends State<StreamChannel> {
|
|||||||
return state;
|
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
|
/// Reloads the channel with latest message
|
||||||
Future<void> reloadChannel() => queryAtMessage(before: 30);
|
Future<void> reloadChannel() => queryAtMessage(before: 30);
|
||||||
|
|
||||||
|
|||||||
@@ -191,6 +191,8 @@ class StreamChatThemeData {
|
|||||||
this.ownMessageTheme.messageBackgroundColor,
|
this.ownMessageTheme.messageBackgroundColor,
|
||||||
avatarTheme: ownMessageTheme?.avatarTheme ??
|
avatarTheme: ownMessageTheme?.avatarTheme ??
|
||||||
this.ownMessageTheme.avatarTheme,
|
this.ownMessageTheme.avatarTheme,
|
||||||
|
replyThreadColor: ownMessageTheme?.replyThreadColor ??
|
||||||
|
this.ownMessageTheme.replyThreadColor,
|
||||||
) ??
|
) ??
|
||||||
this.ownMessageTheme,
|
this.ownMessageTheme,
|
||||||
otherMessageTheme: otherMessageTheme?.copyWith(
|
otherMessageTheme: otherMessageTheme?.copyWith(
|
||||||
@@ -209,6 +211,8 @@ class StreamChatThemeData {
|
|||||||
this.otherMessageTheme.messageBackgroundColor,
|
this.otherMessageTheme.messageBackgroundColor,
|
||||||
avatarTheme: otherMessageTheme?.avatarTheme ??
|
avatarTheme: otherMessageTheme?.avatarTheme ??
|
||||||
this.otherMessageTheme.avatarTheme,
|
this.otherMessageTheme.avatarTheme,
|
||||||
|
replyThreadColor: ownMessageTheme?.replyThreadColor ??
|
||||||
|
this.ownMessageTheme.replyThreadColor,
|
||||||
) ??
|
) ??
|
||||||
this.otherMessageTheme,
|
this.otherMessageTheme,
|
||||||
reactionIcons: reactionIcons ?? this.reactionIcons,
|
reactionIcons: reactionIcons ?? this.reactionIcons,
|
||||||
@@ -297,16 +301,17 @@ class StreamChatThemeData {
|
|||||||
color: isDark
|
color: isDark
|
||||||
? Colors.white.withOpacity(.5)
|
? Colors.white.withOpacity(.5)
|
||||||
: Colors.black.withOpacity(.5),
|
: Colors.black.withOpacity(.5),
|
||||||
fontSize: 11,
|
fontSize: 12,
|
||||||
),
|
),
|
||||||
replies: TextStyle(
|
replies: TextStyle(
|
||||||
color: accentColor,
|
color: accentColor,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.w600,
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
),
|
),
|
||||||
messageBackgroundColor: isDark ? Color(0xff191919) : Color(0xffEAEAEA),
|
messageBackgroundColor: isDark ? Color(0xff191919) : Color(0xffEAEAEA),
|
||||||
reactionsBackgroundColor: isDark ? Colors.black : Colors.white,
|
reactionsBackgroundColor: isDark ? Colors.black : Colors.white,
|
||||||
reactionsBorderColor: isDark ? Color(0xff191919) : Color(0xffEAEAEA),
|
reactionsBorderColor: isDark ? Color(0xff191919) : Color(0xffEAEAEA),
|
||||||
|
replyThreadColor: isDark ? Color(0xff191919) : Color(0xffEAEAEA),
|
||||||
avatarTheme: AvatarTheme(
|
avatarTheme: AvatarTheme(
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
constraints: BoxConstraints.tightFor(
|
constraints: BoxConstraints.tightFor(
|
||||||
@@ -330,17 +335,19 @@ class StreamChatThemeData {
|
|||||||
color: isDark
|
color: isDark
|
||||||
? Colors.white.withOpacity(.5)
|
? Colors.white.withOpacity(.5)
|
||||||
: Colors.black.withOpacity(.5),
|
: Colors.black.withOpacity(.5),
|
||||||
fontSize: 11,
|
fontSize: 12,
|
||||||
),
|
),
|
||||||
replies: TextStyle(
|
replies: TextStyle(
|
||||||
color: accentColor,
|
color: accentColor,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.w600,
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
),
|
),
|
||||||
messageLinks: TextStyle(
|
messageLinks: TextStyle(
|
||||||
color: accentColor,
|
color: accentColor,
|
||||||
),
|
),
|
||||||
messageBackgroundColor: isDark ? Colors.black : Colors.white,
|
messageBackgroundColor: isDark ? Colors.black : Colors.white,
|
||||||
|
replyThreadColor:
|
||||||
|
isDark ? Colors.white.withAlpha(24) : Colors.black.withAlpha(24),
|
||||||
avatarTheme: AvatarTheme(
|
avatarTheme: AvatarTheme(
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
constraints: BoxConstraints.tightFor(
|
constraints: BoxConstraints.tightFor(
|
||||||
@@ -455,6 +462,7 @@ class MessageTheme {
|
|||||||
final Color messageBackgroundColor;
|
final Color messageBackgroundColor;
|
||||||
final Color reactionsBackgroundColor;
|
final Color reactionsBackgroundColor;
|
||||||
final Color reactionsBorderColor;
|
final Color reactionsBorderColor;
|
||||||
|
final Color replyThreadColor;
|
||||||
final AvatarTheme avatarTheme;
|
final AvatarTheme avatarTheme;
|
||||||
|
|
||||||
const MessageTheme({
|
const MessageTheme({
|
||||||
@@ -465,6 +473,7 @@ class MessageTheme {
|
|||||||
this.messageBackgroundColor,
|
this.messageBackgroundColor,
|
||||||
this.reactionsBackgroundColor,
|
this.reactionsBackgroundColor,
|
||||||
this.reactionsBorderColor,
|
this.reactionsBorderColor,
|
||||||
|
this.replyThreadColor,
|
||||||
this.avatarTheme,
|
this.avatarTheme,
|
||||||
this.createdAt,
|
this.createdAt,
|
||||||
});
|
});
|
||||||
@@ -479,6 +488,7 @@ class MessageTheme {
|
|||||||
AvatarTheme avatarTheme,
|
AvatarTheme avatarTheme,
|
||||||
Color reactionsBackgroundColor,
|
Color reactionsBackgroundColor,
|
||||||
Color reactionsBorderColor,
|
Color reactionsBorderColor,
|
||||||
|
Color replyThreadColor,
|
||||||
}) =>
|
}) =>
|
||||||
MessageTheme(
|
MessageTheme(
|
||||||
messageText: messageText ?? this.messageText,
|
messageText: messageText ?? this.messageText,
|
||||||
@@ -492,6 +502,7 @@ class MessageTheme {
|
|||||||
reactionsBackgroundColor:
|
reactionsBackgroundColor:
|
||||||
reactionsBackgroundColor ?? this.reactionsBackgroundColor,
|
reactionsBackgroundColor ?? this.reactionsBackgroundColor,
|
||||||
reactionsBorderColor: reactionsBorderColor ?? this.reactionsBorderColor,
|
reactionsBorderColor: reactionsBorderColor ?? this.reactionsBorderColor,
|
||||||
|
replyThreadColor: replyThreadColor ?? this.replyThreadColor,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -92,3 +92,12 @@ Future<bool> showConfirmationDialog(
|
|||||||
/// Get random png with initials
|
/// Get random png with initials
|
||||||
String getRandomPicUrl(User user) =>
|
String getRandomPicUrl(User user) =>
|
||||||
'https://getstream.io/random_png/?id=${user.id}&name=${user.name}';
|
'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_text.dart';
|
||||||
export 'src/message_widget.dart';
|
export 'src/message_widget.dart';
|
||||||
export 'src/reaction_picker.dart';
|
export 'src/reaction_picker.dart';
|
||||||
export 'src/reply_indicator.dart';
|
|
||||||
export 'src/sending_indicator.dart';
|
export 'src/sending_indicator.dart';
|
||||||
export 'src/stream_channel.dart';
|
export 'src/stream_channel.dart';
|
||||||
export 'src/stream_chat.dart';
|
export 'src/stream_chat.dart';
|
||||||
|
|||||||
Reference in New Issue
Block a user