Merge branch 'feature/new-ui' of github.com:GetStream/stream-chat-flutter into feature/group-info-screen
This commit is contained in:
@@ -510,6 +510,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,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -52,6 +52,9 @@ class ChannelImage extends StatelessWidget {
|
|||||||
this.onTap,
|
this.onTap,
|
||||||
this.showOnlineStatus = true,
|
this.showOnlineStatus = true,
|
||||||
this.borderRadius,
|
this.borderRadius,
|
||||||
|
this.selected = false,
|
||||||
|
this.selectionColor = const Color(0xFF006CFF),
|
||||||
|
this.selectionThickness = 4,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
final BorderRadius borderRadius;
|
final BorderRadius borderRadius;
|
||||||
@@ -67,6 +70,12 @@ class ChannelImage extends StatelessWidget {
|
|||||||
|
|
||||||
final bool showOnlineStatus;
|
final bool showOnlineStatus;
|
||||||
|
|
||||||
|
final bool selected;
|
||||||
|
|
||||||
|
final Color selectionColor;
|
||||||
|
|
||||||
|
final double selectionThickness;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final streamChat = StreamChat.of(context);
|
final streamChat = StreamChat.of(context);
|
||||||
@@ -94,11 +103,10 @@ class ChannelImage extends StatelessWidget {
|
|||||||
.channelPreviewTheme
|
.channelPreviewTheme
|
||||||
.avatarTheme
|
.avatarTheme
|
||||||
.constraints,
|
.constraints,
|
||||||
onTap: onTap != null
|
onTap: onTap != null ? (_) => onTap() : null,
|
||||||
? (_) {
|
selected: selected,
|
||||||
onTap();
|
selectionColor: selectionColor,
|
||||||
}
|
selectionThickness: selectionThickness,
|
||||||
: null,
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@@ -111,16 +119,20 @@ class ChannelImage extends StatelessWidget {
|
|||||||
.toList();
|
.toList();
|
||||||
return GroupImage(
|
return GroupImage(
|
||||||
images: images,
|
images: images,
|
||||||
|
borderRadius: borderRadius,
|
||||||
constraints: constraints ??
|
constraints: constraints ??
|
||||||
StreamChatTheme.of(context)
|
StreamChatTheme.of(context)
|
||||||
.channelPreviewTheme
|
.channelPreviewTheme
|
||||||
.avatarTheme
|
.avatarTheme
|
||||||
.constraints,
|
.constraints,
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
|
selected: selected,
|
||||||
|
selectionColor: selectionColor,
|
||||||
|
selectionThickness: selectionThickness,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return ClipRRect(
|
Widget child = ClipRRect(
|
||||||
borderRadius: borderRadius ??
|
borderRadius: borderRadius ??
|
||||||
StreamChatTheme.of(context)
|
StreamChatTheme.of(context)
|
||||||
.channelPreviewTheme
|
.channelPreviewTheme
|
||||||
@@ -169,6 +181,29 @@ class ChannelImage extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
if (selected) {
|
||||||
|
child = ClipRRect(
|
||||||
|
borderRadius: (borderRadius ??
|
||||||
|
StreamChatTheme.of(context)
|
||||||
|
.ownMessageTheme
|
||||||
|
.avatarTheme
|
||||||
|
.borderRadius) +
|
||||||
|
BorderRadius.circular(selectionThickness),
|
||||||
|
child: Container(
|
||||||
|
constraints: constraints ??
|
||||||
|
StreamChatTheme.of(context)
|
||||||
|
.ownMessageTheme
|
||||||
|
.avatarTheme
|
||||||
|
.constraints,
|
||||||
|
color: selectionColor,
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.all(selectionThickness),
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return child;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+154
-70
@@ -74,6 +74,8 @@ class ChannelListView extends StatefulWidget {
|
|||||||
this.onStartChatPressed,
|
this.onStartChatPressed,
|
||||||
this.swipeToAction = false,
|
this.swipeToAction = false,
|
||||||
this.pullToRefresh = true,
|
this.pullToRefresh = true,
|
||||||
|
this.crossAxisCount = 1,
|
||||||
|
this.selectedChannels = const [],
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
/// The builder that will be used in case of error
|
/// The builder that will be used in case of error
|
||||||
@@ -134,6 +136,11 @@ class ChannelListView extends StatefulWidget {
|
|||||||
/// Callback used in the default empty list widget
|
/// Callback used in the default empty list widget
|
||||||
final VoidCallback onStartChatPressed;
|
final VoidCallback onStartChatPressed;
|
||||||
|
|
||||||
|
/// The number of children in the cross axis.
|
||||||
|
final int crossAxisCount;
|
||||||
|
|
||||||
|
final List<Channel> selectedChannels;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_ChannelListViewState createState() => _ChannelListViewState();
|
_ChannelListViewState createState() => _ChannelListViewState();
|
||||||
}
|
}
|
||||||
@@ -259,22 +266,33 @@ class _ChannelListViewState extends State<ChannelListView>
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (channels.isNotEmpty) {
|
if (channels.isNotEmpty) {
|
||||||
child = ListView.custom(
|
if (widget.crossAxisCount > 1) {
|
||||||
physics: AlwaysScrollableScrollPhysics(),
|
child = GridView.builder(
|
||||||
controller: _scrollController,
|
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
childrenDelegate: SliverChildBuilderDelegate(
|
crossAxisCount: widget.crossAxisCount),
|
||||||
(context, i) {
|
itemCount: channels.length,
|
||||||
return _itemBuilder(context, i, channels);
|
physics: AlwaysScrollableScrollPhysics(),
|
||||||
|
controller: _scrollController,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
return _gridItemBuilder(context, index, channels);
|
||||||
},
|
},
|
||||||
childCount: (channels.length * 2) + 1,
|
);
|
||||||
findChildIndexCallback: (key) {
|
} else {
|
||||||
final ValueKey<String> valueKey = key;
|
child = ListView.separated(
|
||||||
final index = channels.indexWhere(
|
physics: AlwaysScrollableScrollPhysics(),
|
||||||
(channel) => 'CHANNEL-${channel.id}' == valueKey.value);
|
itemCount:
|
||||||
return index != -1 ? (index * 2) : null;
|
channels.isNotEmpty ? channels.length + 1 : channels.length,
|
||||||
|
separatorBuilder: (_, index) {
|
||||||
|
if (widget.separatorBuilder != null) {
|
||||||
|
return widget.separatorBuilder(context, index);
|
||||||
|
}
|
||||||
|
return _separatorBuilder(context, index);
|
||||||
},
|
},
|
||||||
),
|
itemBuilder: (context, index) {
|
||||||
);
|
return _listItemBuilder(context, index, channels);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -292,11 +310,13 @@ class _ChannelListViewState extends State<ChannelListView>
|
|||||||
children: List.generate(
|
children: List.generate(
|
||||||
25,
|
25,
|
||||||
(i) {
|
(i) {
|
||||||
if (i % 2 != 0) {
|
if (widget.crossAxisCount == 1) {
|
||||||
if (widget.separatorBuilder != null) {
|
if (i % 2 != 0) {
|
||||||
return widget.separatorBuilder(context, i);
|
if (widget.separatorBuilder != null) {
|
||||||
|
return widget.separatorBuilder(context, i);
|
||||||
|
}
|
||||||
|
return _separatorBuilder(context, i);
|
||||||
}
|
}
|
||||||
return _separatorBuilder(context, i);
|
|
||||||
}
|
}
|
||||||
return _buildLoadingItem();
|
return _buildLoadingItem();
|
||||||
},
|
},
|
||||||
@@ -305,63 +325,96 @@ class _ChannelListViewState extends State<ChannelListView>
|
|||||||
}
|
}
|
||||||
|
|
||||||
Shimmer _buildLoadingItem() {
|
Shimmer _buildLoadingItem() {
|
||||||
return Shimmer.fromColors(
|
if (widget.crossAxisCount > 1) {
|
||||||
baseColor: Color(0xffE5E5E5),
|
return Shimmer.fromColors(
|
||||||
highlightColor: Color(0xffffffff),
|
baseColor: Color(0xffE5E5E5),
|
||||||
child: ListTile(
|
highlightColor: Color(0xffffffff),
|
||||||
leading: Container(
|
child: Column(
|
||||||
decoration: BoxDecoration(
|
children: [
|
||||||
color: Colors.white,
|
SizedBox(
|
||||||
shape: BoxShape.circle,
|
height: 4.0,
|
||||||
),
|
),
|
||||||
constraints: BoxConstraints.tightFor(
|
Row(
|
||||||
height: 40,
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
width: 40,
|
children: [
|
||||||
),
|
for (int i = 0; i < widget.crossAxisCount; i++)
|
||||||
|
Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
constraints: BoxConstraints.tightFor(
|
||||||
|
height: 70,
|
||||||
|
width: 70,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
SizedBox(
|
||||||
|
height: 16.0,
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
title: Align(
|
);
|
||||||
alignment: Alignment.centerLeft,
|
} else {
|
||||||
child: Container(
|
return Shimmer.fromColors(
|
||||||
|
baseColor: Color(0xffE5E5E5),
|
||||||
|
highlightColor: Color(0xffffffff),
|
||||||
|
child: ListTile(
|
||||||
|
leading: Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
borderRadius: BorderRadius.circular(11),
|
shape: BoxShape.circle,
|
||||||
),
|
),
|
||||||
constraints: BoxConstraints.tightFor(
|
constraints: BoxConstraints.tightFor(
|
||||||
height: 16,
|
height: 40,
|
||||||
width: 82,
|
width: 40,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
title: Align(
|
||||||
subtitle: Row(
|
alignment: Alignment.centerLeft,
|
||||||
children: [
|
child: Container(
|
||||||
Align(
|
|
||||||
alignment: Alignment.centerLeft,
|
|
||||||
child: Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.white,
|
|
||||||
borderRadius: BorderRadius.circular(11),
|
|
||||||
),
|
|
||||||
constraints: BoxConstraints.tightFor(
|
|
||||||
height: 16,
|
|
||||||
width: 238,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Container(
|
|
||||||
margin: const EdgeInsets.only(left: 16),
|
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
borderRadius: BorderRadius.circular(11),
|
borderRadius: BorderRadius.circular(11),
|
||||||
),
|
),
|
||||||
constraints: BoxConstraints.tightFor(
|
constraints: BoxConstraints.tightFor(
|
||||||
height: 16,
|
height: 16,
|
||||||
width: 42,
|
width: 82,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
|
subtitle: Row(
|
||||||
|
children: [
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(11),
|
||||||
|
),
|
||||||
|
constraints: BoxConstraints.tightFor(
|
||||||
|
height: 16,
|
||||||
|
width: 238,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
margin: const EdgeInsets.only(left: 16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(11),
|
||||||
|
),
|
||||||
|
constraints: BoxConstraints.tightFor(
|
||||||
|
height: 16,
|
||||||
|
width: 42,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
);
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildErrorWidget(
|
Widget _buildErrorWidget(
|
||||||
@@ -428,20 +481,10 @@ class _ChannelListViewState extends State<ChannelListView>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _itemBuilder(context, int i, List<Channel> channels) {
|
Widget _listItemBuilder(BuildContext context, int i, List<Channel> channels) {
|
||||||
if (i % 2 != 0) {
|
|
||||||
if (widget.separatorBuilder != null) {
|
|
||||||
return widget.separatorBuilder(context, i);
|
|
||||||
}
|
|
||||||
return _separatorBuilder(context, i);
|
|
||||||
}
|
|
||||||
|
|
||||||
i = i ~/ 2;
|
|
||||||
|
|
||||||
final channelsProvider = ChannelsBloc.of(context);
|
final channelsProvider = ChannelsBloc.of(context);
|
||||||
if (i < channels.length) {
|
if (i < channels.length) {
|
||||||
final channel = channels[i];
|
final channel = channels[i];
|
||||||
|
|
||||||
ChannelTapCallback onTap;
|
ChannelTapCallback onTap;
|
||||||
if (widget.onChannelTap != null) {
|
if (widget.onChannelTap != null) {
|
||||||
onTap = widget.onChannelTap;
|
onTap = widget.onChannelTap;
|
||||||
@@ -581,6 +624,47 @@ class _ChannelListViewState extends State<ChannelListView>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _gridItemBuilder(BuildContext context, int i, List<Channel> channels) {
|
||||||
|
var channel = channels[i];
|
||||||
|
|
||||||
|
var selected = widget.selectedChannels.contains(channel);
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
key: ValueKey<String>('CHANNEL-${channel.id}'),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
ChannelImage(
|
||||||
|
channel: channel,
|
||||||
|
borderRadius: BorderRadius.circular(32),
|
||||||
|
selected: selected,
|
||||||
|
constraints: BoxConstraints.tightFor(
|
||||||
|
width: 64,
|
||||||
|
height: 64,
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
widget.onChannelTap(channel, null);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
SizedBox(height: 7),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
|
child: StreamChannel(
|
||||||
|
child: ChannelName(
|
||||||
|
textStyle: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
channel: channel,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildQueryProgressIndicator(
|
Widget _buildQueryProgressIndicator(
|
||||||
context,
|
context,
|
||||||
ChannelsBlocState channelsProvider,
|
ChannelsBlocState channelsProvider,
|
||||||
|
|||||||
@@ -9,21 +9,33 @@ class GroupImage extends StatelessWidget {
|
|||||||
@required this.images,
|
@required this.images,
|
||||||
this.constraints,
|
this.constraints,
|
||||||
this.onTap,
|
this.onTap,
|
||||||
|
this.borderRadius,
|
||||||
|
this.selected = false,
|
||||||
|
this.selectionColor = const Color(0xFF006CFF),
|
||||||
|
this.selectionThickness = 4,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
final List<String> images;
|
final List<String> images;
|
||||||
final BoxConstraints constraints;
|
final BoxConstraints constraints;
|
||||||
final VoidCallback onTap;
|
final VoidCallback onTap;
|
||||||
|
final bool selected;
|
||||||
|
final BorderRadius borderRadius;
|
||||||
|
final Color selectionColor;
|
||||||
|
final double selectionThickness;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return GestureDetector(
|
var avatar;
|
||||||
|
final streamChatTheme = StreamChatTheme.of(context);
|
||||||
|
|
||||||
|
avatar = GestureDetector(
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
borderRadius: StreamChatTheme.of(context)
|
borderRadius: borderRadius ??
|
||||||
.ownMessageTheme
|
StreamChatTheme.of(context)
|
||||||
.avatarTheme
|
.ownMessageTheme
|
||||||
.borderRadius,
|
.avatarTheme
|
||||||
|
.borderRadius,
|
||||||
child: Container(
|
child: Container(
|
||||||
constraints: constraints ??
|
constraints: constraints ??
|
||||||
StreamChatTheme.of(context)
|
StreamChatTheme.of(context)
|
||||||
@@ -91,5 +103,24 @@ class GroupImage extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (selected) {
|
||||||
|
avatar = ClipRRect(
|
||||||
|
borderRadius: (borderRadius ??
|
||||||
|
streamChatTheme.ownMessageTheme.avatarTheme.borderRadius) +
|
||||||
|
BorderRadius.circular(selectionThickness),
|
||||||
|
child: Container(
|
||||||
|
color: selectionColor,
|
||||||
|
height: 64.0,
|
||||||
|
width: 64.0,
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.all(selectionThickness),
|
||||||
|
child: avatar,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return avatar;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+79
-82
@@ -16,8 +16,6 @@ import 'package:stream_chat_flutter/src/stream_chat.dart';
|
|||||||
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
|
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
|
||||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
|
|
||||||
import 'stream_channel.dart';
|
|
||||||
|
|
||||||
class ImageFooter extends StatefulWidget {
|
class ImageFooter extends StatefulWidget {
|
||||||
/// Callback to call when pressing the back button.
|
/// Callback to call when pressing the back button.
|
||||||
/// By default it calls [Navigator.pop]
|
/// By default it calls [Navigator.pop]
|
||||||
@@ -64,11 +62,11 @@ class _ImageFooterState extends State<ImageFooter> {
|
|||||||
bool _userSearchMode = false;
|
bool _userSearchMode = false;
|
||||||
TextEditingController _searchController;
|
TextEditingController _searchController;
|
||||||
TextEditingController _messageController = TextEditingController();
|
TextEditingController _messageController = TextEditingController();
|
||||||
|
FocusNode _messageFocusNode = FocusNode();
|
||||||
|
|
||||||
String _userNameQuery;
|
String _channelNameQuery;
|
||||||
bool _isSearchActive = false;
|
|
||||||
|
|
||||||
Set<User> _selectedUsers = {};
|
List<Channel> _selectedChannels = [];
|
||||||
bool _loading = false;
|
bool _loading = false;
|
||||||
|
|
||||||
Timer _debounce;
|
Timer _debounce;
|
||||||
@@ -80,8 +78,7 @@ class _ImageFooterState extends State<ImageFooter> {
|
|||||||
_debounce = Timer(const Duration(milliseconds: 350), () {
|
_debounce = Timer(const Duration(milliseconds: 350), () {
|
||||||
if (mounted && modalSetStateCallback != null) {
|
if (mounted && modalSetStateCallback != null) {
|
||||||
modalSetStateCallback(() {
|
modalSetStateCallback(() {
|
||||||
_userNameQuery = _searchController.text;
|
_channelNameQuery = _searchController.text;
|
||||||
_isSearchActive = _userNameQuery.isNotEmpty;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -91,6 +88,9 @@ class _ImageFooterState extends State<ImageFooter> {
|
|||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_searchController = TextEditingController()..addListener(_userNameListener);
|
_searchController = TextEditingController()..addListener(_userNameListener);
|
||||||
|
_messageFocusNode.addListener(() {
|
||||||
|
setState(() {});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -258,7 +258,7 @@ class _ImageFooterState extends State<ImageFooter> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildShareModal(context) {
|
void _buildShareModal(context) {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
@@ -266,7 +266,7 @@ class _ImageFooterState extends State<ImageFooter> {
|
|||||||
modalSetStateCallback = modalSetState;
|
modalSetStateCallback = modalSetState;
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: EdgeInsets.only(
|
padding: EdgeInsets.only(
|
||||||
top: _userSearchMode
|
top: _userSearchMode || _messageFocusNode.hasFocus
|
||||||
? 16.0
|
? 16.0
|
||||||
: MediaQuery.of(context).size.height / 2,
|
: MediaQuery.of(context).size.height / 2,
|
||||||
left: 8.0,
|
left: 8.0,
|
||||||
@@ -282,74 +282,81 @@ class _ImageFooterState extends State<ImageFooter> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
_buildTextInputSection(modalSetState),
|
_buildTextInputSection(modalSetState),
|
||||||
|
if (_userSearchMode)
|
||||||
|
SizedBox(
|
||||||
|
height: 22.0,
|
||||||
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: UserListView(
|
child: ChannelsBloc(
|
||||||
selectedUsers: _selectedUsers,
|
child: ChannelListView(
|
||||||
onUserTap: (user, _) {
|
selectedChannels: _selectedChannels,
|
||||||
_searchController.clear();
|
onChannelTap: (channel, _) {
|
||||||
if (!_selectedUsers.contains(user)) {
|
_searchController.clear();
|
||||||
modalSetState(() {
|
if (!_selectedChannels.contains(channel)) {
|
||||||
_selectedUsers.add(user);
|
modalSetState(() {
|
||||||
});
|
_selectedChannels.add(channel);
|
||||||
} else {
|
});
|
||||||
modalSetState(() {
|
} else {
|
||||||
_selectedUsers.remove(user);
|
modalSetState(() {
|
||||||
});
|
_selectedChannels.remove(channel);
|
||||||
}
|
});
|
||||||
},
|
}
|
||||||
crossAxisCount: 4,
|
|
||||||
pagination: PaginationParams(
|
|
||||||
limit: 25,
|
|
||||||
),
|
|
||||||
filter: {
|
|
||||||
if (_searchController.text.isNotEmpty)
|
|
||||||
'name': {
|
|
||||||
r'$autocomplete': _userNameQuery,
|
|
||||||
},
|
|
||||||
'id': {
|
|
||||||
r'$ne': StreamChat.of(context).user.id,
|
|
||||||
},
|
},
|
||||||
},
|
crossAxisCount: 4,
|
||||||
sort: [
|
pagination: PaginationParams(
|
||||||
SortOption(
|
limit: 25,
|
||||||
'name',
|
|
||||||
direction: 1,
|
|
||||||
),
|
),
|
||||||
],
|
filter: {
|
||||||
emptyBuilder: (_) {
|
if (_searchController.text.isNotEmpty)
|
||||||
return LayoutBuilder(
|
'name': {
|
||||||
builder: (context, viewportConstraints) {
|
r'$autocomplete': _channelNameQuery,
|
||||||
return SingleChildScrollView(
|
},
|
||||||
physics: AlwaysScrollableScrollPhysics(),
|
'id': {
|
||||||
child: ConstrainedBox(
|
r'$ne': StreamChat.of(context).user.id,
|
||||||
constraints: BoxConstraints(
|
},
|
||||||
minHeight: viewportConstraints.maxHeight,
|
},
|
||||||
),
|
sort: [
|
||||||
child: Center(
|
SortOption(
|
||||||
child: Column(
|
'name',
|
||||||
children: [
|
direction: 1,
|
||||||
Padding(
|
),
|
||||||
padding: const EdgeInsets.all(24),
|
],
|
||||||
child: StreamSvgIcon.search(
|
emptyBuilder: (_) {
|
||||||
size: 96,
|
return LayoutBuilder(
|
||||||
color: Colors.grey,
|
builder: (context, viewportConstraints) {
|
||||||
|
return SingleChildScrollView(
|
||||||
|
physics: AlwaysScrollableScrollPhysics(),
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: BoxConstraints(
|
||||||
|
minHeight:
|
||||||
|
viewportConstraints.maxHeight,
|
||||||
|
),
|
||||||
|
child: Center(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
child: StreamSvgIcon.search(
|
||||||
|
size: 96,
|
||||||
|
color: Colors.grey,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
Text(
|
||||||
Text(
|
'No chat matches these keywords...'),
|
||||||
'No user matches these keywords...'),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
);
|
},
|
||||||
},
|
);
|
||||||
);
|
},
|
||||||
},
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (_selectedUsers.isNotEmpty)
|
if (_selectedChannels.isNotEmpty)
|
||||||
_buildShareTextInputSection(modalSetState),
|
_buildShareTextInputSection(modalSetState),
|
||||||
if (!_userSearchMode && _selectedUsers.isEmpty)
|
if (!_userSearchMode && _selectedChannels.isEmpty)
|
||||||
Align(
|
Align(
|
||||||
alignment: Alignment.bottomCenter,
|
alignment: Alignment.bottomCenter,
|
||||||
child: Container(
|
child: Container(
|
||||||
@@ -544,6 +551,7 @@ class _ImageFooterState extends State<ImageFooter> {
|
|||||||
padding: const EdgeInsets.only(left: 8.0),
|
padding: const EdgeInsets.only(left: 8.0),
|
||||||
child: TextField(
|
child: TextField(
|
||||||
controller: _messageController,
|
controller: _messageController,
|
||||||
|
focusNode: _messageFocusNode,
|
||||||
onChanged: (val) {
|
onChanged: (val) {
|
||||||
modalSetState(() {});
|
modalSetState(() {});
|
||||||
},
|
},
|
||||||
@@ -612,27 +620,16 @@ class _ImageFooterState extends State<ImageFooter> {
|
|||||||
|
|
||||||
_messageController.clear();
|
_messageController.clear();
|
||||||
|
|
||||||
final client = StreamChat.of(context).client;
|
for (var channel in _selectedChannels) {
|
||||||
|
|
||||||
for (var user in _selectedUsers) {
|
|
||||||
var c = client.channel('messaging', extraData: {
|
|
||||||
'members': [
|
|
||||||
user.id,
|
|
||||||
StreamChat.of(context).user.id,
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
await c.watch();
|
|
||||||
|
|
||||||
final message = Message(
|
final message = Message(
|
||||||
text: text,
|
text: text,
|
||||||
attachments: [attachments[widget.currentPage]],
|
attachments: [attachments[widget.currentPage]],
|
||||||
);
|
);
|
||||||
|
|
||||||
await c.sendMessage(message);
|
await channel.sendMessage(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
_selectedUsers.clear();
|
_selectedChannels.clear();
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -113,11 +113,12 @@ 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,
|
||||||
showReactionPickerIndicator: true,
|
showReactionPickerIndicator: true,
|
||||||
|
showInChannelIndicator: false,
|
||||||
showSendingIndicator: DisplayWidget.gone,
|
showSendingIndicator: DisplayWidget.gone,
|
||||||
shape: messageShape,
|
shape: messageShape,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -301,7 +301,7 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
),
|
),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
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) {
|
builder: (context, snapshot) {
|
||||||
if (!snapshot.hasData) {
|
if (!snapshot.hasData) {
|
||||||
return Center(
|
return Center(
|
||||||
child: CircularProgressIndicator(),
|
child: const CircularProgressIndicator(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -336,18 +336,27 @@ 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(
|
begin: Alignment.topCenter,
|
||||||
padding: const EdgeInsets.all(8),
|
end: Alignment.bottomCenter,
|
||||||
|
colors: [
|
||||||
|
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 +594,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 +688,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 +758,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,
|
||||||
|
|||||||
+303
-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)
|
||||||
|
_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() {
|
Widget _buildUrlAttachment() {
|
||||||
var urlAttachment = widget.message.attachments
|
var urlAttachment = widget.message.attachments
|
||||||
.firstWhere((element) => element.ogScrapeUrl != null);
|
.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 =>
|
bool get isGiphy =>
|
||||||
widget.message.attachments?.any((element) => element.type == 'giphy') ==
|
widget.message.attachments?.any((element) => element.type == 'giphy') ==
|
||||||
true;
|
true;
|
||||||
@@ -565,7 +626,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 +776,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 +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;
|
if (_topPaginationEnded || _queryTopMessagesController.value) return;
|
||||||
_queryTopMessagesController.add(true);
|
_queryTopMessagesController.add(true);
|
||||||
|
|
||||||
if (!channel.state.threads.containsKey(parentId)) {
|
Message message;
|
||||||
return _queryTopMessagesController.add(false);
|
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 {
|
try {
|
||||||
final state = await queryBeforeMessage(
|
final response = await channel.getReplies(
|
||||||
message.id,
|
parentId,
|
||||||
limit: limit,
|
PaginationParams(
|
||||||
|
lessThan: message?.id,
|
||||||
|
limit: limit,
|
||||||
|
),
|
||||||
preferOffline: preferOffline,
|
preferOffline: preferOffline,
|
||||||
);
|
);
|
||||||
if (state.messages.isEmpty || state.messages.length < limit) {
|
if (response.messages.isEmpty || response.messages.length < limit) {
|
||||||
_topPaginationEnded = true;
|
_topPaginationEnded = true;
|
||||||
}
|
}
|
||||||
_queryTopMessagesController.add(false);
|
_queryTopMessagesController.add(false);
|
||||||
@@ -267,6 +268,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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+45
-36
@@ -1,7 +1,9 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:stream_chat/stream_chat.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_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(
|
return AppBar(
|
||||||
automaticallyImplyLeading: false,
|
automaticallyImplyLeading: false,
|
||||||
elevation: 1,
|
elevation: 1,
|
||||||
|
leading: showBackButton
|
||||||
|
? StreamBackButton(
|
||||||
|
onPressed: onBackPressed,
|
||||||
|
showUnreads: true,
|
||||||
|
)
|
||||||
|
: SizedBox(),
|
||||||
backgroundColor:
|
backgroundColor:
|
||||||
StreamChatTheme.of(context).channelTheme.channelHeaderTheme.color,
|
StreamChatTheme.of(context).channelTheme.channelHeaderTheme.color,
|
||||||
actions: <Widget>[
|
centerTitle: true,
|
||||||
Container(
|
title: Column(
|
||||||
child: showBackButton
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
? AspectRatio(
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
aspectRatio: 1,
|
children: [
|
||||||
child: IconButton(
|
Text(
|
||||||
onPressed: onBackPressed ?? () => Navigator.pop(context),
|
'Thread Reply',
|
||||||
icon: StreamSvgIcon.close(
|
style: StreamChatTheme.of(context)
|
||||||
size: 24,
|
.channelTheme
|
||||||
color: Theme.of(context).brightness == Brightness.dark
|
.channelHeaderTheme
|
||||||
? Colors.white
|
.title,
|
||||||
: Colors.black,
|
),
|
||||||
),
|
SizedBox(height: 2),
|
||||||
),
|
Row(
|
||||||
)
|
mainAxisSize: MainAxisSize.min,
|
||||||
: SizedBox(),
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
),
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
],
|
children: [
|
||||||
centerTitle: false,
|
Text(
|
||||||
title: Text.rich(
|
'with ',
|
||||||
TextSpan(
|
style: StreamChatTheme.of(context)
|
||||||
text: 'Thread',
|
.channelTheme
|
||||||
children: [
|
.channelHeaderTheme
|
||||||
TextSpan(
|
.lastMessageAt,
|
||||||
text:
|
),
|
||||||
' ${parent.replyCount} ${parent.replyCount == 1 ? 'reply' : 'replies'}',
|
Flexible(
|
||||||
style: StreamChatTheme.of(context)
|
child: ChannelName(
|
||||||
.channelTheme
|
textStyle: StreamChatTheme.of(context)
|
||||||
.channelHeaderTheme
|
.channelTheme
|
||||||
.lastMessageAt,
|
.channelHeaderTheme
|
||||||
),
|
.lastMessageAt,
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
style:
|
],
|
||||||
StreamChatTheme.of(context).channelTheme.channelHeaderTheme.title,
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,12 +61,15 @@ class UserAvatar extends StatelessWidget {
|
|||||||
: streamChatTheme.defaultUserImage(context, user),
|
: streamChatTheme.defaultUserImage(context, user),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (selected) {
|
if (selected) {
|
||||||
avatar = ClipRRect(
|
avatar = ClipRRect(
|
||||||
borderRadius: (borderRadius ??
|
borderRadius: (borderRadius ??
|
||||||
streamChatTheme.ownMessageTheme.avatarTheme.borderRadius) +
|
streamChatTheme.ownMessageTheme.avatarTheme.borderRadius) +
|
||||||
BorderRadius.circular(selectionThickness),
|
BorderRadius.circular(selectionThickness),
|
||||||
child: Container(
|
child: Container(
|
||||||
|
constraints: constraints ??
|
||||||
|
streamChatTheme.ownMessageTheme.avatarTheme.constraints,
|
||||||
color: selectionColor,
|
color: selectionColor,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: EdgeInsets.all(selectionThickness),
|
padding: EdgeInsets.all(selectionThickness),
|
||||||
|
|||||||
@@ -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