Merge branch feat/new-ui into feat/messg-reply
Signed-off-by: Sahil Kumar <[email protected]>
This commit is contained in:
@@ -40,7 +40,7 @@ PODS:
|
||||
- Firebase/Messaging (6.33.0):
|
||||
- Firebase/CoreOnly
|
||||
- FirebaseMessaging (~> 4.7.0)
|
||||
- firebase_core (0.5.2-1):
|
||||
- firebase_core (0.5.2):
|
||||
- Firebase/CoreOnly (~> 6.33.0)
|
||||
- Flutter
|
||||
- firebase_messaging (7.0.3):
|
||||
@@ -258,7 +258,7 @@ SPEC CHECKSUMS:
|
||||
esys_flutter_share: 403498dab005b36ce1f8d7aff377e81f0621b0b4
|
||||
file_picker: 3e6c3790de664ccf9b882732d9db5eaf6b8d4eb1
|
||||
Firebase: 8db6f2d1b2c5e2984efba4949a145875a8f65fe5
|
||||
firebase_core: 7423d688a1c6f2f2d859d64ae26991be39989781
|
||||
firebase_core: 350ba329d1641211bc6183a3236893cafdacfea7
|
||||
firebase_messaging: 0aea2cd5885b65e19ede58ee3507f485c992cc75
|
||||
FirebaseCore: d889d9e12535b7f36ac8bfbf1713a0836a3012cd
|
||||
FirebaseCoreDiagnostics: 770ac5958e1372ce67959ae4b4f31d8e127c3ac1
|
||||
|
||||
@@ -133,7 +133,7 @@ class _GroupChatDetailsScreenState extends State<GroupChatDetailsScreen> {
|
||||
context,
|
||||
Routes.CHANNEL_PAGE,
|
||||
ModalRoute.withName(Routes.HOME),
|
||||
arguments: channel,
|
||||
arguments: ChannelPageArgs(channel: channel),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
+112
-111
@@ -213,12 +213,16 @@ class _HomePageState extends State<HomePage> {
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: ListTile(
|
||||
onTap: () async {
|
||||
await StreamChat.of(context).client.disconnect();
|
||||
Navigator.pop(context);
|
||||
|
||||
final secureStorage = FlutterSecureStorage();
|
||||
await secureStorage.deleteAll();
|
||||
Navigator.pop(context);
|
||||
Navigator.pushReplacementNamed(
|
||||
|
||||
StreamChat.of(context).client.disconnect(
|
||||
clearUser: true,
|
||||
);
|
||||
|
||||
await Navigator.pushReplacementNamed(
|
||||
context,
|
||||
Routes.CHOOSE_USER,
|
||||
);
|
||||
@@ -294,128 +298,115 @@ class _ChannelListPageState extends State<ChannelListPage> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final user = StreamChat.of(context).user;
|
||||
return ChannelsBloc(
|
||||
child: MessageSearchBloc(
|
||||
child: Column(
|
||||
children: [
|
||||
SearchTextField(
|
||||
controller: _controller,
|
||||
showCloseButton: _isSearchActive,
|
||||
),
|
||||
Expanded(
|
||||
child: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 350),
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onPanDown: (_) => FocusScope.of(context).unfocus(),
|
||||
child: _isSearchActive
|
||||
? MessageSearchListView(
|
||||
messageQuery: _channelQuery,
|
||||
filters: {
|
||||
'members': {
|
||||
r'$in': [user.id]
|
||||
}
|
||||
},
|
||||
sortOptions: [
|
||||
SortOption(
|
||||
'created_at',
|
||||
direction: SortOption.ASC,
|
||||
),
|
||||
],
|
||||
paginationParams: PaginationParams(limit: 20),
|
||||
onItemTap: (message) {},
|
||||
)
|
||||
: ChannelListView(
|
||||
onStartChatPressed: () {
|
||||
Navigator.pushNamed(context, Routes.NEW_CHAT);
|
||||
},
|
||||
swipeToAction: true,
|
||||
filter: {
|
||||
'members': {
|
||||
r'$in': [user.id],
|
||||
return WillPopScope(
|
||||
onWillPop: () async {
|
||||
if (_isSearchActive) {
|
||||
_controller.clear();
|
||||
setState(() => _isSearchActive = false);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
child: ChannelsBloc(
|
||||
child: MessageSearchBloc(
|
||||
child: Column(
|
||||
children: [
|
||||
SearchTextField(
|
||||
controller: _controller,
|
||||
showCloseButton: _isSearchActive,
|
||||
),
|
||||
Expanded(
|
||||
child: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 350),
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onPanDown: (_) => FocusScope.of(context).unfocus(),
|
||||
child: _isSearchActive
|
||||
? MessageSearchListView(
|
||||
messageQuery: _channelQuery,
|
||||
filters: {
|
||||
'members': {
|
||||
r'$in': [user.id]
|
||||
}
|
||||
},
|
||||
},
|
||||
options: {
|
||||
'presence': true,
|
||||
},
|
||||
pagination: PaginationParams(
|
||||
limit: 20,
|
||||
sortOptions: [
|
||||
SortOption(
|
||||
'created_at',
|
||||
direction: SortOption.ASC,
|
||||
),
|
||||
],
|
||||
paginationParams: PaginationParams(limit: 20),
|
||||
onItemTap: (messageResponse) async {
|
||||
final client = StreamChat.of(context).client;
|
||||
final message = messageResponse.message;
|
||||
final channel = client.channel(
|
||||
messageResponse.channel.type,
|
||||
id: messageResponse.channel.id,
|
||||
);
|
||||
if (channel.state == null) {
|
||||
await channel.watch();
|
||||
}
|
||||
Navigator.pushNamed(
|
||||
context,
|
||||
Routes.CHANNEL_PAGE,
|
||||
arguments: ChannelPageArgs(
|
||||
channel: channel,
|
||||
initialMessage: message,
|
||||
),
|
||||
);
|
||||
},
|
||||
)
|
||||
: ChannelListView(
|
||||
onStartChatPressed: () {
|
||||
Navigator.pushNamed(context, Routes.NEW_CHAT);
|
||||
},
|
||||
swipeToAction: true,
|
||||
filter: {
|
||||
'members': {
|
||||
r'$in': [user.id],
|
||||
},
|
||||
},
|
||||
options: {
|
||||
'presence': true,
|
||||
},
|
||||
pagination: PaginationParams(
|
||||
limit: 20,
|
||||
),
|
||||
channelWidget: ChannelPage(),
|
||||
),
|
||||
channelWidget: ChannelPage(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ChannelQuerySearchResultPage extends StatelessWidget {
|
||||
final Stream<List<Message>> searchResultStream;
|
||||
class ChannelPageArgs {
|
||||
final Channel channel;
|
||||
final Message initialMessage;
|
||||
|
||||
const ChannelQuerySearchResultPage({
|
||||
Key key,
|
||||
@required this.searchResultStream,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return StreamBuilder<List<Message>>(
|
||||
initialData: const <Message>[],
|
||||
stream: searchResultStream,
|
||||
builder: (context, snapshot) {
|
||||
final result = snapshot.data;
|
||||
return Column(
|
||||
children: [
|
||||
if (result.isNotEmpty)
|
||||
Container(
|
||||
width: double.maxFinite,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.centerLeft,
|
||||
end: Alignment.centerRight,
|
||||
colors: [
|
||||
Colors.black.withOpacity(0.02),
|
||||
Colors.white.withOpacity(0.05),
|
||||
],
|
||||
stops: [0, 1],
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 8,
|
||||
horizontal: 8,
|
||||
),
|
||||
child: Text(
|
||||
'${result.length} results',
|
||||
style: TextStyle(
|
||||
color: Colors.black.withOpacity(0.5),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
itemCount: result.length,
|
||||
itemBuilder: (context, index) {
|
||||
return ListTile(
|
||||
leading: UserAvatar(),
|
||||
title: Text(result[index].toJson().toString()),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
const ChannelPageArgs({
|
||||
this.channel,
|
||||
this.initialMessage,
|
||||
});
|
||||
}
|
||||
|
||||
class ChannelPage extends StatefulWidget {
|
||||
final int initialScrollIndex;
|
||||
final double initialAlignment;
|
||||
final bool highlightInitialMessage;
|
||||
|
||||
const ChannelPage({
|
||||
Key key,
|
||||
this.initialScrollIndex,
|
||||
this.initialAlignment,
|
||||
this.highlightInitialMessage = false,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
_ChannelPageState createState() => _ChannelPageState();
|
||||
}
|
||||
@@ -454,6 +445,9 @@ class _ChannelPageState extends State<ChannelPage> {
|
||||
child: Stack(
|
||||
children: <Widget>[
|
||||
MessageListView(
|
||||
initialScrollIndex: widget.initialScrollIndex,
|
||||
initialAlignment: widget.initialAlignment,
|
||||
highlightInitialMessage: widget.highlightInitialMessage,
|
||||
onMessageSwiped: _reply,
|
||||
onReplyTap: _reply,
|
||||
threadBuilder: (_, parentMessage) {
|
||||
@@ -493,15 +487,20 @@ class _ChannelPageState extends State<ChannelPage> {
|
||||
|
||||
class ThreadPage extends StatelessWidget {
|
||||
final Message parent;
|
||||
final int initialScrollIndex;
|
||||
final double initialAlignment;
|
||||
|
||||
ThreadPage({
|
||||
Key key,
|
||||
this.parent,
|
||||
this.initialScrollIndex,
|
||||
this.initialAlignment,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Color.fromRGBO(252, 252, 252, 1),
|
||||
appBar: ThreadHeader(
|
||||
parent: parent,
|
||||
),
|
||||
@@ -510,6 +509,8 @@ class ThreadPage extends StatelessWidget {
|
||||
Expanded(
|
||||
child: MessageListView(
|
||||
parentMessage: parent,
|
||||
initialScrollIndex: initialScrollIndex,
|
||||
initialAlignment: initialAlignment,
|
||||
),
|
||||
),
|
||||
if (parent.type != 'deleted')
|
||||
|
||||
@@ -37,6 +37,8 @@ class _NewChatScreenState extends State<NewChatScreen> {
|
||||
|
||||
bool _showUserList = true;
|
||||
|
||||
bool _channelExisted = false;
|
||||
|
||||
void _userNameListener() {
|
||||
if (_debounce?.isActive ?? false) _debounce.cancel();
|
||||
_debounce = Timer(const Duration(milliseconds: 350), () {
|
||||
@@ -56,11 +58,6 @@ class _NewChatScreenState extends State<NewChatScreen> {
|
||||
|
||||
_searchFocusNode.addListener(() async {
|
||||
if (_searchFocusNode.hasFocus && !_showUserList) {
|
||||
if (channel.extraData['draft'] == true) {
|
||||
await channel.stopWatching();
|
||||
channel.dispose();
|
||||
channel.client.state.channels.remove(channel.cid);
|
||||
}
|
||||
setState(() {
|
||||
_showUserList = true;
|
||||
});
|
||||
@@ -71,19 +68,38 @@ class _NewChatScreenState extends State<NewChatScreen> {
|
||||
if (_messageInputFocusNode.hasFocus && _selectedUsers.isNotEmpty) {
|
||||
final chatState = StreamChat.of(context);
|
||||
|
||||
channel = chatState.client.channel(
|
||||
'messaging',
|
||||
extraData: {
|
||||
final res = await chatState.client.queryChannels(
|
||||
options: {
|
||||
'state': false,
|
||||
'watch': false,
|
||||
},
|
||||
filter: {
|
||||
'members': [
|
||||
..._selectedUsers.map((e) => e.id),
|
||||
chatState.user.id,
|
||||
],
|
||||
'draft': true,
|
||||
'distinct': true,
|
||||
},
|
||||
messageLimit: 0,
|
||||
paginationParams: PaginationParams(
|
||||
limit: 1,
|
||||
),
|
||||
);
|
||||
|
||||
if (!chatState.client.state.channels.containsKey(channel.cid)) {
|
||||
final _channelExisted = res.length == 1;
|
||||
if (_channelExisted) {
|
||||
channel = res.first;
|
||||
await channel.watch();
|
||||
} else {
|
||||
channel = chatState.client.channel(
|
||||
'messaging',
|
||||
extraData: {
|
||||
'members': [
|
||||
..._selectedUsers.map((e) => e.id),
|
||||
chatState.user.id,
|
||||
],
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
setState(() {
|
||||
@@ -134,6 +150,7 @@ class _NewChatScreenState extends State<NewChatScreen> {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
_chipInputTextFieldState.removeItem(user);
|
||||
_searchFocusNode.requestFocus();
|
||||
},
|
||||
child: Stack(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
@@ -311,18 +328,38 @@ class _NewChatScreenState extends State<NewChatScreen> {
|
||||
),
|
||||
),
|
||||
)
|
||||
: MessageListView(),
|
||||
: FutureBuilder<bool>(
|
||||
future: channel.initialized,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.data == true) {
|
||||
return MessageListView();
|
||||
}
|
||||
|
||||
return Center(
|
||||
child: Text(
|
||||
'No chats here yet...',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.black.withOpacity(.5),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
MessageInput(
|
||||
focusNode: _messageInputFocusNode,
|
||||
preMessageSending: (message) async {
|
||||
await channel.watch();
|
||||
return message;
|
||||
},
|
||||
onMessageSent: (m) {
|
||||
if (!m.isEphemeral) {
|
||||
_updateChannelAndNavigate(context);
|
||||
} else {
|
||||
channel.on('message.new').first.then((_) {
|
||||
_updateChannelAndNavigate(context);
|
||||
});
|
||||
}
|
||||
Navigator.pushNamedAndRemoveUntil(
|
||||
context,
|
||||
Routes.CHANNEL_PAGE,
|
||||
ModalRoute.withName(Routes.HOME),
|
||||
arguments: ChannelPageArgs(channel: channel),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
@@ -330,16 +367,4 @@ class _NewChatScreenState extends State<NewChatScreen> {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _updateChannelAndNavigate(BuildContext context) {
|
||||
channel.update({
|
||||
'draft': false,
|
||||
});
|
||||
Navigator.pushNamedAndRemoveUntil(
|
||||
context,
|
||||
Routes.CHANNEL_PAGE,
|
||||
ModalRoute.withName(Routes.HOME),
|
||||
arguments: channel,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,9 +34,13 @@ class AppRoutes {
|
||||
return MaterialPageRoute(
|
||||
settings: const RouteSettings(name: Routes.CHANNEL_PAGE),
|
||||
builder: (_) {
|
||||
final arg = args as ChannelPageArgs;
|
||||
return StreamChannel(
|
||||
channel: args as Channel,
|
||||
child: ChannelPage(),
|
||||
channel: arg.channel,
|
||||
initialMessageId: arg.initialMessage?.id,
|
||||
child: ChannelPage(
|
||||
highlightInitialMessage: arg.initialMessage != null,
|
||||
),
|
||||
);
|
||||
});
|
||||
case Routes.NEW_CHAT:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
name: example
|
||||
description: A new Flutter project.
|
||||
version: 1.0.88+90
|
||||
version: 1.0.94+97
|
||||
|
||||
environment:
|
||||
sdk: ">=2.2.2 <3.0.0"
|
||||
|
||||
@@ -69,40 +69,26 @@ class ChannelBottomSheet extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
Divider(),
|
||||
StreamBuilder<bool>(
|
||||
stream: channel.isMutedStream,
|
||||
initialData: channel.isMuted,
|
||||
builder: (context, snapshot) {
|
||||
return ListTile(
|
||||
leading: StreamSvgIcon.mute(
|
||||
size: 22,
|
||||
color: StreamChatTheme.of(context).primaryIconTheme.color,
|
||||
),
|
||||
title: Text('Mute ${channel.isGroup ? 'group' : 'user'}'),
|
||||
trailing: Switch(
|
||||
onChanged: (bool muted) async {
|
||||
if (muted) {
|
||||
await channel.mute();
|
||||
} else {
|
||||
await channel.unmute();
|
||||
}
|
||||
},
|
||||
value: snapshot.data,
|
||||
),
|
||||
);
|
||||
}),
|
||||
Divider(),
|
||||
if (channel.isGroup && !channel.isDistinct)
|
||||
ListTile(
|
||||
leading: StreamSvgIcon.userRemove(
|
||||
size: 22,
|
||||
color: Colors.black,
|
||||
size: 24,
|
||||
color: Color(0xff7A7A7A),
|
||||
),
|
||||
title: Text(
|
||||
'Leave Group',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
title: Text('Leave Group'),
|
||||
onTap: () async {
|
||||
final confirm = await showConfirmationDialog(
|
||||
context,
|
||||
'Do you want to leave the group?',
|
||||
title: 'Leave Group',
|
||||
okText: 'LEAVE',
|
||||
question: 'Are you sure you want to leave this group?',
|
||||
cancelText: 'CANCEL',
|
||||
icon: StreamSvgIcon.userRemove(
|
||||
color: Colors.red,
|
||||
),
|
||||
);
|
||||
if (confirm == true) {
|
||||
await channel
|
||||
@@ -111,11 +97,17 @@ class ChannelBottomSheet extends StatelessWidget {
|
||||
}
|
||||
},
|
||||
),
|
||||
if (!channel.isGroup && !channel.isDistinct)
|
||||
if ([
|
||||
'admin',
|
||||
'owner',
|
||||
].contains(channel.state.members
|
||||
.firstWhere((m) => m.userId == channel.client.state.user.id,
|
||||
orElse: () => null)
|
||||
?.role))
|
||||
ListTile(
|
||||
leading: Icon(
|
||||
Icons.delete_outline,
|
||||
leading: StreamSvgIcon.delete(
|
||||
color: Color(0xFFFF3742),
|
||||
size: 24,
|
||||
),
|
||||
title: Text(
|
||||
'Delete chat',
|
||||
@@ -124,14 +116,22 @@ class ChannelBottomSheet extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
onTap: () async {
|
||||
final confirm = await showConfirmationDialog(
|
||||
final res = await showConfirmationDialog(
|
||||
context,
|
||||
'Do you want to delete the chat?',
|
||||
title: 'Delete Conversation',
|
||||
okText: 'DELETE',
|
||||
question:
|
||||
'Are you sure you want to delete this conversation?',
|
||||
cancelText: 'CANCEL',
|
||||
icon: StreamSvgIcon.delete(
|
||||
color: Colors.red,
|
||||
),
|
||||
);
|
||||
if (confirm == true) {
|
||||
await channel
|
||||
.removeMembers([StreamChat.of(context).user.id]);
|
||||
Navigator.pop(context);
|
||||
var channel = StreamChannel.of(context).channel;
|
||||
if (res == true) {
|
||||
await channel.delete().then((value) {
|
||||
Navigator.pop(context);
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
|
||||
@@ -5,8 +5,10 @@ import 'package:stream_chat_flutter/src/channel_info.dart';
|
||||
import 'package:stream_chat_flutter/src/channel_name.dart';
|
||||
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
|
||||
|
||||
import '../stream_chat_flutter.dart';
|
||||
import './channel_name.dart';
|
||||
import 'channel_image.dart';
|
||||
import 'chat_info_screen.dart';
|
||||
import 'stream_channel.dart';
|
||||
|
||||
/// 
|
||||
@@ -97,7 +99,33 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||
padding: const EdgeInsets.only(right: 10.0),
|
||||
child: Center(
|
||||
child: ChannelImage(
|
||||
onTap: onImageTap,
|
||||
onTap: onImageTap ??
|
||||
() async {
|
||||
if (channel.memberCount == 2 && channel.isDistinct) {
|
||||
final currentUser = StreamChat.of(context).user;
|
||||
final otherUser = channel.state.members.firstWhere(
|
||||
(element) => element.user.id != currentUser.id,
|
||||
orElse: () => null,
|
||||
);
|
||||
if (otherUser != null) {
|
||||
final pop = await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => StreamChannel(
|
||||
channel: channel,
|
||||
child: ChatInfoScreen(
|
||||
user: otherUser.user,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
if (pop == true) {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -524,44 +524,33 @@ class _ChannelListViewState extends State<ChannelListView>
|
||||
);
|
||||
},
|
||||
),
|
||||
IconSlideAction(
|
||||
color: backgroundColor,
|
||||
iconWidget: StreamSvgIcon.mute(),
|
||||
onTap: () async {
|
||||
if (!channel.isMuted) {
|
||||
await channel.mute();
|
||||
} else {
|
||||
await channel.unmute();
|
||||
}
|
||||
},
|
||||
),
|
||||
if (channel.isGroup && !channel.isDistinct)
|
||||
if ([
|
||||
'admin',
|
||||
'owner',
|
||||
].contains(channel.state.members
|
||||
.firstWhere(
|
||||
(m) => m.userId == channel.client.state.user.id,
|
||||
orElse: () => null)
|
||||
?.role))
|
||||
IconSlideAction(
|
||||
color: backgroundColor,
|
||||
iconWidget: StreamSvgIcon.userRemove(),
|
||||
iconWidget: StreamSvgIcon.delete(
|
||||
color: Color(0xFFFF3742),
|
||||
),
|
||||
onTap: () async {
|
||||
final confirm = await showConfirmationDialog(
|
||||
final res = await showConfirmationDialog(
|
||||
context,
|
||||
'Do you want to leave the group?',
|
||||
title: 'Delete Conversation',
|
||||
okText: 'DELETE',
|
||||
question:
|
||||
'Are you sure you want to delete this conversation?',
|
||||
cancelText: 'CANCEL',
|
||||
icon: StreamSvgIcon.delete(
|
||||
color: Color(0xFFFF3742),
|
||||
),
|
||||
);
|
||||
if (confirm == true) {
|
||||
await channel
|
||||
.removeMembers([StreamChat.of(context).user.id]);
|
||||
}
|
||||
},
|
||||
),
|
||||
if (!channel.isGroup && !channel.isDistinct)
|
||||
IconSlideAction(
|
||||
color: backgroundColor,
|
||||
icon: Icons.delete_outline,
|
||||
onTap: () async {
|
||||
final confirm = await showConfirmationDialog(
|
||||
context,
|
||||
'Do you want to delete the chat?',
|
||||
);
|
||||
if (confirm == true) {
|
||||
await channel
|
||||
.removeMembers([StreamChat.of(context).user.id]);
|
||||
if (res == true) {
|
||||
await channel.delete();
|
||||
}
|
||||
},
|
||||
),
|
||||
|
||||
@@ -7,6 +7,7 @@ import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
|
||||
import '../stream_chat_flutter.dart';
|
||||
import 'channel_name.dart';
|
||||
import 'channel_unread_indicator.dart';
|
||||
import 'chat_info_screen.dart';
|
||||
|
||||
/// 
|
||||
/// 
|
||||
@@ -60,7 +61,29 @@ class ChannelPreview extends StatelessWidget {
|
||||
}
|
||||
},
|
||||
leading: ChannelImage(
|
||||
onTap: onImageTap,
|
||||
onTap: onImageTap ??
|
||||
() {
|
||||
if (channel.memberCount == 2 && channel.isDistinct) {
|
||||
final currentUser = StreamChat.of(context).user;
|
||||
final otherUser = channel.state.members.firstWhere(
|
||||
(element) => element.user.id != currentUser.id,
|
||||
orElse: () => null,
|
||||
);
|
||||
if (otherUser != null) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => StreamChannel(
|
||||
channel: channel,
|
||||
child: ChatInfoScreen(
|
||||
user: otherUser.user,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
title: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
|
||||
@@ -85,7 +85,7 @@ class ChannelsBlocState extends State<ChannelsBloc>
|
||||
paginationParams.offset == null ||
|
||||
paginationParams.offset == 0;
|
||||
final oldChannels = List<Channel>.from(channels ?? []);
|
||||
client
|
||||
await client
|
||||
.queryChannels(
|
||||
filter: filter,
|
||||
sort: sortOptions,
|
||||
@@ -93,21 +93,18 @@ class ChannelsBlocState extends State<ChannelsBloc>
|
||||
paginationParams: paginationParams,
|
||||
onlyOffline: onlyOffline,
|
||||
)
|
||||
.listen((channels) {
|
||||
.then((channels) {
|
||||
if (clear) {
|
||||
_channelsController.add(channels);
|
||||
} else {
|
||||
final l = oldChannels + channels;
|
||||
_channelsController.add(l);
|
||||
}
|
||||
}, onDone: () {
|
||||
_queryChannelsLoadingController.sink.add(false);
|
||||
}, onError: (err, stackTrace) {
|
||||
print(err);
|
||||
print(stackTrace);
|
||||
_queryChannelsLoadingController.addError(err, stackTrace);
|
||||
});
|
||||
} catch (err, stackTrace) {
|
||||
print(err);
|
||||
print(stackTrace);
|
||||
_queryChannelsLoadingController.addError(err, stackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,574 @@
|
||||
import 'package:emojis/emojis.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:jiffy/jiffy.dart';
|
||||
|
||||
import '../stream_chat_flutter.dart';
|
||||
|
||||
/// Detail screen for a 1:1 chat correspondence
|
||||
class ChatInfoScreen extends StatefulWidget {
|
||||
/// User in consideration
|
||||
final User user;
|
||||
|
||||
const ChatInfoScreen({Key key, this.user}) : super(key: key);
|
||||
|
||||
@override
|
||||
_ChatInfoScreenState createState() => _ChatInfoScreenState();
|
||||
}
|
||||
|
||||
class _ChatInfoScreenState extends State<ChatInfoScreen> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final channel = StreamChannel.of(context).channel;
|
||||
return Scaffold(
|
||||
backgroundColor: Color(0xFFe6e6e6),
|
||||
body: ListView(
|
||||
children: [
|
||||
_buildUserHeader(),
|
||||
SizedBox(
|
||||
height: 8.0,
|
||||
),
|
||||
_buildOptionListTiles(),
|
||||
SizedBox(
|
||||
height: 8.0,
|
||||
),
|
||||
if ([
|
||||
'admin',
|
||||
'owner',
|
||||
].contains(channel.state.members
|
||||
.firstWhere((m) => m.userId == channel.client.state.user.id,
|
||||
orElse: () => null)
|
||||
?.role))
|
||||
_buildDeleteListTile(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildUserHeader() {
|
||||
return Material(
|
||||
color: Colors.white,
|
||||
child: SafeArea(
|
||||
child: Stack(
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: UserAvatar(
|
||||
user: widget.user,
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: 72.0,
|
||||
maxHeight: 72.0,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(36.0),
|
||||
showOnlineStatus: false,
|
||||
),
|
||||
),
|
||||
//SizedBox(height: 4.0),
|
||||
Text(
|
||||
widget.user.name,
|
||||
style: TextStyle(fontSize: 16.0, fontWeight: FontWeight.bold),
|
||||
),
|
||||
SizedBox(height: 7.0),
|
||||
_buildConnectedTitleState(),
|
||||
SizedBox(height: 15.0),
|
||||
_OptionListTile(
|
||||
title: '@${widget.user.id}',
|
||||
trailing: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
child: Text(
|
||||
widget.user.name,
|
||||
style: TextStyle(
|
||||
color: Colors.black.withOpacity(0.5), fontSize: 16.0),
|
||||
),
|
||||
),
|
||||
onTap: () {},
|
||||
),
|
||||
],
|
||||
),
|
||||
Positioned(
|
||||
top: 21,
|
||||
left: 16,
|
||||
child: InkWell(
|
||||
child: StreamSvgIcon.left(),
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildOptionListTiles() {
|
||||
var channel = StreamChannel.of(context);
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
// _OptionListTile(
|
||||
// title: 'Notifications',
|
||||
// leading: StreamSvgIcon.Icon_notification(
|
||||
// size: 24.0,
|
||||
// color: Colors.black.withOpacity(0.5),
|
||||
// ),
|
||||
// trailing: CupertinoSwitch(
|
||||
// value: true,
|
||||
// onChanged: (val) {},
|
||||
// ),
|
||||
// onTap: () {},
|
||||
// ),
|
||||
StreamBuilder<bool>(
|
||||
stream: StreamChannel.of(context).channel.isMutedStream,
|
||||
builder: (context, snapshot) {
|
||||
return _OptionListTile(
|
||||
title: 'Mute user',
|
||||
leading: StreamSvgIcon.mute(
|
||||
size: 23.0,
|
||||
color: Colors.black.withOpacity(0.5),
|
||||
),
|
||||
trailing: snapshot.data == null
|
||||
? CircularProgressIndicator()
|
||||
: CupertinoSwitch(
|
||||
value: snapshot.data,
|
||||
onChanged: (val) {
|
||||
if (snapshot.data) {
|
||||
channel.channel.unmute();
|
||||
} else {
|
||||
channel.channel.mute();
|
||||
}
|
||||
},
|
||||
),
|
||||
onTap: () {},
|
||||
);
|
||||
}),
|
||||
// _OptionListTile(
|
||||
// title: 'Block User',
|
||||
// leading: StreamSvgIcon.Icon_user_delete(
|
||||
// size: 24.0,
|
||||
// color: Colors.black.withOpacity(0.5),
|
||||
// ),
|
||||
// trailing: CupertinoSwitch(
|
||||
// value: widget.user.banned,
|
||||
// onChanged: (val) {
|
||||
// if (widget.user.banned) {
|
||||
// channel.channel.shadowBan(widget.user.id, {});
|
||||
// } else {
|
||||
// channel.channel.unbanUser(widget.user.id);
|
||||
// }
|
||||
// },
|
||||
// ),
|
||||
// onTap: () {},
|
||||
// ),
|
||||
_OptionListTile(
|
||||
title: 'Photos & Videos',
|
||||
leading: StreamSvgIcon.pictures(
|
||||
size: 32.0,
|
||||
color: Colors.black.withOpacity(0.5),
|
||||
),
|
||||
trailing: StreamSvgIcon.right(),
|
||||
onTap: () {
|
||||
Navigator.push(context,
|
||||
MaterialPageRoute(builder: (context) => _MediaDisplayScreen()));
|
||||
},
|
||||
),
|
||||
_OptionListTile(
|
||||
title: 'Files',
|
||||
leading: StreamSvgIcon.files(
|
||||
size: 32.0,
|
||||
color: Colors.black.withOpacity(0.5),
|
||||
),
|
||||
trailing: StreamSvgIcon.right(),
|
||||
onTap: () {
|
||||
Navigator.push(context,
|
||||
MaterialPageRoute(builder: (context) => _FileDisplayScreen()));
|
||||
},
|
||||
),
|
||||
_OptionListTile(
|
||||
title: 'Shared groups',
|
||||
leading: StreamSvgIcon.Icon_group(
|
||||
size: 24.0,
|
||||
color: Colors.black.withOpacity(0.5),
|
||||
),
|
||||
trailing: StreamSvgIcon.right(),
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => _SharedGroupsScreen(
|
||||
StreamChat.of(context).user, widget.user)));
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDeleteListTile() {
|
||||
return _OptionListTile(
|
||||
title: 'Delete',
|
||||
leading: StreamSvgIcon.delete(
|
||||
color: Colors.red,
|
||||
size: 24.0,
|
||||
),
|
||||
onTap: () {
|
||||
_showDeleteDialog();
|
||||
},
|
||||
titleColor: Colors.red,
|
||||
);
|
||||
}
|
||||
|
||||
void _showDeleteDialog() async {
|
||||
final res = await showConfirmationDialog(
|
||||
context,
|
||||
title: 'Delete Conversation',
|
||||
okText: 'DELETE',
|
||||
question: 'Are you sure you want to delete this conversation?',
|
||||
cancelText: 'CANCEL',
|
||||
icon: StreamSvgIcon.delete(
|
||||
color: Colors.red,
|
||||
),
|
||||
);
|
||||
var channel = StreamChannel.of(context).channel;
|
||||
if (res == true) {
|
||||
await channel.delete().then((value) {
|
||||
Navigator.pop(context);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildConnectedTitleState() {
|
||||
var alternativeWidget;
|
||||
|
||||
final otherMember = widget.user;
|
||||
|
||||
if (otherMember != null) {
|
||||
if (otherMember.online) {
|
||||
alternativeWidget = Text(
|
||||
'Online',
|
||||
style: TextStyle(color: Colors.black.withOpacity(0.5)),
|
||||
);
|
||||
} else {
|
||||
alternativeWidget = Text(
|
||||
'Last seen ${Jiffy(otherMember.lastActive).fromNow()}',
|
||||
style: TextStyle(color: Colors.black.withOpacity(0.5)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
if (widget.user.online)
|
||||
Material(
|
||||
type: MaterialType.circle,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
constraints: BoxConstraints.tightFor(
|
||||
width: 28,
|
||||
height: 12,
|
||||
),
|
||||
child: Material(
|
||||
shape: CircleBorder(),
|
||||
color: Color(0xff20E070),
|
||||
),
|
||||
),
|
||||
color: Colors.white,
|
||||
),
|
||||
alternativeWidget,
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _OptionListTile extends StatelessWidget {
|
||||
final String title;
|
||||
final StreamSvgIcon leading;
|
||||
final Widget trailing;
|
||||
final VoidCallback onTap;
|
||||
final Color titleColor;
|
||||
|
||||
_OptionListTile({
|
||||
this.title,
|
||||
this.leading,
|
||||
this.trailing,
|
||||
this.onTap,
|
||||
this.titleColor,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
Container(
|
||||
color: Color(0xffe6e6e6),
|
||||
height: 2.0,
|
||||
),
|
||||
Material(
|
||||
color: Colors.white,
|
||||
child: Container(
|
||||
height: 56.0,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
child: Row(
|
||||
children: [
|
||||
if (leading != null)
|
||||
Expanded(
|
||||
child: Center(child: leading),
|
||||
),
|
||||
if (leading == null)
|
||||
SizedBox(
|
||||
width: 16.0,
|
||||
),
|
||||
Expanded(
|
||||
flex: 4,
|
||||
child: Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600, color: titleColor),
|
||||
)),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(right: 16.0),
|
||||
child: Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: trailing ?? Container(),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SharedGroupsScreen extends StatefulWidget {
|
||||
final User mainUser;
|
||||
final User otherUser;
|
||||
|
||||
_SharedGroupsScreen(this.mainUser, this.otherUser);
|
||||
|
||||
@override
|
||||
__SharedGroupsScreenState createState() => __SharedGroupsScreenState();
|
||||
}
|
||||
|
||||
class __SharedGroupsScreenState extends State<_SharedGroupsScreen> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var chat = StreamChat.of(context);
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
appBar: AppBar(
|
||||
brightness: Theme.of(context).brightness,
|
||||
elevation: 1,
|
||||
centerTitle: true,
|
||||
title: Text(
|
||||
'Shared Groups',
|
||||
style: TextStyle(color: Colors.black, fontSize: 16.0),
|
||||
),
|
||||
leading: Center(
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Container(
|
||||
child: StreamSvgIcon.left(
|
||||
color: Colors.black,
|
||||
size: 24.0,
|
||||
),
|
||||
width: 24.0,
|
||||
height: 24.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
backgroundColor: StreamChatTheme.of(context).primaryColor,
|
||||
),
|
||||
body: FutureBuilder<List<Channel>>(
|
||||
future: chat.client.queryChannels(
|
||||
filter: {
|
||||
r'$and': [
|
||||
{
|
||||
'members': {
|
||||
r'$in': [widget.otherUser.id],
|
||||
},
|
||||
},
|
||||
{
|
||||
'members': {
|
||||
r'$in': [widget.mainUser.id],
|
||||
},
|
||||
}
|
||||
],
|
||||
},
|
||||
),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.data == null) {
|
||||
return Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
}
|
||||
|
||||
return ListView.builder(
|
||||
itemCount: snapshot.data.length,
|
||||
itemBuilder: (context, position) {
|
||||
return StreamChannel(
|
||||
channel: snapshot.data[position],
|
||||
child: _buildListTile(snapshot.data[position]),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildListTile(Channel channel) {
|
||||
var extraData = channel.extraData;
|
||||
var members = channel.state.members;
|
||||
|
||||
var textStyle = TextStyle(fontSize: 14.0, fontWeight: FontWeight.bold);
|
||||
|
||||
return Container(
|
||||
height: 64.0,
|
||||
child: LayoutBuilder(builder: (context, constraints) {
|
||||
String title;
|
||||
if (extraData['name'] == null) {
|
||||
final otherMembers = members.where(
|
||||
(member) => member.userId != StreamChat.of(context).user.id);
|
||||
if (otherMembers.isNotEmpty) {
|
||||
final maxWidth = constraints.maxWidth;
|
||||
final maxChars = maxWidth / textStyle.fontSize;
|
||||
var currentChars = 0;
|
||||
final currentMembers = <Member>[];
|
||||
otherMembers.forEach((element) {
|
||||
final newLength = currentChars + element.user.name.length;
|
||||
if (newLength < maxChars) {
|
||||
currentChars = newLength;
|
||||
currentMembers.add(element);
|
||||
}
|
||||
});
|
||||
|
||||
final exceedingMembers =
|
||||
otherMembers.length - currentMembers.length;
|
||||
title =
|
||||
'${currentMembers.map((e) => e.user.name).join(', ')} ${exceedingMembers > 0 ? '+ $exceedingMembers' : ''}';
|
||||
} else {
|
||||
title = 'No title';
|
||||
}
|
||||
} else {
|
||||
title = extraData['name'];
|
||||
}
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Row(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: ChannelImage(
|
||||
channel: channel,
|
||||
constraints:
|
||||
BoxConstraints(maxWidth: 40.0, maxHeight: 40.0),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
title,
|
||||
style: textStyle,
|
||||
)),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text(
|
||||
'${channel.memberCount} members',
|
||||
style: TextStyle(color: Colors.black.withOpacity(0.5)),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(
|
||||
height: 1.0,
|
||||
color: Color(0xffe6e6e6),
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _MediaDisplayScreen extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
appBar: AppBar(
|
||||
brightness: Theme.of(context).brightness,
|
||||
elevation: 1,
|
||||
centerTitle: true,
|
||||
title: Text(
|
||||
'Photos & Videos',
|
||||
style: TextStyle(color: Colors.black, fontSize: 16.0),
|
||||
),
|
||||
leading: Center(
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Container(
|
||||
child: StreamSvgIcon.left(
|
||||
color: Colors.black,
|
||||
size: 24.0,
|
||||
),
|
||||
width: 24.0,
|
||||
height: 24.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
backgroundColor: StreamChatTheme.of(context).primaryColor,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _FileDisplayScreen extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
appBar: AppBar(
|
||||
brightness: Theme.of(context).brightness,
|
||||
elevation: 1,
|
||||
centerTitle: true,
|
||||
title: Text(
|
||||
'Files',
|
||||
style: TextStyle(color: Colors.black, fontSize: 16.0),
|
||||
),
|
||||
leading: Center(
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Container(
|
||||
child: StreamSvgIcon.left(
|
||||
color: Colors.black,
|
||||
size: 24.0,
|
||||
),
|
||||
width: 24.0,
|
||||
height: 24.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
backgroundColor: StreamChatTheme.of(context).primaryColor,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -3,3 +3,12 @@ extension StringExtension on String {
|
||||
return "${this[0].toUpperCase()}${this.substring(1)}";
|
||||
}
|
||||
}
|
||||
|
||||
/// 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);
|
||||
}
|
||||
|
||||
+122
-41
@@ -1,38 +1,79 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat/stream_chat.dart';
|
||||
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
|
||||
import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
|
||||
import 'package:stream_chat_flutter/src/utils.dart';
|
||||
import 'package:video_compress/video_compress.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
import 'media_utils.dart';
|
||||
|
||||
class FileAttachment extends StatelessWidget {
|
||||
enum FileAttachmentType { local, online }
|
||||
|
||||
class FileAttachment extends StatefulWidget {
|
||||
final Attachment attachment;
|
||||
final Size size;
|
||||
final Widget trailing;
|
||||
final FileAttachmentType attachmentType;
|
||||
final PlatformFile file;
|
||||
|
||||
const FileAttachment({
|
||||
Key key,
|
||||
@required this.attachment,
|
||||
this.size,
|
||||
this.trailing,
|
||||
this.attachmentType = FileAttachmentType.online,
|
||||
this.file,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
_FileAttachmentState createState() => _FileAttachmentState();
|
||||
}
|
||||
|
||||
class _FileAttachmentState extends State<FileAttachment> {
|
||||
VideoPlayerController _controller;
|
||||
Future<void> _initializeVideoPlayerFuture;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (MediaUtils.getMimeType(widget.attachment.title).type == 'video') {
|
||||
if (widget.attachmentType == FileAttachmentType.online) {
|
||||
_controller = VideoPlayerController.network(
|
||||
widget.attachment.assetUrl,
|
||||
);
|
||||
} else {
|
||||
_controller = VideoPlayerController.file(
|
||||
File.fromRawPath(widget.file.bytes),
|
||||
);
|
||||
}
|
||||
|
||||
_initializeVideoPlayerFuture = _controller.initialize();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Material(
|
||||
child: Container(
|
||||
width: size?.width ?? 100,
|
||||
width: widget.size?.width ?? 100,
|
||||
height: 56.0,
|
||||
margin: trailing != null ? EdgeInsets.only(top: 4.0) : null,
|
||||
margin: widget.trailing != null ? EdgeInsets.only(top: 4.0) : null,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: trailing != null ? BorderRadius.circular(16.0) : null,
|
||||
border: trailing != null
|
||||
borderRadius:
|
||||
widget.trailing != null ? BorderRadius.circular(16.0) : null,
|
||||
border: widget.trailing != null
|
||||
? Border.fromBorderSide(BorderSide(color: Color(0xFFE6E6E6)))
|
||||
: null,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
child: getFileTypeImage(attachment.extraData['mime_type']),
|
||||
child: _getFileTypeImage(),
|
||||
height: 40.0,
|
||||
width: 33.33,
|
||||
margin: EdgeInsets.all(8.0),
|
||||
@@ -46,7 +87,7 @@ class FileAttachment extends StatelessWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
attachment?.title ?? 'File',
|
||||
widget.attachment?.title ?? 'File',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 14.0,
|
||||
@@ -58,7 +99,7 @@ class FileAttachment extends StatelessWidget {
|
||||
height: 3.0,
|
||||
),
|
||||
Text(
|
||||
'${attachment.extraData['file_size'] ?? 'N/A'} bytes',
|
||||
'${getSizeText(widget.attachment.extraData['file_size'])}',
|
||||
style: TextStyle(
|
||||
color: Colors.black.withOpacity(0.5),
|
||||
fontSize: 14.0,
|
||||
@@ -69,51 +110,91 @@ class FileAttachment extends StatelessWidget {
|
||||
),
|
||||
Column(
|
||||
children: [
|
||||
trailing ??
|
||||
widget.trailing ??
|
||||
IconButton(
|
||||
icon: StreamSvgIcon.cloud_download(
|
||||
color: Colors.black,
|
||||
),
|
||||
onPressed: () {
|
||||
launchURL(context, attachment.assetUrl);
|
||||
launchURL(context, widget.attachment.assetUrl);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// ListTile(
|
||||
// dense: true,
|
||||
// leading: Container(
|
||||
// child: _getFileTypeImage(attachment.extraData['mime_type']),
|
||||
// height: 40.0,
|
||||
// width: 33.33,
|
||||
// ),
|
||||
// title: Text(
|
||||
// attachment?.title ?? 'File',
|
||||
// style: TextStyle(
|
||||
// fontWeight: FontWeight.bold,
|
||||
// ),
|
||||
// maxLines: 3,
|
||||
// ),
|
||||
// subtitle: Text(
|
||||
// '${attachment.extraData['file_size'] ?? 'N/A'} bytes',
|
||||
// style: TextStyle(
|
||||
// color: Colors.black.withOpacity(0.5),
|
||||
// ),
|
||||
// ),
|
||||
// trailing: trailing ??
|
||||
// IconButton(
|
||||
// icon: StreamSvgIcon.cloud_download(
|
||||
// color: Colors.black,
|
||||
// ),
|
||||
// onPressed: () {
|
||||
// launchURL(context, attachment.assetUrl);
|
||||
// },
|
||||
// ),
|
||||
// ),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _getFileTypeImage() {
|
||||
if ((MediaUtils.getMimeType(widget.attachment.title).type == 'image')) {
|
||||
switch (widget.attachmentType) {
|
||||
case FileAttachmentType.local:
|
||||
return Image.memory(
|
||||
widget.file.bytes,
|
||||
fit: BoxFit.cover,
|
||||
);
|
||||
break;
|
||||
case FileAttachmentType.online:
|
||||
return CachedNetworkImage(
|
||||
imageUrl: widget.attachment.imageUrl ??
|
||||
widget.attachment.assetUrl ??
|
||||
widget.attachment.thumbUrl,
|
||||
fit: BoxFit.cover,
|
||||
progressIndicatorBuilder: (context, _, progress) {
|
||||
return Center(
|
||||
child: Container(
|
||||
width: 20.0,
|
||||
height: 20.0,
|
||||
child: CircularProgressIndicator(
|
||||
backgroundColor: StreamChatTheme.of(context).accentColor,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ((MediaUtils.getMimeType(widget.attachment.title).type == 'video')) {
|
||||
switch (widget.attachmentType) {
|
||||
case FileAttachmentType.local:
|
||||
return FutureBuilder<File>(
|
||||
future: VideoCompress.getFileThumbnail(widget.file.path),
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData) {
|
||||
return Image.asset(
|
||||
'images/placeholder.png',
|
||||
package: 'stream_chat_flutter',
|
||||
);
|
||||
}
|
||||
|
||||
return Image.file(
|
||||
snapshot.data,
|
||||
fit: BoxFit.cover,
|
||||
);
|
||||
},
|
||||
);
|
||||
break;
|
||||
case FileAttachmentType.online:
|
||||
return FutureBuilder(
|
||||
future: _initializeVideoPlayerFuture,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
return AspectRatio(
|
||||
aspectRatio: _controller.value.aspectRatio,
|
||||
child: VideoPlayer(_controller),
|
||||
);
|
||||
} else {
|
||||
return Center(child: CircularProgressIndicator());
|
||||
}
|
||||
},
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return getFileTypeImage(widget.attachment.extraData['mime_type']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,33 +1,47 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
enum LoadingStatus { LOADING, STABLE }
|
||||
enum _LoadingStatus { LOADING, STABLE }
|
||||
|
||||
/// Signature for EndOfPageListeners
|
||||
typedef EndOfPageListenerCallback = void Function();
|
||||
|
||||
/// A widget that wraps a [Widget] and will trigger [onEndOfPage] when it
|
||||
/// reaches the bottom of the list
|
||||
/// A widget that wraps a [Widget] and will trigger [onEndOfPage]/[onStartOfPage] when it
|
||||
/// reaches the bottom/start of the list
|
||||
class LazyLoadScrollView extends StatefulWidget {
|
||||
/// The [Widget] that this widget watches for changes on
|
||||
final Widget child;
|
||||
|
||||
/// Called when the [child] reaches the end of the list
|
||||
final EndOfPageListenerCallback onEndOfPage;
|
||||
/// Called when the [child] reaches the start of the list
|
||||
final AsyncCallback onStartOfPage;
|
||||
|
||||
/// The offset to take into account when triggering [onEndOfPage] in pixels
|
||||
final int scrollOffset;
|
||||
/// Called when the [child] reaches the end of the list
|
||||
final AsyncCallback onEndOfPage;
|
||||
|
||||
/// Called when the list scrolling starts
|
||||
final VoidCallback onPageScrollStart;
|
||||
|
||||
/// Called when the list scrolling ends
|
||||
final VoidCallback onPageScrollEnd;
|
||||
|
||||
/// Called every time the [child] is in-between the list
|
||||
final VoidCallback onInBetweenOfPage;
|
||||
|
||||
/// The offset to take into account when triggering [onEndOfPage]/[onStartOfPage] in pixels
|
||||
final double scrollOffset;
|
||||
|
||||
/// Used to determine if loading of new data has finished. You should use set this if you aren't using a FutureBuilder or StreamBuilder
|
||||
final bool isLoading;
|
||||
|
||||
/// Initiates a LazyLoadScrollView widget
|
||||
LazyLoadScrollView({
|
||||
Key key,
|
||||
@required this.child,
|
||||
@required this.onEndOfPage,
|
||||
this.onStartOfPage,
|
||||
this.onEndOfPage,
|
||||
this.onPageScrollStart,
|
||||
this.onPageScrollEnd,
|
||||
this.onInBetweenOfPage,
|
||||
this.isLoading = false,
|
||||
this.scrollOffset = 100,
|
||||
}) : assert(onEndOfPage != null),
|
||||
assert(child != null),
|
||||
}) : assert(child != null),
|
||||
super(key: key);
|
||||
|
||||
@override
|
||||
@@ -35,15 +49,8 @@ class LazyLoadScrollView extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _LazyLoadScrollViewState extends State<LazyLoadScrollView> {
|
||||
LoadingStatus _loadMoreStatus = LoadingStatus.STABLE;
|
||||
|
||||
@override
|
||||
void didUpdateWidget(LazyLoadScrollView oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (!widget.isLoading) {
|
||||
_loadMoreStatus = LoadingStatus.STABLE;
|
||||
}
|
||||
}
|
||||
_LoadingStatus _loadMoreStatus = _LoadingStatus.STABLE;
|
||||
double _scrollPosition = 0.0;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -54,28 +61,88 @@ class _LazyLoadScrollViewState extends State<LazyLoadScrollView> {
|
||||
}
|
||||
|
||||
bool _onNotification(Notification notification) {
|
||||
if (notification is ScrollStartNotification) {
|
||||
if (widget.onPageScrollStart != null) {
|
||||
widget.onPageScrollStart();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (notification is ScrollEndNotification) {
|
||||
if (widget.onPageScrollEnd != null) {
|
||||
widget.onPageScrollEnd();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (notification is ScrollUpdateNotification) {
|
||||
if (notification.metrics.maxScrollExtent > notification.metrics.pixels &&
|
||||
notification.metrics.maxScrollExtent - notification.metrics.pixels <=
|
||||
widget.scrollOffset) {
|
||||
if (_loadMoreStatus != null &&
|
||||
_loadMoreStatus == LoadingStatus.STABLE) {
|
||||
_loadMoreStatus = LoadingStatus.LOADING;
|
||||
widget.onEndOfPage();
|
||||
final pixels = notification.metrics.pixels;
|
||||
final maxScrollExtent = notification.metrics.maxScrollExtent;
|
||||
final minScrollExtent = notification.metrics.minScrollExtent;
|
||||
final scrollOffset = widget.scrollOffset;
|
||||
|
||||
if (pixels > (minScrollExtent + scrollOffset) &&
|
||||
pixels < (maxScrollExtent - scrollOffset)) {
|
||||
if (widget.onInBetweenOfPage != null) {
|
||||
widget.onInBetweenOfPage();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
final extentBefore = notification.metrics.extentBefore;
|
||||
final extentAfter = notification.metrics.extentAfter;
|
||||
final scrollingDown = _scrollPosition < pixels;
|
||||
|
||||
if (scrollOffset == null || scrollOffset == 0) {
|
||||
if (extentAfter == 0) {
|
||||
_onEndOfPage();
|
||||
}
|
||||
if (extentBefore == 0) {
|
||||
_onStartOfPage();
|
||||
}
|
||||
} else {
|
||||
if (scrollingDown) {
|
||||
if (extentAfter <= scrollOffset) {
|
||||
_onEndOfPage();
|
||||
}
|
||||
} else {
|
||||
if (extentBefore <= scrollOffset) {
|
||||
_onStartOfPage();
|
||||
}
|
||||
}
|
||||
}
|
||||
_scrollPosition = pixels;
|
||||
return true;
|
||||
}
|
||||
if (notification is OverscrollNotification) {
|
||||
if (notification.overscroll > 0) {
|
||||
if (_loadMoreStatus != null &&
|
||||
_loadMoreStatus == LoadingStatus.STABLE) {
|
||||
_loadMoreStatus = LoadingStatus.LOADING;
|
||||
widget.onEndOfPage();
|
||||
}
|
||||
_onEndOfPage();
|
||||
}
|
||||
if (notification.overscroll < 0) {
|
||||
_onStartOfPage();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void _onEndOfPage() {
|
||||
if (_loadMoreStatus != null && _loadMoreStatus == _LoadingStatus.STABLE) {
|
||||
_loadMoreStatus = _LoadingStatus.LOADING;
|
||||
if (widget.onEndOfPage != null) {
|
||||
widget.onEndOfPage().whenComplete(() {
|
||||
_loadMoreStatus = _LoadingStatus.STABLE;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _onStartOfPage() {
|
||||
if (_loadMoreStatus != null && _loadMoreStatus == _LoadingStatus.STABLE) {
|
||||
_loadMoreStatus = _LoadingStatus.LOADING;
|
||||
if (widget.onStartOfPage != null) {
|
||||
widget.onStartOfPage().whenComplete(() {
|
||||
_loadMoreStatus = _LoadingStatus.STABLE;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -180,7 +180,7 @@ class MediaThumbnailProvider extends ImageProvider<MediaThumbnailProvider> {
|
||||
MediaThumbnailProvider key, DecoderCallback decode) async {
|
||||
assert(key == this);
|
||||
final bytes = await media.thumbData;
|
||||
if (bytes.isEmpty) return null;
|
||||
if (bytes?.isNotEmpty != true) return null;
|
||||
|
||||
return await decode(bytes);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import 'package:http_parser/http_parser.dart' as httpParser;
|
||||
import 'package:mime/mime.dart';
|
||||
|
||||
class MediaUtils {
|
||||
static httpParser.MediaType getMimeType(String filename) {
|
||||
httpParser.MediaType mimeType;
|
||||
if (filename != null) {
|
||||
if (filename.toLowerCase().endsWith('heic')) {
|
||||
mimeType = httpParser.MediaType.parse('image/heic');
|
||||
} else {
|
||||
mimeType = httpParser.MediaType.parse(lookupMimeType(filename));
|
||||
}
|
||||
}
|
||||
|
||||
return mimeType;
|
||||
}
|
||||
}
|
||||
@@ -117,12 +117,13 @@ class MessageActionsModal extends StatelessWidget {
|
||||
messageTheme: messageTheme,
|
||||
showReactions: false,
|
||||
showUsername: false,
|
||||
showReplyIndicator: false,
|
||||
showThreadReplyIndicator: false,
|
||||
showReplyIndicator: false,
|
||||
showUserAvatar: showUserAvatar,
|
||||
showTimestamp: false,
|
||||
translateUserAvatar: false,
|
||||
showReactionPickerIndicator: true,
|
||||
showInChannelIndicator: false,
|
||||
showSendingIndicator: DisplayWidget.gone,
|
||||
shape: messageShape,
|
||||
),
|
||||
|
||||
+126
-59
@@ -343,7 +343,7 @@ class MessageInputState extends State<MessageInput> {
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: Text('Send also as direct message'),
|
||||
child: Text('Also send as direct message'),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -384,7 +384,10 @@ class MessageInputState extends State<MessageInput> {
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
if (!widget.disableAttachments) _buildAttachmentButton(),
|
||||
if (widget.editMessage == null) _buildCommandButton(),
|
||||
if (widget.editMessage == null &&
|
||||
StreamChannel.of(context).channel?.config?.commands?.isNotEmpty ==
|
||||
true)
|
||||
_buildCommandButton(),
|
||||
],
|
||||
),
|
||||
duration: Duration(milliseconds: 300),
|
||||
@@ -398,11 +401,12 @@ class MessageInputState extends State<MessageInput> {
|
||||
child: Container(
|
||||
clipBehavior: Clip.antiAlias,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(20.0),
|
||||
borderRadius: BorderRadius.circular(24.0),
|
||||
border: Border.all(
|
||||
color: Colors.black.withOpacity(0.16),
|
||||
),
|
||||
),
|
||||
padding: _attachments.isEmpty ? null : EdgeInsets.all(6.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -450,19 +454,30 @@ class MessageInputState extends State<MessageInput> {
|
||||
child: Chip(
|
||||
backgroundColor:
|
||||
StreamChatTheme.of(context).accentColor,
|
||||
label: Text(
|
||||
_chosenCommand?.name ?? "",
|
||||
style: TextStyle(color: Colors.white),
|
||||
),
|
||||
avatar: StreamSvgIcon.lightning(
|
||||
color: Colors.white,
|
||||
padding: EdgeInsets.zero,
|
||||
labelPadding:
|
||||
EdgeInsets.symmetric(horizontal: 9.0),
|
||||
label: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
StreamSvgIcon.lightning(
|
||||
color: Colors.white,
|
||||
size: 16.0,
|
||||
),
|
||||
Text(
|
||||
_chosenCommand?.name?.toUpperCase() ?? "",
|
||||
style: TextStyle(
|
||||
color: Colors.white, fontSize: 12.0),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
: null,
|
||||
suffixIcon: _commandEnabled
|
||||
? IconButton(
|
||||
icon: Icon(Icons.cancel_outlined),
|
||||
icon: StreamSvgIcon.close_small(),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_commandEnabled = false;
|
||||
@@ -470,6 +485,10 @@ class MessageInputState extends State<MessageInput> {
|
||||
},
|
||||
)
|
||||
: null,
|
||||
suffixIconConstraints: BoxConstraints(
|
||||
maxHeight: 24.0,
|
||||
maxWidth: 40.0,
|
||||
),
|
||||
),
|
||||
textCapitalization: TextCapitalization.sentences,
|
||||
),
|
||||
@@ -567,11 +586,12 @@ class MessageInputState extends State<MessageInput> {
|
||||
void _checkCommands(String s, BuildContext context) {
|
||||
if (s.startsWith('/')) {
|
||||
var matchedCommandsList = StreamChannel.of(context)
|
||||
.channel
|
||||
.config
|
||||
.commands
|
||||
.where((element) => element.name == s.substring(1))
|
||||
.toList();
|
||||
.channel
|
||||
.config
|
||||
?.commands
|
||||
?.where((element) => element.name == s.substring(1))
|
||||
?.toList() ??
|
||||
[];
|
||||
|
||||
if (matchedCommandsList.length == 1) {
|
||||
_chosenCommand = matchedCommandsList[0];
|
||||
@@ -592,11 +612,12 @@ class MessageInputState extends State<MessageInput> {
|
||||
OverlayEntry _buildCommandsOverlayEntry() {
|
||||
final text = textEditingController.text.trimLeft();
|
||||
final commands = StreamChannel.of(context)
|
||||
.channel
|
||||
.config
|
||||
.commands
|
||||
.where((c) => c.name.contains(text.replaceFirst('/', '')))
|
||||
.toList();
|
||||
.channel
|
||||
.config
|
||||
?.commands
|
||||
?.where((c) => c.name.contains(text.replaceFirst('/', '')))
|
||||
?.toList() ??
|
||||
[];
|
||||
|
||||
RenderBox renderBox = context.findRenderObject();
|
||||
final size = renderBox.size;
|
||||
@@ -711,14 +732,18 @@ class MessageInputState extends State<MessageInput> {
|
||||
Color _getIconColor(int index) {
|
||||
switch (index) {
|
||||
case 0:
|
||||
return _attachmentContainsFile && _attachments.isNotEmpty
|
||||
? Colors.black.withOpacity(0.2)
|
||||
: Colors.black.withOpacity(0.5);
|
||||
return _attachments.isEmpty
|
||||
? StreamChatTheme.of(context).accentColor
|
||||
: (!_attachmentContainsFile
|
||||
? StreamChatTheme.of(context).accentColor
|
||||
: Colors.black.withOpacity(0.2));
|
||||
break;
|
||||
case 1:
|
||||
return !_attachmentContainsFile && _attachments.isNotEmpty
|
||||
? Colors.black.withOpacity(0.2)
|
||||
: Colors.black.withOpacity(0.5);
|
||||
return _attachmentContainsFile
|
||||
? StreamChatTheme.of(context).accentColor
|
||||
: (_attachments.isEmpty
|
||||
? Colors.black.withOpacity(0.5)
|
||||
: Colors.black.withOpacity(0.2));
|
||||
break;
|
||||
case 2:
|
||||
return _attachmentContainsFile && _attachments.isNotEmpty
|
||||
@@ -860,22 +885,38 @@ class MessageInputState extends State<MessageInput> {
|
||||
}
|
||||
|
||||
if (snapshot.data) {
|
||||
return IgnorePointer(
|
||||
ignoring: _attachmentContainsFile,
|
||||
child: MediaListView(
|
||||
selectedIds: _attachments.map((e) => e.id).toList(),
|
||||
onSelect: (media) async {
|
||||
if (!_attachments
|
||||
.any((element) => element.id == media.id)) {
|
||||
_addAttachment(media);
|
||||
} else {
|
||||
setState(() {
|
||||
_attachments
|
||||
.removeWhere((element) => element.id == media.id);
|
||||
});
|
||||
}
|
||||
if (_attachmentContainsFile) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
pickFile(DefaultAttachmentTypes.file);
|
||||
},
|
||||
),
|
||||
child: Container(
|
||||
constraints: BoxConstraints.expand(),
|
||||
color: Color(0xfff2f2f2),
|
||||
child: Text(
|
||||
'Add more files',
|
||||
style: TextStyle(
|
||||
color: StreamChatTheme.of(context).accentColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
),
|
||||
);
|
||||
}
|
||||
return MediaListView(
|
||||
selectedIds: _attachments.map((e) => e.id).toList(),
|
||||
onSelect: (media) async {
|
||||
if (!_attachments
|
||||
.any((element) => element.id == media.id)) {
|
||||
_addAttachment(media);
|
||||
} else {
|
||||
setState(() {
|
||||
_attachments
|
||||
.removeWhere((element) => element.id == media.id);
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1305,10 +1346,11 @@ class MessageInputState extends State<MessageInput> {
|
||||
children: [
|
||||
if (_attachments.any((e) => e.attachment?.type == 'file'))
|
||||
LimitedBox(
|
||||
maxHeight: 73.0,
|
||||
maxHeight: 136.0,
|
||||
child: ListView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
children: _attachments
|
||||
reverse: true,
|
||||
shrinkWrap: true,
|
||||
children: _attachments.reversed
|
||||
.where((e) => e.attachment?.type == 'file')
|
||||
.map(
|
||||
(e) => Padding(
|
||||
@@ -1319,9 +1361,11 @@ class MessageInputState extends State<MessageInput> {
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: FileAttachment(
|
||||
attachment: e.attachment,
|
||||
attachmentType: FileAttachmentType.local,
|
||||
file: e.file,
|
||||
size: Size(
|
||||
MediaQuery.of(context).size.width * 0.55,
|
||||
MediaQuery.of(context).size.height * 0.3,
|
||||
MediaQuery.of(context).size.width * 0.65,
|
||||
56.0,
|
||||
),
|
||||
trailing: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
@@ -1507,7 +1551,9 @@ class MessageInputState extends State<MessageInput> {
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: IconButton(
|
||||
icon: StreamSvgIcon.lightning(
|
||||
color: Color(0xFF000000).withAlpha(128),
|
||||
color: _commandsOverlay != null
|
||||
? StreamChatTheme.of(context).accentColor
|
||||
: Color(0xFF000000).withAlpha(128),
|
||||
),
|
||||
padding: const EdgeInsets.all(0),
|
||||
constraints: BoxConstraints.tightFor(
|
||||
@@ -1515,13 +1561,26 @@ class MessageInputState extends State<MessageInput> {
|
||||
width: 24,
|
||||
),
|
||||
splashRadius: 24,
|
||||
onPressed: () {
|
||||
onPressed: () async {
|
||||
if (_openFilePickerSection) {
|
||||
setState(() {
|
||||
_animateContainer = false;
|
||||
_openFilePickerSection = false;
|
||||
_filePickerSize = _kMinMediaPickerSize;
|
||||
});
|
||||
await Future.delayed(Duration(milliseconds: 300));
|
||||
}
|
||||
|
||||
if (_commandsOverlay == null) {
|
||||
_commandsOverlay = _buildCommandsOverlayEntry();
|
||||
Overlay.of(context).insert(_commandsOverlay);
|
||||
setState(() {
|
||||
_commandsOverlay = _buildCommandsOverlayEntry();
|
||||
Overlay.of(context).insert(_commandsOverlay);
|
||||
});
|
||||
} else {
|
||||
_commandsOverlay?.remove();
|
||||
_commandsOverlay = null;
|
||||
setState(() {
|
||||
_commandsOverlay?.remove();
|
||||
_commandsOverlay = null;
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
@@ -1723,12 +1782,16 @@ class MessageInputState extends State<MessageInput> {
|
||||
|
||||
final mimeType = _getMimeType(file.path.split('/').last);
|
||||
|
||||
if (mimeType.type == 'video' || mimeType.type == 'image') {
|
||||
attachmentType = mimeType.type;
|
||||
}
|
||||
|
||||
Map<String, dynamic> extraDataMap = {};
|
||||
|
||||
if (camera) {
|
||||
if (mimeType.type == 'video' || mimeType.type == 'image') {
|
||||
attachmentType = mimeType.type;
|
||||
}
|
||||
} else {
|
||||
attachmentType = 'file';
|
||||
}
|
||||
|
||||
if (mimeType?.subtype != null) {
|
||||
extraDataMap['mime_type'] = mimeType.subtype.toLowerCase();
|
||||
}
|
||||
@@ -1744,7 +1807,7 @@ class MessageInputState extends State<MessageInput> {
|
||||
localUri: file.path != null ? Uri.parse(file.path) : null,
|
||||
type: attachmentType,
|
||||
extraData: extraDataMap.isNotEmpty ? extraDataMap : null,
|
||||
title: file.name ?? 'File',
|
||||
title: file.name,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1938,8 +2001,6 @@ class MessageInputState extends State<MessageInput> {
|
||||
_mentionsOverlay?.remove();
|
||||
_mentionsOverlay = null;
|
||||
|
||||
final channel = StreamChannel.of(context).channel;
|
||||
|
||||
Future sendingFuture;
|
||||
Message message;
|
||||
if (widget.editMessage != null) {
|
||||
@@ -1964,6 +2025,12 @@ class MessageInputState extends State<MessageInput> {
|
||||
message = await widget.preMessageSending(message);
|
||||
}
|
||||
|
||||
final streamChannel = StreamChannel.of(context);
|
||||
final channel = streamChannel.channel;
|
||||
if (!channel.state.isUpToDate) {
|
||||
await streamChannel.reloadChannel();
|
||||
}
|
||||
|
||||
if (widget.editMessage == null ||
|
||||
widget.editMessage.status == MessageSendingStatus.FAILED) {
|
||||
sendingFuture = channel.sendMessage(message);
|
||||
|
||||
+507
-327
@@ -1,10 +1,13 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:jiffy/jiffy.dart';
|
||||
import 'package:rxdart/rxdart.dart';
|
||||
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
|
||||
import 'package:stream_chat/stream_chat.dart';
|
||||
import 'package:stream_chat_flutter/src/lazy_load_scroll_view.dart';
|
||||
import 'package:stream_chat_flutter/src/message_widget.dart';
|
||||
import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
|
||||
import 'package:stream_chat_flutter/src/system_message.dart';
|
||||
@@ -113,11 +116,12 @@ class MessageListView extends StatefulWidget {
|
||||
this.onReplyTap,
|
||||
this.dateDividerBuilder,
|
||||
this.scrollPhysics = const AlwaysScrollableScrollPhysics(),
|
||||
this.initialScrollIndex = 0,
|
||||
this.initialAlignment = 0,
|
||||
this.initialScrollIndex,
|
||||
this.initialAlignment,
|
||||
this.scrollController,
|
||||
this.itemPositionListener,
|
||||
this.onMessageSwiped,
|
||||
this.highlightInitialMessage = false,
|
||||
}) : super(key: key);
|
||||
|
||||
/// Function used to build a custom message widget
|
||||
@@ -165,6 +169,11 @@ class MessageListView extends StatefulWidget {
|
||||
///
|
||||
final ReplyTapCallback onReplyTap;
|
||||
|
||||
/// If true the list will highlight the initialMessage if there is any.
|
||||
///
|
||||
/// Also See [StreamChannel]
|
||||
final bool highlightInitialMessage;
|
||||
|
||||
@override
|
||||
_MessageListViewState createState() => _MessageListViewState();
|
||||
}
|
||||
@@ -176,6 +185,47 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
Function _onThreadTap;
|
||||
bool _showScrollToBottom = false;
|
||||
ItemPositionsListener _itemPositionListener;
|
||||
int _messageListLength;
|
||||
|
||||
int get _initialIndex {
|
||||
if (widget.initialScrollIndex != null) return widget.initialScrollIndex;
|
||||
final streamChannel = StreamChannel.of(context);
|
||||
if (streamChannel.initialMessageId != null) {
|
||||
final messages = streamChannel.channel.state.messages;
|
||||
final totalMessages = messages.length;
|
||||
final messageIndex = messages.indexWhere((e) {
|
||||
return e.id == streamChannel.initialMessageId;
|
||||
});
|
||||
final index = totalMessages - messageIndex;
|
||||
if (index != 0) return index - 1;
|
||||
return index;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
double get _initialAlignment {
|
||||
if (widget.initialAlignment != null) return widget.initialAlignment;
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool _isInitialMessage(String id) {
|
||||
final streamChannel = StreamChannel.of(context);
|
||||
return streamChannel.initialMessageId == id;
|
||||
}
|
||||
|
||||
bool get _upToDate => StreamChannel.of(context).channel.state.isUpToDate;
|
||||
|
||||
bool _topPaginationActive = false;
|
||||
bool _bottomPaginationActive = false;
|
||||
|
||||
int initialIndex;
|
||||
double initialAlignment;
|
||||
|
||||
List<Message> messages = <Message>[];
|
||||
|
||||
bool initialMessageHighlightComplete = false;
|
||||
|
||||
bool _inBetweenList = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -185,179 +235,266 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
? streamChannel.channel.state.threadsStream
|
||||
.where((threads) => threads.containsKey(widget.parentMessage.id))
|
||||
.map((threads) => threads[widget.parentMessage.id])
|
||||
: streamChannel.channel.state.messagesStream;
|
||||
: streamChannel.channel.state?.messagesStream;
|
||||
|
||||
return StreamBuilder<List<Message>>(
|
||||
stream: messagesStream.map((messages) => messages
|
||||
.where((e) =>
|
||||
!e.isDeleted ||
|
||||
(e.isDeleted &&
|
||||
e.user.id == streamChannel.channel.client.state.user.id))
|
||||
.toList()),
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData) {
|
||||
return Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
}
|
||||
return WillPopScope(
|
||||
onWillPop: () async {
|
||||
if (!_upToDate) {
|
||||
await streamChannel.reloadChannel();
|
||||
}
|
||||
return true;
|
||||
},
|
||||
child: StreamBuilder<List<Message>>(
|
||||
stream: messagesStream?.map((messages) => messages
|
||||
?.where((e) =>
|
||||
!e.isDeleted ||
|
||||
(e.isDeleted &&
|
||||
e.user.id == streamChannel.channel.client.state.user.id))
|
||||
?.toList()),
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData) {
|
||||
return Center(
|
||||
child: const CircularProgressIndicator(),
|
||||
);
|
||||
}
|
||||
|
||||
final messages = snapshot.data?.reversed?.toList() ?? [];
|
||||
final messageList = snapshot.data?.reversed?.toList() ?? [];
|
||||
|
||||
if (messages.isEmpty) {
|
||||
return Center(
|
||||
child: Text(
|
||||
'No chats here yet...',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.black.withOpacity(.5),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
if (messageList.isEmpty) {
|
||||
if (_upToDate) {
|
||||
return Center(
|
||||
child: Text(
|
||||
'No chats here yet...',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.black.withOpacity(.5),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
messages = messageList;
|
||||
}
|
||||
|
||||
return Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
ScrollablePositionedList.builder(
|
||||
itemPositionsListener: _itemPositionListener,
|
||||
addAutomaticKeepAlives: true,
|
||||
key: Key('messageListView'),
|
||||
initialScrollIndex: widget.initialScrollIndex,
|
||||
initialAlignment: widget.initialAlignment,
|
||||
physics: widget.scrollPhysics,
|
||||
itemScrollController: _scrollController,
|
||||
reverse: true,
|
||||
itemCount: messages.length +
|
||||
1 +
|
||||
(widget.parentMessage != null ? 1 : 0),
|
||||
itemBuilder: (context, i) {
|
||||
if (i == messages.length + 1) {
|
||||
if (widget.parentMessageBuilder != null) {
|
||||
return widget.parentMessageBuilder(
|
||||
context,
|
||||
widget.parentMessage,
|
||||
);
|
||||
} else {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: <Widget>[
|
||||
buildParentMessage(widget.parentMessage),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 32),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Text(
|
||||
'Start of thread',
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
color:
|
||||
Theme.of(context).accentColor.withAlpha(50),
|
||||
),
|
||||
),
|
||||
],
|
||||
final newMessagesListLength = messages.length;
|
||||
|
||||
if (_messageListLength != null) {
|
||||
if (_bottomPaginationActive || (_inBetweenList && _upToDate)) {
|
||||
if (_itemPositionListener.itemPositions.value?.isNotEmpty ==
|
||||
true) {
|
||||
final first = _itemPositionListener.itemPositions.value.first;
|
||||
final diff = newMessagesListLength - _messageListLength;
|
||||
if (diff > 0) {
|
||||
initialIndex = first.index + diff;
|
||||
initialAlignment = first.itemLeadingEdge;
|
||||
}
|
||||
}
|
||||
} else if (!_topPaginationActive && _upToDate) {
|
||||
// Reset the index in-case we send any new message
|
||||
initialIndex = 0;
|
||||
initialAlignment = 0;
|
||||
}
|
||||
}
|
||||
|
||||
_messageListLength = newMessagesListLength;
|
||||
|
||||
return Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
LazyLoadScrollView(
|
||||
onStartOfPage: () async {
|
||||
_inBetweenList = false;
|
||||
if (!_upToDate) {
|
||||
_topPaginationActive = false;
|
||||
_bottomPaginationActive = true;
|
||||
return _paginateData(
|
||||
streamChannel,
|
||||
QueryDirection.bottom,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (i == messages.length) {
|
||||
return _buildLoadingIndicator(streamChannel);
|
||||
}
|
||||
final message = messages[i];
|
||||
final nextMessage = i > 0 ? messages[i - 1] : null;
|
||||
|
||||
Widget messageWidget;
|
||||
|
||||
if (i == 0) {
|
||||
messageWidget = _buildBottomMessage(
|
||||
context,
|
||||
message,
|
||||
messages,
|
||||
streamChannel,
|
||||
);
|
||||
} else if (i == messages.length - 1) {
|
||||
messageWidget = _buildTopMessage(
|
||||
context,
|
||||
message,
|
||||
messages,
|
||||
streamChannel,
|
||||
);
|
||||
} else {
|
||||
if (widget.messageBuilder != null) {
|
||||
messageWidget = Builder(
|
||||
key: ValueKey<String>('MESSAGE-${message.id}'),
|
||||
builder: (context) => widget.messageBuilder(
|
||||
context,
|
||||
MessageDetails(
|
||||
context,
|
||||
message,
|
||||
messages,
|
||||
i,
|
||||
),
|
||||
messages),
|
||||
);
|
||||
} else {
|
||||
messageWidget = buildMessage(message, messages, i);
|
||||
}
|
||||
}
|
||||
|
||||
if (nextMessage != null &&
|
||||
!Jiffy(message.createdAt.toLocal())
|
||||
.isSame(nextMessage.createdAt.toLocal(), Units.DAY)) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: <Widget>[
|
||||
messageWidget,
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12.0),
|
||||
child: widget.dateDividerBuilder != null
|
||||
? widget.dateDividerBuilder(
|
||||
nextMessage.createdAt.toLocal())
|
||||
: DateDivider(
|
||||
dateTime: nextMessage.createdAt.toLocal(),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return messageWidget;
|
||||
},
|
||||
),
|
||||
if (widget.showScrollToBottom && _showScrollToBottom)
|
||||
_buildScrollToBottom(),
|
||||
Positioned(
|
||||
top: 20.0,
|
||||
child: ValueListenableBuilder<Iterable<ItemPosition>>(
|
||||
valueListenable: _itemPositionListener.itemPositions,
|
||||
builder: (context, values, _) {
|
||||
final items = _itemPositionListener.itemPositions?.value;
|
||||
if (items.isEmpty || messages.isEmpty) {
|
||||
return SizedBox();
|
||||
}
|
||||
|
||||
var index = _getTopElement(values).index;
|
||||
|
||||
if (index > messages.length) {
|
||||
return SizedBox();
|
||||
}
|
||||
|
||||
if (index == messages.length) {
|
||||
index = max(index - 1, 0);
|
||||
}
|
||||
|
||||
return widget.dateDividerBuilder != null
|
||||
? widget.dateDividerBuilder(
|
||||
messages[index].createdAt.toLocal(),
|
||||
)
|
||||
: DateDivider(
|
||||
dateTime: messages[index].createdAt.toLocal(),
|
||||
);
|
||||
},
|
||||
onEndOfPage: () async {
|
||||
_inBetweenList = false;
|
||||
_topPaginationActive = true;
|
||||
_bottomPaginationActive = false;
|
||||
return _paginateData(
|
||||
streamChannel,
|
||||
QueryDirection.top,
|
||||
);
|
||||
},
|
||||
onInBetweenOfPage: () {
|
||||
_inBetweenList = true;
|
||||
},
|
||||
child: ScrollablePositionedList.builder(
|
||||
key: ValueKey(initialIndex + initialAlignment),
|
||||
itemPositionsListener: _itemPositionListener,
|
||||
addAutomaticKeepAlives: true,
|
||||
initialScrollIndex: initialIndex ?? 0,
|
||||
initialAlignment: initialAlignment ?? 0,
|
||||
physics: widget.scrollPhysics,
|
||||
itemScrollController: _scrollController,
|
||||
reverse: true,
|
||||
itemCount: messages.length +
|
||||
2 +
|
||||
(widget.parentMessage != null ? 1 : 0),
|
||||
itemBuilder: (context, i) {
|
||||
if (i == messages.length + 2) {
|
||||
if (widget.parentMessageBuilder != null) {
|
||||
return widget.parentMessageBuilder(
|
||||
context,
|
||||
widget.parentMessage,
|
||||
);
|
||||
} else {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: <Widget>[
|
||||
buildParentMessage(widget.parentMessage),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
Color(0XFFF7F7F7),
|
||||
Color(0XFFFCFCFC),
|
||||
],
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text(
|
||||
'${widget.parentMessage.replyCount} ${widget.parentMessage.replyCount == 1 ? 'Reply' : 'Replies'}',
|
||||
textAlign: TextAlign.center,
|
||||
style: StreamChatTheme.of(context)
|
||||
.channelTheme
|
||||
.channelHeaderTheme
|
||||
.lastMessageAt,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
if (i == messages.length + 1) {
|
||||
return _buildLoadingIndicator(
|
||||
streamChannel,
|
||||
QueryDirection.top,
|
||||
);
|
||||
}
|
||||
if (i == 0) {
|
||||
return _buildLoadingIndicator(
|
||||
streamChannel,
|
||||
QueryDirection.bottom,
|
||||
);
|
||||
}
|
||||
final message = messages[i - 1];
|
||||
final nextMessage = (i - 1) > 0 ? messages[i - 2] : null;
|
||||
|
||||
Widget messageWidget;
|
||||
|
||||
if (i == 1) {
|
||||
messageWidget = _buildBottomMessage(
|
||||
context,
|
||||
message,
|
||||
messages,
|
||||
streamChannel,
|
||||
);
|
||||
} else if (i == messages.length - 1) {
|
||||
messageWidget = _buildTopMessage(
|
||||
context,
|
||||
message,
|
||||
messages,
|
||||
streamChannel,
|
||||
);
|
||||
} else {
|
||||
if (widget.messageBuilder != null) {
|
||||
messageWidget = Builder(
|
||||
key: ValueKey<String>('MESSAGE-${message.id}'),
|
||||
builder: (_) => widget.messageBuilder(
|
||||
context,
|
||||
MessageDetails(
|
||||
context,
|
||||
message,
|
||||
messages,
|
||||
i,
|
||||
),
|
||||
messages),
|
||||
);
|
||||
} else {
|
||||
messageWidget = buildMessage(message, messages, i);
|
||||
}
|
||||
}
|
||||
|
||||
if (nextMessage != null &&
|
||||
!Jiffy(message.createdAt.toLocal()).isSame(
|
||||
nextMessage.createdAt.toLocal(), Units.DAY)) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: <Widget>[
|
||||
messageWidget,
|
||||
Padding(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(vertical: 12.0),
|
||||
child: widget.dateDividerBuilder != null
|
||||
? widget.dateDividerBuilder(
|
||||
nextMessage.createdAt.toLocal())
|
||||
: DateDivider(
|
||||
dateTime: nextMessage.createdAt.toLocal(),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return messageWidget;
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
if (widget.showScrollToBottom) _buildScrollToBottom(),
|
||||
Positioned(
|
||||
top: 20.0,
|
||||
child: ValueListenableBuilder<Iterable<ItemPosition>>(
|
||||
valueListenable: _itemPositionListener.itemPositions,
|
||||
builder: (context, values, _) {
|
||||
final items = _itemPositionListener.itemPositions?.value;
|
||||
if (items.isEmpty || messages.isEmpty) {
|
||||
return SizedBox();
|
||||
}
|
||||
|
||||
var index = _getTopElement(values).index;
|
||||
|
||||
if (index > messages.length) {
|
||||
return SizedBox();
|
||||
}
|
||||
|
||||
if (index == messages.length) {
|
||||
index = max(index - 1, 0);
|
||||
}
|
||||
|
||||
return widget.dateDividerBuilder != null
|
||||
? widget.dateDividerBuilder(
|
||||
messages[index].createdAt.toLocal(),
|
||||
)
|
||||
: DateDivider(
|
||||
dateTime: messages[index].createdAt.toLocal(),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _paginateData(
|
||||
StreamChannelState channel, QueryDirection direction) {
|
||||
if (widget.parentMessage == null) {
|
||||
return channel.queryMessages(direction: direction);
|
||||
} else {
|
||||
return channel.getReplies(widget.parentMessage.id);
|
||||
}
|
||||
}
|
||||
|
||||
ItemPosition _getTopElement(Iterable<ItemPosition> values) {
|
||||
@@ -369,91 +506,120 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
|
||||
Widget _buildScrollToBottom() {
|
||||
final streamChannel = StreamChannel.of(context);
|
||||
return Positioned(
|
||||
bottom: 8,
|
||||
right: 8,
|
||||
width: 40,
|
||||
height: 40,
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
FloatingActionButton(
|
||||
backgroundColor: Colors.white,
|
||||
child: StreamSvgIcon.down(
|
||||
color: Colors.black,
|
||||
),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_showScrollToBottom = false;
|
||||
});
|
||||
_scrollController.scrollTo(
|
||||
index: 0,
|
||||
duration: Duration(seconds: 1),
|
||||
curve: Curves.easeInOut,
|
||||
);
|
||||
},
|
||||
),
|
||||
if (streamChannel.channel.state.members.any((Member e) =>
|
||||
e.userId == streamChannel.channel.client.state.user.id))
|
||||
StreamBuilder<int>(
|
||||
stream: streamChannel.channel.state.unreadCountStream,
|
||||
initialData: streamChannel.channel.state.unreadCount,
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData || snapshot.data <= 0) {
|
||||
return Offstage();
|
||||
return StreamBuilder<Tuple2<bool, int>>(
|
||||
stream: Rx.combineLatest2(
|
||||
streamChannel.channel.state.isUpToDateStream,
|
||||
streamChannel.channel.state.unreadCountStream,
|
||||
(bool isUpToDate, int unreadCount) => Tuple2(isUpToDate, unreadCount),
|
||||
),
|
||||
builder: (_, snapshot) {
|
||||
if (snapshot.hasError) {
|
||||
return Offstage();
|
||||
} else if (!snapshot.hasData) {
|
||||
return Offstage();
|
||||
}
|
||||
final isUpToDate = snapshot.data.item1;
|
||||
final showScrollToBottom = !isUpToDate || _showScrollToBottom;
|
||||
if (!showScrollToBottom) {
|
||||
return Offstage();
|
||||
}
|
||||
final unreadCount = snapshot.data.item2;
|
||||
final showUnreadCount = unreadCount > 0 &&
|
||||
streamChannel.channel.state.members.any(
|
||||
(e) => e.userId == streamChannel.channel.client.state.user.id);
|
||||
return Positioned(
|
||||
bottom: 8,
|
||||
right: 8,
|
||||
width: 40,
|
||||
height: 40,
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
FloatingActionButton(
|
||||
backgroundColor: Colors.white,
|
||||
child: StreamSvgIcon.down(
|
||||
color: Colors.black,
|
||||
),
|
||||
onPressed: () {
|
||||
if (unreadCount > 0) {
|
||||
streamChannel.channel.markRead();
|
||||
}
|
||||
return Positioned(
|
||||
width: 20,
|
||||
height: 20,
|
||||
left: 10,
|
||||
top: -10,
|
||||
child: CircleAvatar(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(3.0),
|
||||
child: Text(
|
||||
snapshot.data.toString(),
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
if (!_upToDate) {
|
||||
_bottomPaginationActive = false;
|
||||
_topPaginationActive = false;
|
||||
streamChannel.reloadChannel();
|
||||
} else {
|
||||
setState(() => _showScrollToBottom = false);
|
||||
_scrollController.scrollTo(
|
||||
index: 0,
|
||||
duration: Duration(seconds: 1),
|
||||
curve: Curves.easeInOut,
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
if (showUnreadCount)
|
||||
Positioned(
|
||||
width: 20,
|
||||
height: 20,
|
||||
left: 10,
|
||||
top: -10,
|
||||
child: CircleAvatar(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(3.0),
|
||||
child: Text(
|
||||
'$unreadCount',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Container _buildLoadingIndicator(StreamChannelState streamChannel) {
|
||||
return Container(
|
||||
key: Key('LOADING-INDICATOR'),
|
||||
height: 50,
|
||||
width: double.infinity,
|
||||
child: StreamBuilder<bool>(
|
||||
stream: streamChannel.queryMessage,
|
||||
initialData: false,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasError) {
|
||||
return Container(
|
||||
color: Color(0xffd0021B).withAlpha(26),
|
||||
child: Center(
|
||||
child: Text('Error loading messages'),
|
||||
),
|
||||
);
|
||||
}
|
||||
if (!snapshot.data) {
|
||||
return SizedBox();
|
||||
}
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: CircularProgressIndicator(),
|
||||
Widget _buildLoadingIndicator(
|
||||
StreamChannelState streamChannel,
|
||||
QueryDirection direction,
|
||||
) {
|
||||
final stream = direction == QueryDirection.top
|
||||
? streamChannel.queryTopMessages
|
||||
: streamChannel.queryBottomMessages;
|
||||
return StreamBuilder<bool>(
|
||||
key: Key('LOADING-INDICATOR'),
|
||||
stream: stream,
|
||||
initialData: false,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasError) {
|
||||
return Container(
|
||||
color: Color(0xffd0021B).withAlpha(26),
|
||||
child: Center(
|
||||
child: Text('Error loading messages'),
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (!snapshot.data) {
|
||||
if (direction == QueryDirection.top) {
|
||||
return Container(
|
||||
height: 52,
|
||||
width: double.infinity,
|
||||
);
|
||||
}
|
||||
return Offstage();
|
||||
}
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: const CircularProgressIndicator(),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildTopMessage(
|
||||
@@ -480,22 +646,7 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
} else {
|
||||
messageWidget = buildMessage(message, messages, messages.length - 1);
|
||||
}
|
||||
|
||||
return VisibilityDetector(
|
||||
key: ValueKey<String>('TOP-MESSAGE'),
|
||||
child: messageWidget,
|
||||
onVisibilityChanged: (visibility) {
|
||||
final topIsVisible = visibility.visibleBounds != Rect.zero;
|
||||
if (topIsVisible && !_topWasVisible) {
|
||||
if (widget.parentMessage == null) {
|
||||
streamChannel.queryMessages();
|
||||
} else {
|
||||
streamChannel.getReplies(widget.parentMessage.id);
|
||||
}
|
||||
}
|
||||
_topWasVisible = topIsVisible;
|
||||
},
|
||||
);
|
||||
return messageWidget;
|
||||
}
|
||||
|
||||
Widget _buildBottomMessage(
|
||||
@@ -527,18 +678,17 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
key: ValueKey<String>('BOTTOM-MESSAGE'),
|
||||
onVisibilityChanged: (visibility) {
|
||||
final isVisible = visibility.visibleBounds != Rect.zero;
|
||||
if (isVisible &&
|
||||
!_bottomWasVisible &&
|
||||
streamChannel.channel.config?.readEvents == true) {
|
||||
if (streamChannel.channel.state.unreadCount > 0) {
|
||||
if (isVisible && !_bottomWasVisible) {
|
||||
final channel = streamChannel.channel;
|
||||
if (_upToDate &&
|
||||
channel.config?.readEvents == true &&
|
||||
channel.state.unreadCount > 0) {
|
||||
streamChannel.channel.markRead();
|
||||
}
|
||||
_bottomWasVisible = !isVisible;
|
||||
}
|
||||
_bottomWasVisible = isVisible;
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_showScrollToBottom = !isVisible;
|
||||
});
|
||||
setState(() => _showScrollToBottom = !isVisible);
|
||||
}
|
||||
},
|
||||
child: messageWidget,
|
||||
@@ -551,8 +701,9 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
final isMyMessage = message.user.id == StreamChat.of(context).user.id;
|
||||
|
||||
return MessageWidget(
|
||||
showReplyIndicator: false,
|
||||
showThreadReplyIndicator: false,
|
||||
showInChannelIndicator: false,
|
||||
showReplyIndicator: false,
|
||||
message: message,
|
||||
reverse: isMyMessage,
|
||||
showUsername: !isMyMessage,
|
||||
@@ -595,7 +746,7 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
final userId = StreamChat.of(context).user.id;
|
||||
final isMyMessage = message.user.id == userId;
|
||||
final isNextUser =
|
||||
index - 1 >= 0 && message.user.id == messages[index - 1]?.user?.id;
|
||||
index - 2 >= 0 && message.user.id == messages[index - 2]?.user?.id;
|
||||
|
||||
final channel = StreamChannel.of(context).channel;
|
||||
final readList = channel.state?.read
|
||||
@@ -610,55 +761,89 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
|
||||
final allRead = readList.length >= (channel.memberCount ?? 0) - 1;
|
||||
|
||||
return Swipeable(
|
||||
onSwipeEnd: () => widget.onMessageSwiped(message),
|
||||
backgroundIcon: StreamSvgIcon.reply(
|
||||
color: StreamChatTheme.of(context).accentColor,
|
||||
final isThreadMessage =
|
||||
widget.parentMessage != null || message?.showInChannel == true;
|
||||
|
||||
Widget child = MessageWidget(
|
||||
key: ValueKey<String>('MESSAGE-${message.id}'),
|
||||
message: message,
|
||||
reverse: isMyMessage,
|
||||
showReactions: !message.isDeleted,
|
||||
padding: EdgeInsets.only(
|
||||
left: 8.0,
|
||||
right: 8.0,
|
||||
bottom: index == 0 ? 30 : (isNextUser ? 2 : 7),
|
||||
top: 3,
|
||||
),
|
||||
child: MessageWidget(
|
||||
key: ValueKey<String>('MESSAGE-${message.id}'),
|
||||
message: message,
|
||||
reverse: isMyMessage,
|
||||
showReactions: !message.isDeleted,
|
||||
padding: EdgeInsets.only(
|
||||
left: 8.0,
|
||||
right: 8.0,
|
||||
bottom: index == 0 ? 30 : (isNextUser ? 5 : 10),
|
||||
),
|
||||
showUsername: !isMyMessage && !isNextUser,
|
||||
showSendingIndicator: isMyMessage &&
|
||||
(index == 0 || message.status != MessageSendingStatus.SENT)
|
||||
? DisplayWidget.show
|
||||
: DisplayWidget.hide,
|
||||
showTimestamp: !isNextUser || readList?.isNotEmpty == true,
|
||||
showEditMessage: isMyMessage,
|
||||
showDeleteMessage: isMyMessage,
|
||||
borderSide: isMyMessage ? BorderSide.none : null,
|
||||
onThreadTap: _onThreadTap,
|
||||
onReplyTap: widget.onReplyTap,
|
||||
attachmentBorderRadiusGeometry: BorderRadius.only(
|
||||
topLeft: Radius.circular(16),
|
||||
bottomLeft: Radius.circular(!isNextUser ? 0 : 16),
|
||||
topRight: Radius.circular(16),
|
||||
bottomRight: Radius.circular(16),
|
||||
),
|
||||
attachmentPadding: const EdgeInsets.all(2),
|
||||
borderRadiusGeometry: BorderRadius.only(
|
||||
topLeft: Radius.circular(16),
|
||||
bottomLeft: Radius.circular(!isNextUser ? 0 : 16),
|
||||
topRight: Radius.circular(16),
|
||||
bottomRight: Radius.circular(16),
|
||||
),
|
||||
showUserAvatar: isMyMessage
|
||||
? DisplayWidget.gone
|
||||
: (isNextUser ? DisplayWidget.hide : DisplayWidget.show),
|
||||
messageTheme: isMyMessage
|
||||
? StreamChatTheme.of(context).ownMessageTheme
|
||||
: StreamChatTheme.of(context).otherMessageTheme,
|
||||
readList: readList,
|
||||
allRead: allRead,
|
||||
showInChannelIndicator: widget.parentMessage == null,
|
||||
showThreadReplyIndicator: widget.parentMessage == null,
|
||||
showUsername: !isMyMessage && !isNextUser,
|
||||
showSendingIndicator: isMyMessage &&
|
||||
(index == 0 || message.status != MessageSendingStatus.SENT)
|
||||
? DisplayWidget.show
|
||||
: DisplayWidget.hide,
|
||||
showTimestamp: !isNextUser || readList?.isNotEmpty == true,
|
||||
showEditMessage: isMyMessage,
|
||||
showDeleteMessage: isMyMessage,
|
||||
borderSide: isMyMessage ? BorderSide.none : null,
|
||||
onThreadTap: _onThreadTap,
|
||||
onReplyTap: widget.onReplyTap,
|
||||
attachmentBorderRadiusGeometry: BorderRadius.only(
|
||||
topLeft: Radius.circular(16),
|
||||
bottomLeft: Radius.circular(!isNextUser ? 0 : 16),
|
||||
topRight: Radius.circular(16),
|
||||
bottomRight: Radius.circular(16),
|
||||
),
|
||||
attachmentPadding: const EdgeInsets.all(2),
|
||||
borderRadiusGeometry: BorderRadius.only(
|
||||
topLeft: Radius.circular(16),
|
||||
bottomLeft: Radius.circular(!isNextUser ? 0 : 16),
|
||||
topRight: Radius.circular(16),
|
||||
bottomRight: Radius.circular(16),
|
||||
),
|
||||
showUserAvatar: isMyMessage
|
||||
? DisplayWidget.gone
|
||||
: (isNextUser ? DisplayWidget.hide : DisplayWidget.show),
|
||||
messageTheme: isMyMessage
|
||||
? StreamChatTheme.of(context).ownMessageTheme
|
||||
: StreamChatTheme.of(context).otherMessageTheme,
|
||||
readList: readList,
|
||||
allRead: allRead,
|
||||
);
|
||||
|
||||
if (!isThreadMessage) {
|
||||
child = Swipeable(
|
||||
onSwipeEnd: () => widget.onMessageSwiped(message),
|
||||
backgroundIcon: StreamSvgIcon.reply(
|
||||
color: StreamChatTheme.of(context).accentColor,
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
if (!initialMessageHighlightComplete &&
|
||||
widget.highlightInitialMessage &&
|
||||
_isInitialMessage(message.id)) {
|
||||
final accentColor = Theme.of(context).accentColor;
|
||||
child = TweenAnimationBuilder<Color>(
|
||||
tween: ColorTween(
|
||||
begin: accentColor.withOpacity(0.7),
|
||||
end: Colors.transparent,
|
||||
),
|
||||
duration: const Duration(seconds: 2),
|
||||
child: child,
|
||||
onEnd: () {
|
||||
initialMessageHighlightComplete = true;
|
||||
},
|
||||
builder: (_, color, child) {
|
||||
return Container(
|
||||
color: color,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
return child;
|
||||
}
|
||||
|
||||
StreamSubscription _messageNewListener;
|
||||
@@ -671,26 +856,21 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
|
||||
final streamChannel = StreamChannel.of(context);
|
||||
|
||||
initialIndex = _initialIndex;
|
||||
initialAlignment = _initialAlignment;
|
||||
|
||||
_messageNewListener =
|
||||
streamChannel.channel.on(EventType.messageNew).listen((event) {
|
||||
final firstElementInViewport =
|
||||
_itemPositionListener.itemPositions.value.first;
|
||||
if (_upToDate) {
|
||||
_bottomPaginationActive = false;
|
||||
_topPaginationActive = false;
|
||||
}
|
||||
if (event.message.user.id == streamChannel.channel.client.state.user.id) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_scrollController.jumpTo(
|
||||
index: 0,
|
||||
);
|
||||
});
|
||||
} else {
|
||||
if (firstElementInViewport.index != 0) {
|
||||
_scrollController.jumpTo(
|
||||
index: firstElementInViewport.index + 1,
|
||||
alignment: firstElementInViewport.itemLeadingEdge,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (firstElementInViewport.index == 0) {
|
||||
streamChannel.channel.markRead();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -77,16 +77,16 @@ class MessageSearchItem extends StatelessWidget {
|
||||
}
|
||||
|
||||
Widget _buildDate(BuildContext context, Message message) {
|
||||
final lastUpdatedAt = message.updatedAt;
|
||||
final createdAt = message.createdAt;
|
||||
String stringDate;
|
||||
final now = DateTime.now();
|
||||
|
||||
if (now.year != lastUpdatedAt.year ||
|
||||
now.month != lastUpdatedAt.month ||
|
||||
now.day != lastUpdatedAt.day) {
|
||||
stringDate = Jiffy(lastUpdatedAt.toLocal()).format('dd/MM/yyyy');
|
||||
if (now.year != createdAt.year ||
|
||||
now.month != createdAt.month ||
|
||||
now.day != createdAt.day) {
|
||||
stringDate = Jiffy(createdAt.toLocal()).format('dd/MM/yyyy');
|
||||
} else {
|
||||
stringDate = Jiffy(lastUpdatedAt.toLocal()).format('HH:mm');
|
||||
stringDate = Jiffy(createdAt.toLocal()).format('HH:mm');
|
||||
}
|
||||
|
||||
return Text(
|
||||
|
||||
@@ -14,6 +14,10 @@ typedef MessageSearchItemTapCallback = void Function(GetMessageResponse);
|
||||
typedef MessageSearchItemBuilder = Widget Function(
|
||||
BuildContext, GetMessageResponse);
|
||||
|
||||
/// Builder used when [MessageSearchListView] is empty
|
||||
typedef EmptyMessageSearchBuilder = Widget Function(
|
||||
BuildContext context, String searchQuery);
|
||||
|
||||
///
|
||||
/// It shows the list of searched messages.
|
||||
///
|
||||
@@ -85,7 +89,7 @@ class MessageSearchListView extends StatefulWidget {
|
||||
final MessageSearchItemTapCallback onItemTap;
|
||||
|
||||
/// The builder used when the channel list is empty.
|
||||
final WidgetBuilder emptyBuilder;
|
||||
final EmptyMessageSearchBuilder emptyBuilder;
|
||||
|
||||
/// The builder that will be used in case of error
|
||||
final Widget Function(Error error) errorBuilder;
|
||||
@@ -250,11 +254,10 @@ class _MessageSearchListViewState extends State<MessageSearchListView> {
|
||||
|
||||
final items = snapshot.data;
|
||||
|
||||
if (items.isEmpty && widget.emptyBuilder != null) {
|
||||
return widget.emptyBuilder(context);
|
||||
}
|
||||
|
||||
if (items.isEmpty && widget.emptyBuilder == null) {
|
||||
if (items.isEmpty) {
|
||||
if (widget.emptyBuilder != null) {
|
||||
return widget.emptyBuilder(context, widget.messageQuery);
|
||||
}
|
||||
return LayoutBuilder(
|
||||
builder: (context, viewportConstraints) {
|
||||
return SingleChildScrollView(
|
||||
|
||||
+329
-255
@@ -111,6 +111,9 @@ class MessageWidget extends StatefulWidget {
|
||||
/// If true the widget will show the reply indicator
|
||||
final bool showReplyIndicator;
|
||||
|
||||
/// If true the widget will show the show in channel indicator
|
||||
final bool showInChannelIndicator;
|
||||
|
||||
/// The function called when tapping on UserAvatar
|
||||
final void Function(User) onUserAvatarTap;
|
||||
|
||||
@@ -132,6 +135,7 @@ class MessageWidget extends StatefulWidget {
|
||||
/// Center user avatar with bottom of the message
|
||||
final bool translateUserAvatar;
|
||||
|
||||
///
|
||||
MessageWidget({
|
||||
Key key,
|
||||
@required this.message,
|
||||
@@ -149,8 +153,9 @@ class MessageWidget extends StatefulWidget {
|
||||
this.showReactionPickerIndicator = false,
|
||||
this.showUserAvatar = DisplayWidget.show,
|
||||
this.showSendingIndicator = DisplayWidget.show,
|
||||
this.showReplyIndicator = true,
|
||||
this.showThreadReplyIndicator = true,
|
||||
this.showInChannelIndicator = true,
|
||||
this.showReplyIndicator = true,
|
||||
this.onReplyTap,
|
||||
this.onThreadTap,
|
||||
this.showUsername = true,
|
||||
@@ -223,12 +228,25 @@ class MessageWidget extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _MessageWidgetState extends State<MessageWidget> {
|
||||
bool get showThreadReplyIndicator =>
|
||||
widget.showThreadReplyIndicator && widget.message.replyCount > 0;
|
||||
|
||||
bool get showUsername => widget.showUsername;
|
||||
|
||||
bool get showTimeStamp =>
|
||||
widget.message.createdAt != null && widget.showTimestamp;
|
||||
|
||||
bool get showReadList => widget.readList?.isNotEmpty == true;
|
||||
|
||||
bool get showInChannel =>
|
||||
widget.showInChannelIndicator && widget.message?.showInChannel == true;
|
||||
|
||||
bool get _hasReplyMessage => widget.replyMessage != null;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var leftPadding = widget.showUserAvatar != DisplayWidget.gone
|
||||
? widget.messageTheme.avatarTheme.constraints.maxWidth + 16.0
|
||||
? widget.messageTheme.avatarTheme.constraints.maxWidth + 14.5
|
||||
: 6.0;
|
||||
|
||||
final isGiphy =
|
||||
@@ -258,160 +276,179 @@ class _MessageWidgetState extends State<MessageWidget> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
Stack(
|
||||
alignment: AlignmentDirectional.bottomStart,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
if (widget.showUserAvatar == DisplayWidget.show)
|
||||
_buildUserAvatar(),
|
||||
SizedBox(
|
||||
width: 6,
|
||||
),
|
||||
if (widget.showUserAvatar == DisplayWidget.hide)
|
||||
SizedBox(
|
||||
width: widget.messageTheme.avatarTheme.constraints
|
||||
.maxWidth +
|
||||
8,
|
||||
),
|
||||
Flexible(
|
||||
child: PortalEntry(
|
||||
portal: Container(
|
||||
transform: Matrix4.translationValues(-16, 2, 0),
|
||||
child: _buildReactionIndicator(context),
|
||||
constraints: BoxConstraints(maxWidth: 22 * 6.0),
|
||||
),
|
||||
portalAnchor: Alignment(-1.0, -1.0),
|
||||
childAnchor: Alignment(1, -1.0),
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Padding(
|
||||
padding: widget.showReactions
|
||||
? EdgeInsets.only(
|
||||
top: widget.message.reactionCounts
|
||||
?.isNotEmpty ==
|
||||
true
|
||||
? 18
|
||||
: 0,
|
||||
)
|
||||
: EdgeInsets.zero,
|
||||
child: (widget.message.isDeleted &&
|
||||
widget.message.status !=
|
||||
MessageSendingStatus
|
||||
.FAILED_DELETE)
|
||||
? Transform(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
if (widget.showUserAvatar == DisplayWidget.show)
|
||||
_buildUserAvatar(),
|
||||
SizedBox(width: 6),
|
||||
if (widget.showUserAvatar == DisplayWidget.hide)
|
||||
SizedBox(
|
||||
width: widget.messageTheme.avatarTheme
|
||||
.constraints.maxWidth +
|
||||
8,
|
||||
),
|
||||
Flexible(
|
||||
child: PortalEntry(
|
||||
portal: Container(
|
||||
transform:
|
||||
Matrix4.translationValues(-16, 2, 0),
|
||||
child: _buildReactionIndicator(context),
|
||||
constraints:
|
||||
BoxConstraints(maxWidth: 22 * 6.0),
|
||||
),
|
||||
portalAnchor: Alignment(-1.0, -1.0),
|
||||
childAnchor: Alignment(1, -1.0),
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Padding(
|
||||
padding: widget.showReactions
|
||||
? EdgeInsets.only(
|
||||
top: widget.message.reactionCounts
|
||||
?.isNotEmpty ==
|
||||
true
|
||||
? 18
|
||||
: 0,
|
||||
)
|
||||
: EdgeInsets.zero,
|
||||
child:
|
||||
(widget.message.isDeleted &&
|
||||
widget.message.status !=
|
||||
MessageSendingStatus
|
||||
.FAILED_DELETE)
|
||||
? Transform(
|
||||
alignment: Alignment.center,
|
||||
transform: Matrix4.rotationY(
|
||||
widget.reverse ? pi : 0),
|
||||
child: DeletedMessage(
|
||||
reverse: widget.reverse,
|
||||
borderRadiusGeometry: widget
|
||||
.borderRadiusGeometry,
|
||||
borderSide:
|
||||
widget.borderSide,
|
||||
shape: widget.shape,
|
||||
messageTheme:
|
||||
widget.messageTheme,
|
||||
),
|
||||
)
|
||||
: GestureDetector(
|
||||
onTap: () =>
|
||||
retryMessage(context),
|
||||
onLongPress: () =>
|
||||
onLongPress(context),
|
||||
child: Material(
|
||||
clipBehavior:
|
||||
Clip.antiAlias,
|
||||
shape: widget.shape ??
|
||||
RoundedRectangleBorder(
|
||||
side: isOnlyEmoji &&
|
||||
!_hasReplyMessage
|
||||
? 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>[
|
||||
if (_hasReplyMessage)
|
||||
ReplyMessageWidget(
|
||||
message: widget
|
||||
.replyMessage,
|
||||
messageTheme: isMyMessage
|
||||
? StreamChatTheme.of(
|
||||
context)
|
||||
.otherMessageTheme
|
||||
: StreamChatTheme.of(
|
||||
context)
|
||||
.ownMessageTheme,
|
||||
reverse: widget
|
||||
.reverse,
|
||||
),
|
||||
..._parseAttachments(
|
||||
context),
|
||||
if (widget
|
||||
.message.text
|
||||
.trim()
|
||||
.isNotEmpty &&
|
||||
!isGiphy)
|
||||
_buildTextBubble(
|
||||
context),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (widget.showReactionPickerIndicator)
|
||||
Positioned(
|
||||
right: 0,
|
||||
top: -6,
|
||||
child: Transform(
|
||||
transform: Matrix4.rotationY(
|
||||
widget.reverse ? pi : 0),
|
||||
child: DeletedMessage(
|
||||
reverse: widget.reverse,
|
||||
borderRadiusGeometry:
|
||||
widget.borderRadiusGeometry,
|
||||
borderSide: widget.borderSide,
|
||||
shape: widget.shape,
|
||||
messageTheme: widget.messageTheme,
|
||||
),
|
||||
)
|
||||
: GestureDetector(
|
||||
onTap: () => retryMessage(context),
|
||||
onLongPress: () =>
|
||||
onLongPress(context),
|
||||
child: Material(
|
||||
clipBehavior: Clip.antiAlias,
|
||||
shape: widget.shape ??
|
||||
RoundedRectangleBorder(
|
||||
side: isOnlyEmoji &&
|
||||
!_hasReplyMessage
|
||||
? 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>[
|
||||
if (_hasReplyMessage)
|
||||
ReplyMessageWidget(
|
||||
message:
|
||||
widget.replyMessage,
|
||||
messageTheme: isMyMessage
|
||||
? StreamChatTheme.of(
|
||||
context)
|
||||
.otherMessageTheme
|
||||
: StreamChatTheme.of(
|
||||
context)
|
||||
.ownMessageTheme,
|
||||
reverse: widget.reverse,
|
||||
),
|
||||
..._parseAttachments(context),
|
||||
if (widget.message.text
|
||||
.trim()
|
||||
.isNotEmpty &&
|
||||
!isGiphy)
|
||||
_buildTextBubble(context),
|
||||
],
|
||||
),
|
||||
child: CustomPaint(
|
||||
painter: ReactionBubblePainter(
|
||||
widget.messageTheme
|
||||
.reactionsBackgroundColor,
|
||||
widget.messageTheme
|
||||
.reactionsBorderColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (widget.showReactionPickerIndicator)
|
||||
Positioned(
|
||||
right: 0,
|
||||
top: -6,
|
||||
child: Transform(
|
||||
transform: Matrix4.rotationY(
|
||||
widget.reverse ? pi : 0),
|
||||
child: CustomPaint(
|
||||
painter: ReactionBubblePainter(
|
||||
widget.messageTheme
|
||||
.reactionsBackgroundColor,
|
||||
widget.messageTheme
|
||||
.reactionsBorderColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (showThreadReplyIndicator ||
|
||||
showUsername ||
|
||||
showTimeStamp ||
|
||||
showInChannel)
|
||||
SizedBox(height: 20.0),
|
||||
],
|
||||
),
|
||||
if (widget.showThreadReplyIndicator &&
|
||||
widget.message.replyCount > 0)
|
||||
_buildReplyIndicator(leftPadding),
|
||||
if (showThreadReplyIndicator ||
|
||||
showUsername ||
|
||||
showTimeStamp ||
|
||||
showInChannel)
|
||||
_buildBottomRow(leftPadding)
|
||||
],
|
||||
),
|
||||
if ((widget.message.createdAt != null &&
|
||||
widget.showTimestamp) ||
|
||||
widget.showUsername ||
|
||||
widget.readList?.isNotEmpty == true)
|
||||
_buildBottomRow(leftPadding),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -420,6 +457,119 @@ class _MessageWidgetState extends State<MessageWidget> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBottomRow(double leftPadding) {
|
||||
final deleted = widget.message.isDeleted;
|
||||
var children = <Widget>[];
|
||||
if (deleted) {
|
||||
children.add(
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
StreamSvgIcon.eye(
|
||||
color: Colors.black.withOpacity(0.5),
|
||||
size: 16.0,
|
||||
),
|
||||
SizedBox(width: 8.0),
|
||||
Text(
|
||||
'Only visible to you',
|
||||
style: TextStyle(
|
||||
color: Colors.black.withOpacity(0.5),
|
||||
fontSize: 12.0,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
} else if (showInChannel) {
|
||||
final onThreadTap = () async {
|
||||
try {
|
||||
final channel = StreamChannel.of(context);
|
||||
final message = await channel.getMessage(widget.message.parentId);
|
||||
return widget.onThreadTap(message);
|
||||
} catch (e, stk) {
|
||||
print(e);
|
||||
print(stk);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
children.add(
|
||||
InkWell(
|
||||
onTap: widget.onThreadTap != null ? onThreadTap : null,
|
||||
child: Text('Thread Reply', style: widget.messageTheme?.replies),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
final showSendingIndicator =
|
||||
widget.showSendingIndicator == DisplayWidget.show;
|
||||
final replyCount = widget.message.replyCount;
|
||||
final msg = replyCount != 0
|
||||
? '$replyCount ${replyCount > 1 ? 'Thread Replies' : 'Thread Reply'}'
|
||||
: 'Thread Reply';
|
||||
|
||||
final onThreadTap = () async {
|
||||
var message = widget.message;
|
||||
return widget.onThreadTap(message);
|
||||
};
|
||||
|
||||
children.addAll([
|
||||
if (showSendingIndicator) _buildSendingIndicator(),
|
||||
if (showReadList)
|
||||
SizedBox.fromSize(
|
||||
size: Size((widget.readList.length * 10.0) + 10, 17),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 4.0),
|
||||
child: _buildReadIndicator(),
|
||||
),
|
||||
),
|
||||
if (showThreadReplyIndicator)
|
||||
InkWell(
|
||||
onTap: widget.onThreadTap != null ? onThreadTap : null,
|
||||
child: Text(msg, style: widget.messageTheme?.replies),
|
||||
),
|
||||
if (showUsername)
|
||||
Text(
|
||||
widget.message.user.name,
|
||||
style: widget.messageTheme.replies.copyWith(
|
||||
color: widget.messageTheme.createdAt.color,
|
||||
),
|
||||
),
|
||||
if (showTimeStamp)
|
||||
Text(
|
||||
Jiffy(widget.message.createdAt.toLocal()).jm,
|
||||
style: widget.messageTheme.createdAt,
|
||||
),
|
||||
]);
|
||||
}
|
||||
if (widget.reverse) children = children.reversed.toList();
|
||||
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(left: leftPadding),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
if (!deleted && (showThreadReplyIndicator || showInChannel))
|
||||
Container(
|
||||
margin: EdgeInsets.only(
|
||||
bottom: widget.messageTheme.replies.fontSize / 2),
|
||||
child: CustomPaint(
|
||||
size: const Size(16, 32),
|
||||
painter: _ThreadReplyPainter(
|
||||
color: widget.messageTheme.replyThreadColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
...children.map(
|
||||
(child) => Transform(
|
||||
transform: Matrix4.rotationY(widget.reverse ? pi : 0),
|
||||
alignment: Alignment.center,
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
].insertBetween(const SizedBox(width: 8.0)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildUrlAttachment() {
|
||||
var urlAttachment = widget.message.attachments
|
||||
.firstWhere((element) => element.ogScrapeUrl != null);
|
||||
@@ -438,87 +588,6 @@ class _MessageWidgetState extends State<MessageWidget> {
|
||||
);
|
||||
}
|
||||
|
||||
Padding _buildBottomRow(double leftPadding) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: leftPadding,
|
||||
top: 2,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Transform(
|
||||
alignment: Alignment.center,
|
||||
transform: Matrix4.rotationY(widget.reverse ? pi : 0),
|
||||
child: RichText(
|
||||
text: TextSpan(
|
||||
style: widget.messageTheme.createdAt,
|
||||
children: <TextSpan>[
|
||||
if (widget.showUsername)
|
||||
TextSpan(
|
||||
text: widget.message.user.name,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: widget.messageTheme.createdAt.color
|
||||
.withOpacity(1)),
|
||||
),
|
||||
if (widget.message.createdAt != null && widget.showTimestamp)
|
||||
TextSpan(
|
||||
text: Jiffy(widget.message.createdAt.toLocal())
|
||||
.format(' HH:mm'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (widget.showSendingIndicator == DisplayWidget.show)
|
||||
_buildSendingIndicator(),
|
||||
if (widget.readList?.isNotEmpty == true)
|
||||
SizedBox.fromSize(
|
||||
size: Size((widget.readList.length * 10.0) + 10, 17),
|
||||
child: Transform(
|
||||
alignment: Alignment.center,
|
||||
transform: Matrix4.rotationY(widget.reverse ? pi : 0),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 4.0),
|
||||
child: _buildReadIndicator(),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (widget.message.isDeleted)
|
||||
Transform(
|
||||
transform: Matrix4.rotationY(widget.reverse ? pi : 0),
|
||||
alignment: Alignment.center,
|
||||
child: Padding(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
StreamSvgIcon.eye(
|
||||
color: Colors.black.withOpacity(0.5),
|
||||
size: 16.0,
|
||||
),
|
||||
SizedBox(
|
||||
width: 8.0,
|
||||
),
|
||||
Text(
|
||||
'Only visible to you',
|
||||
style: TextStyle(
|
||||
color: Colors.black.withOpacity(0.5),
|
||||
fontSize: 12.0,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
bool get isGiphy =>
|
||||
widget.message.attachments?.any((element) => element.type == 'giphy') ==
|
||||
true;
|
||||
@@ -603,6 +672,7 @@ class _MessageWidgetState extends State<MessageWidget> {
|
||||
editMessageInputBuilder: widget.editMessageInputBuilder,
|
||||
onReplyTap: widget.onReplyTap,
|
||||
onThreadReplyTap: widget.onThreadTap,
|
||||
showCopyMessage: widget.message.text?.trim()?.isNotEmpty == true,
|
||||
showEditMessage: widget.showEditMessage &&
|
||||
widget.message.attachments
|
||||
?.any((element) => element.type == 'giphy') !=
|
||||
@@ -756,38 +826,13 @@ class _MessageWidgetState extends State<MessageWidget> {
|
||||
return;
|
||||
}
|
||||
|
||||
Widget _buildReplyIndicator(double leftPadding) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: leftPadding,
|
||||
),
|
||||
child: Transform(
|
||||
transform: Matrix4.rotationY(widget.reverse ? pi : 0),
|
||||
alignment: Alignment.center,
|
||||
child: ReplyIndicator(
|
||||
message: widget.message,
|
||||
reversed: widget.reverse,
|
||||
messageTheme: widget.messageTheme,
|
||||
onTap: widget.onThreadTap != null
|
||||
? () {
|
||||
widget.onThreadTap(widget.message);
|
||||
}
|
||||
: null,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSendingIndicator() {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(right: 4.0),
|
||||
child: Transform(
|
||||
transform: Matrix4.rotationY(widget.reverse ? pi : 0),
|
||||
alignment: Alignment.center,
|
||||
child: SendingIndicator(
|
||||
message: widget.message,
|
||||
allRead: widget.allRead,
|
||||
),
|
||||
return Container(
|
||||
height: widget.messageTheme.createdAt.fontSize + 2,
|
||||
width: widget.messageTheme.createdAt.fontSize + 2,
|
||||
child: SendingIndicator(
|
||||
message: widget.message,
|
||||
allRead: widget.allRead,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -958,3 +1003,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;
|
||||
}
|
||||
|
||||
@@ -136,7 +136,9 @@ class _ReactionPickerState extends State<ReactionPicker>
|
||||
void sendReaction(BuildContext context, String reactionType) {
|
||||
StreamChannel.of(context)
|
||||
.channel
|
||||
.sendReaction(widget.message, reactionType);
|
||||
.sendReaction(widget.message, reactionType, extraData: {
|
||||
'enforce_unique': true,
|
||||
});
|
||||
pop();
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+262
-86
@@ -4,23 +4,29 @@ import 'package:flutter/material.dart';
|
||||
import 'package:rxdart/rxdart.dart';
|
||||
import 'package:stream_chat/stream_chat.dart';
|
||||
|
||||
enum QueryDirection { top, bottom }
|
||||
|
||||
/// Widget used to provide information about the channel to the widget tree
|
||||
///
|
||||
/// Use [StreamChannel.of] to get the current [StreamChannelState] instance.
|
||||
class StreamChannel extends StatefulWidget {
|
||||
StreamChannel({
|
||||
const StreamChannel({
|
||||
Key key,
|
||||
@required this.child,
|
||||
@required this.channel,
|
||||
this.showLoading = true,
|
||||
}) : super(
|
||||
key: key,
|
||||
);
|
||||
this.initialMessageId,
|
||||
}) : assert(child != null),
|
||||
assert(channel != null),
|
||||
super(key: key);
|
||||
|
||||
final Widget child;
|
||||
final Channel channel;
|
||||
final bool showLoading;
|
||||
|
||||
/// If passed the channel will load from this particular message.
|
||||
final String initialMessageId;
|
||||
|
||||
/// Use this method to get the current [StreamChannelState] instance
|
||||
static StreamChannelState of(BuildContext context) {
|
||||
StreamChannelState streamChannelState;
|
||||
@@ -43,87 +49,124 @@ class StreamChannelState extends State<StreamChannel> {
|
||||
/// Current channel
|
||||
Channel get channel => widget.channel;
|
||||
|
||||
/// InitialMessageId
|
||||
String get initialMessageId => widget.initialMessageId;
|
||||
|
||||
/// Current channel state stream
|
||||
Stream<ChannelState> get channelStateStream =>
|
||||
widget.channel.state.channelStateStream;
|
||||
|
||||
final BehaviorSubject<bool> _queryMessageController = BehaviorSubject();
|
||||
final _queryTopMessagesController = BehaviorSubject.seeded(false);
|
||||
final _queryBottomMessagesController = BehaviorSubject.seeded(false);
|
||||
|
||||
/// The stream notifying the state of queryMessage call
|
||||
Stream<bool> get queryMessage => _queryMessageController.stream;
|
||||
/// The stream notifying the state of [_queryTopMessages] call
|
||||
Stream<bool> get queryTopMessages => _queryTopMessagesController.stream;
|
||||
|
||||
bool _paginationEnded = false;
|
||||
/// The stream notifying the state of [_queryBottomMessages] call
|
||||
Stream<bool> get queryBottomMessages => _queryBottomMessagesController.stream;
|
||||
|
||||
/// Calls [channel.query] updating [queryMessage] stream
|
||||
void queryMessages() {
|
||||
if (_queryMessageController.value == true || _paginationEnded) {
|
||||
bool _topPaginationEnded = false;
|
||||
bool _bottomPaginationEnded = false;
|
||||
|
||||
Future<void> _queryTopMessages({
|
||||
int limit = 20,
|
||||
bool preferOffline = false,
|
||||
}) async {
|
||||
if (_topPaginationEnded || _queryTopMessagesController?.value == true) {
|
||||
return;
|
||||
}
|
||||
_queryTopMessagesController.add(true);
|
||||
|
||||
_queryMessageController.add(true);
|
||||
|
||||
String firstId;
|
||||
if (channel.state.messages.isNotEmpty) {
|
||||
firstId = channel.state.messages.first.id;
|
||||
if (channel.state.messages.isEmpty) {
|
||||
return _queryTopMessagesController.add(false);
|
||||
}
|
||||
|
||||
final messageLimit = 50;
|
||||
final oldestMessage = channel.state.messages.first;
|
||||
|
||||
widget.channel
|
||||
.query(
|
||||
messagesPagination: PaginationParams(
|
||||
lessThan: firstId,
|
||||
limit: messageLimit,
|
||||
),
|
||||
preferOffline: true,
|
||||
)
|
||||
.then((res) {
|
||||
if (res.messages.isEmpty || res.messages.length < messageLimit) {
|
||||
_paginationEnded = true;
|
||||
try {
|
||||
final state = await queryBeforeMessage(
|
||||
oldestMessage.id,
|
||||
limit: limit,
|
||||
preferOffline: preferOffline,
|
||||
);
|
||||
if (state.messages.isEmpty || state.messages.length < limit) {
|
||||
_topPaginationEnded = true;
|
||||
}
|
||||
_queryMessageController.add(false);
|
||||
}).catchError((e, stack) {
|
||||
if (!_queryMessageController.isClosed) {
|
||||
_queryMessageController.addError(e, stack);
|
||||
_queryTopMessagesController.add(false);
|
||||
} catch (e, stk) {
|
||||
_queryTopMessagesController.addError(e, stk);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _queryBottomMessages({
|
||||
int limit = 20,
|
||||
bool preferOffline = false,
|
||||
}) async {
|
||||
if (_bottomPaginationEnded ||
|
||||
_queryBottomMessagesController?.value == true ||
|
||||
channel?.state?.isUpToDate == true) return;
|
||||
_queryBottomMessagesController.add(true);
|
||||
|
||||
if (channel.state.messages.isEmpty) {
|
||||
return _queryBottomMessagesController.add(false);
|
||||
}
|
||||
|
||||
final recentMessage = channel.state.messages.last;
|
||||
|
||||
try {
|
||||
final state = await queryAfterMessage(
|
||||
recentMessage.id,
|
||||
limit: limit,
|
||||
preferOffline: preferOffline,
|
||||
);
|
||||
if (state.messages.isEmpty || state.messages.length < limit) {
|
||||
_bottomPaginationEnded = true;
|
||||
}
|
||||
});
|
||||
_queryBottomMessagesController.add(false);
|
||||
} catch (e, stk) {
|
||||
_queryBottomMessagesController.addError(e, stk);
|
||||
}
|
||||
}
|
||||
|
||||
/// Calls [channel.query] updating [queryMessage] stream
|
||||
Future<void> queryMessages({QueryDirection direction = QueryDirection.top}) {
|
||||
if (direction == QueryDirection.top) return _queryTopMessages();
|
||||
return _queryBottomMessages();
|
||||
}
|
||||
|
||||
/// Calls [channel.getReplies] updating [queryMessage] stream
|
||||
Future<void> getReplies(String parentId) async {
|
||||
if (_queryMessageController.value == true || _paginationEnded) {
|
||||
return;
|
||||
}
|
||||
Future<void> getReplies(
|
||||
String parentId, {
|
||||
int limit = 50,
|
||||
bool preferOffline = false,
|
||||
}) async {
|
||||
if (_topPaginationEnded || _queryTopMessagesController.value) return;
|
||||
_queryTopMessagesController.add(true);
|
||||
|
||||
_queryMessageController.add(true);
|
||||
|
||||
String firstId;
|
||||
if (widget.channel.state.threads.containsKey(parentId)) {
|
||||
final thread = widget.channel.state.threads[parentId];
|
||||
|
||||
if (thread != null && thread.isNotEmpty) {
|
||||
firstId = thread?.first?.id;
|
||||
Message message;
|
||||
if (channel.state.threads.containsKey(parentId)) {
|
||||
final thread = channel.state.threads[parentId];
|
||||
if (thread.isNotEmpty) {
|
||||
message = thread.first;
|
||||
}
|
||||
}
|
||||
|
||||
final messageLimit = 50;
|
||||
return widget.channel
|
||||
.getReplies(
|
||||
parentId,
|
||||
PaginationParams(
|
||||
lessThan: firstId,
|
||||
limit: messageLimit,
|
||||
),
|
||||
preferOffline: true,
|
||||
)
|
||||
.then((res) {
|
||||
if (res.messages.isEmpty || res.messages.length < messageLimit) {
|
||||
_paginationEnded = true;
|
||||
try {
|
||||
final response = await channel.getReplies(
|
||||
parentId,
|
||||
PaginationParams(
|
||||
lessThan: message?.id,
|
||||
limit: limit,
|
||||
),
|
||||
preferOffline: preferOffline,
|
||||
);
|
||||
if (response.messages.isEmpty || response.messages.length < limit) {
|
||||
_topPaginationEnded = true;
|
||||
}
|
||||
_queryMessageController.add(false);
|
||||
}).catchError((e, stack) {
|
||||
_queryMessageController.addError(e, stack);
|
||||
});
|
||||
_queryTopMessagesController.add(false);
|
||||
} catch (e, stk) {
|
||||
_queryTopMessagesController.addError(e, stk);
|
||||
}
|
||||
}
|
||||
|
||||
/// Query the channel members and watchers
|
||||
@@ -140,41 +183,174 @@ class StreamChannelState extends State<StreamChannel> {
|
||||
);
|
||||
}
|
||||
|
||||
/// Loads channel at specific message
|
||||
Future<void> loadChannelAtMessage(
|
||||
String messageId, {
|
||||
int before = 20,
|
||||
int after = 20,
|
||||
bool preferOffline = false,
|
||||
}) {
|
||||
return queryAtMessage(
|
||||
messageId: messageId,
|
||||
before: before,
|
||||
after: after,
|
||||
preferOffline: preferOffline,
|
||||
);
|
||||
}
|
||||
|
||||
///
|
||||
Future<void> queryAtMessage({
|
||||
String messageId,
|
||||
int before = 20,
|
||||
int after = 20,
|
||||
bool preferOffline = false,
|
||||
}) async {
|
||||
if (channel.state == null) return;
|
||||
channel.state.isUpToDate = false;
|
||||
channel.state.truncate();
|
||||
|
||||
if (messageId == null) {
|
||||
await channel.query(
|
||||
messagesPagination: PaginationParams(
|
||||
limit: before,
|
||||
),
|
||||
preferOffline: preferOffline,
|
||||
);
|
||||
channel.state.isUpToDate = true;
|
||||
return;
|
||||
}
|
||||
|
||||
return Future.wait([
|
||||
queryBeforeMessage(
|
||||
messageId,
|
||||
limit: before,
|
||||
preferOffline: preferOffline,
|
||||
),
|
||||
queryAfterMessage(
|
||||
messageId,
|
||||
limit: after,
|
||||
preferOffline: preferOffline,
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
///
|
||||
Future<ChannelState> queryBeforeMessage(
|
||||
String messageId, {
|
||||
int limit = 20,
|
||||
bool preferOffline = false,
|
||||
}) {
|
||||
return channel.query(
|
||||
messagesPagination: PaginationParams(
|
||||
lessThan: messageId,
|
||||
limit: limit,
|
||||
),
|
||||
preferOffline: preferOffline,
|
||||
);
|
||||
}
|
||||
|
||||
///
|
||||
Future<ChannelState> queryAfterMessage(
|
||||
String messageId, {
|
||||
int limit = 20,
|
||||
bool preferOffline = false,
|
||||
}) async {
|
||||
final state = await channel.query(
|
||||
messagesPagination: PaginationParams(
|
||||
greaterThanOrEqual: messageId,
|
||||
limit: limit,
|
||||
),
|
||||
preferOffline: preferOffline,
|
||||
);
|
||||
if (state.messages.isEmpty || state.messages.length < limit) {
|
||||
channel.state.isUpToDate = true;
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
///
|
||||
Future<Message> getMessage(String messageId) async {
|
||||
var message = channel.state.messages.firstWhere(
|
||||
(it) => it.id == messageId,
|
||||
orElse: () => null,
|
||||
);
|
||||
if (message == null) {
|
||||
final response = await channel.getMessagesById([messageId]);
|
||||
message = response.messages.first;
|
||||
}
|
||||
return message;
|
||||
}
|
||||
|
||||
/// Reloads the channel with latest message
|
||||
Future<void> reloadChannel() => queryAtMessage(before: 30);
|
||||
|
||||
List<Future<bool>> _futures;
|
||||
|
||||
Future<bool> get _loadChannelAtMessage async {
|
||||
try {
|
||||
await loadChannelAtMessage(initialMessageId);
|
||||
return true;
|
||||
} catch (e, stk) {
|
||||
print('Error: $e\nStack: $stk');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_futures = [widget.channel.initialized];
|
||||
if (initialMessageId != null) {
|
||||
_futures.add(_loadChannelAtMessage);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_queryMessageController.close();
|
||||
_queryTopMessagesController.close();
|
||||
_queryBottomMessagesController.close();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (widget.channel == null) {
|
||||
return Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
}
|
||||
return FutureBuilder<bool>(
|
||||
future: widget.channel.initialized,
|
||||
initialData: widget.channel.state != null,
|
||||
Widget child = FutureBuilder<List<bool>>(
|
||||
future: Future.wait(_futures),
|
||||
initialData: [
|
||||
channel.state != null,
|
||||
if (initialMessageId != null) false,
|
||||
],
|
||||
builder: (context, snapshot) {
|
||||
if (widget.showLoading && (!snapshot.hasData || !snapshot.data)) {
|
||||
return Container(
|
||||
height: 30,
|
||||
child: Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
if (snapshot.hasError) {
|
||||
if (snapshot.error is Error) {
|
||||
print((snapshot.error as Error).stackTrace);
|
||||
}
|
||||
var message = snapshot.error.toString();
|
||||
if (snapshot.error is DioError) {
|
||||
final dioError = snapshot.error as DioError;
|
||||
if (dioError.type == DioErrorType.RESPONSE) {
|
||||
message = dioError.message;
|
||||
} else {
|
||||
message = 'Check your connection and retry';
|
||||
}
|
||||
}
|
||||
return Center(
|
||||
child: Text(message),
|
||||
);
|
||||
} else if (snapshot.hasError) {
|
||||
return Container(
|
||||
height: 30,
|
||||
child: Center(
|
||||
child: Text(snapshot.error),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return widget.child;
|
||||
}
|
||||
final initialized = snapshot.data[0];
|
||||
final dataLoaded = initialMessageId == null ? true : snapshot.data[1];
|
||||
if (widget.showLoading && (!initialized || !dataLoaded)) {
|
||||
return Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
}
|
||||
return widget.child;
|
||||
},
|
||||
);
|
||||
if (initialMessageId != null) {
|
||||
child = Material(child: child);
|
||||
}
|
||||
return child;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -191,6 +191,8 @@ class StreamChatThemeData {
|
||||
this.ownMessageTheme.messageBackgroundColor,
|
||||
avatarTheme: ownMessageTheme?.avatarTheme ??
|
||||
this.ownMessageTheme.avatarTheme,
|
||||
replyThreadColor: ownMessageTheme?.replyThreadColor ??
|
||||
this.ownMessageTheme.replyThreadColor,
|
||||
) ??
|
||||
this.ownMessageTheme,
|
||||
otherMessageTheme: otherMessageTheme?.copyWith(
|
||||
@@ -209,6 +211,8 @@ class StreamChatThemeData {
|
||||
this.otherMessageTheme.messageBackgroundColor,
|
||||
avatarTheme: otherMessageTheme?.avatarTheme ??
|
||||
this.otherMessageTheme.avatarTheme,
|
||||
replyThreadColor: ownMessageTheme?.replyThreadColor ??
|
||||
this.ownMessageTheme.replyThreadColor,
|
||||
) ??
|
||||
this.otherMessageTheme,
|
||||
reactionIcons: reactionIcons ?? this.reactionIcons,
|
||||
@@ -297,16 +301,17 @@ class StreamChatThemeData {
|
||||
color: isDark
|
||||
? Colors.white.withOpacity(.5)
|
||||
: Colors.black.withOpacity(.5),
|
||||
fontSize: 11,
|
||||
fontSize: 12,
|
||||
),
|
||||
replies: TextStyle(
|
||||
color: accentColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 12,
|
||||
),
|
||||
messageBackgroundColor: isDark ? Color(0xff191919) : Color(0xffEAEAEA),
|
||||
reactionsBackgroundColor: isDark ? Colors.black : Colors.white,
|
||||
reactionsBorderColor: isDark ? Color(0xff191919) : Color(0xffEAEAEA),
|
||||
replyThreadColor: isDark ? Color(0xff191919) : Color(0xffEAEAEA),
|
||||
avatarTheme: AvatarTheme(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
constraints: BoxConstraints.tightFor(
|
||||
@@ -330,17 +335,19 @@ class StreamChatThemeData {
|
||||
color: isDark
|
||||
? Colors.white.withOpacity(.5)
|
||||
: Colors.black.withOpacity(.5),
|
||||
fontSize: 11,
|
||||
fontSize: 12,
|
||||
),
|
||||
replies: TextStyle(
|
||||
color: accentColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 12,
|
||||
),
|
||||
messageLinks: TextStyle(
|
||||
color: accentColor,
|
||||
),
|
||||
messageBackgroundColor: isDark ? Colors.black : Colors.white,
|
||||
replyThreadColor:
|
||||
isDark ? Colors.white.withAlpha(24) : Colors.black.withAlpha(24),
|
||||
avatarTheme: AvatarTheme(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
constraints: BoxConstraints.tightFor(
|
||||
@@ -455,6 +462,7 @@ class MessageTheme {
|
||||
final Color messageBackgroundColor;
|
||||
final Color reactionsBackgroundColor;
|
||||
final Color reactionsBorderColor;
|
||||
final Color replyThreadColor;
|
||||
final AvatarTheme avatarTheme;
|
||||
|
||||
const MessageTheme({
|
||||
@@ -465,6 +473,7 @@ class MessageTheme {
|
||||
this.messageBackgroundColor,
|
||||
this.reactionsBackgroundColor,
|
||||
this.reactionsBorderColor,
|
||||
this.replyThreadColor,
|
||||
this.avatarTheme,
|
||||
this.createdAt,
|
||||
});
|
||||
@@ -479,6 +488,7 @@ class MessageTheme {
|
||||
AvatarTheme avatarTheme,
|
||||
Color reactionsBackgroundColor,
|
||||
Color reactionsBorderColor,
|
||||
Color replyThreadColor,
|
||||
}) =>
|
||||
MessageTheme(
|
||||
messageText: messageText ?? this.messageText,
|
||||
@@ -492,6 +502,7 @@ class MessageTheme {
|
||||
reactionsBackgroundColor:
|
||||
reactionsBackgroundColor ?? this.reactionsBackgroundColor,
|
||||
reactionsBorderColor: reactionsBorderColor ?? this.reactionsBorderColor,
|
||||
replyThreadColor: replyThreadColor ?? this.replyThreadColor,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -733,4 +733,40 @@ class StreamSvgIcon extends StatelessWidget {
|
||||
height: size,
|
||||
);
|
||||
}
|
||||
|
||||
factory StreamSvgIcon.Icon_group({
|
||||
double size,
|
||||
Color color,
|
||||
}) {
|
||||
return StreamSvgIcon(
|
||||
assetName: 'Icon_group.svg',
|
||||
color: color,
|
||||
width: size,
|
||||
height: size,
|
||||
);
|
||||
}
|
||||
|
||||
factory StreamSvgIcon.Icon_notification({
|
||||
double size,
|
||||
Color color,
|
||||
}) {
|
||||
return StreamSvgIcon(
|
||||
assetName: 'Icon_notification.svg',
|
||||
color: color,
|
||||
width: size,
|
||||
height: size,
|
||||
);
|
||||
}
|
||||
|
||||
factory StreamSvgIcon.Icon_user_delete({
|
||||
double size,
|
||||
Color color,
|
||||
}) {
|
||||
return StreamSvgIcon(
|
||||
assetName: 'Icon_user_delete.svg',
|
||||
color: color,
|
||||
width: size,
|
||||
height: size,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+45
-36
@@ -1,7 +1,9 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat/stream_chat.dart';
|
||||
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
|
||||
import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
|
||||
|
||||
import 'back_button.dart';
|
||||
import 'channel_name.dart';
|
||||
|
||||
/// 
|
||||
/// 
|
||||
@@ -77,43 +79,50 @@ class ThreadHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||
return AppBar(
|
||||
automaticallyImplyLeading: false,
|
||||
elevation: 1,
|
||||
leading: showBackButton
|
||||
? StreamBackButton(
|
||||
onPressed: onBackPressed,
|
||||
showUnreads: true,
|
||||
)
|
||||
: SizedBox(),
|
||||
backgroundColor:
|
||||
StreamChatTheme.of(context).channelTheme.channelHeaderTheme.color,
|
||||
actions: <Widget>[
|
||||
Container(
|
||||
child: showBackButton
|
||||
? AspectRatio(
|
||||
aspectRatio: 1,
|
||||
child: IconButton(
|
||||
onPressed: onBackPressed ?? () => Navigator.pop(context),
|
||||
icon: StreamSvgIcon.close(
|
||||
size: 24,
|
||||
color: Theme.of(context).brightness == Brightness.dark
|
||||
? Colors.white
|
||||
: Colors.black,
|
||||
),
|
||||
),
|
||||
)
|
||||
: SizedBox(),
|
||||
),
|
||||
],
|
||||
centerTitle: false,
|
||||
title: Text.rich(
|
||||
TextSpan(
|
||||
text: 'Thread',
|
||||
children: [
|
||||
TextSpan(
|
||||
text:
|
||||
' ${parent.replyCount} ${parent.replyCount == 1 ? 'reply' : 'replies'}',
|
||||
style: StreamChatTheme.of(context)
|
||||
.channelTheme
|
||||
.channelHeaderTheme
|
||||
.lastMessageAt,
|
||||
),
|
||||
],
|
||||
),
|
||||
style:
|
||||
StreamChatTheme.of(context).channelTheme.channelHeaderTheme.title,
|
||||
centerTitle: true,
|
||||
title: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
'Thread Reply',
|
||||
style: StreamChatTheme.of(context)
|
||||
.channelTheme
|
||||
.channelHeaderTheme
|
||||
.title,
|
||||
),
|
||||
SizedBox(height: 2),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
'with ',
|
||||
style: StreamChatTheme.of(context)
|
||||
.channelTheme
|
||||
.channelHeaderTheme
|
||||
.lastMessageAt,
|
||||
),
|
||||
Flexible(
|
||||
child: ChannelName(
|
||||
textStyle: StreamChatTheme.of(context)
|
||||
.channelTheme
|
||||
.channelHeaderTheme
|
||||
.lastMessageAt,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -78,8 +78,9 @@ class UrlAttachment extends StatelessWidget {
|
||||
children: [
|
||||
if (urlAttachment.title != null)
|
||||
Text(
|
||||
urlAttachment.title,
|
||||
urlAttachment.title.trim(),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w700,
|
||||
fontSize: 12.0,
|
||||
|
||||
@@ -90,6 +90,8 @@ class UserItem extends StatelessWidget {
|
||||
}
|
||||
|
||||
Widget _buildLastActive(context) {
|
||||
return Text('Last online ${Jiffy(user.lastActive).fromNow()}');
|
||||
return user.online == true
|
||||
? Text('Online')
|
||||
: Text('Last online ${Jiffy(user.lastActive).fromNow()}');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -190,7 +190,7 @@ class _UserListViewState extends State<UserListView>
|
||||
}
|
||||
final groupedUsers = <String, List<User>>{};
|
||||
for (var e in temp) {
|
||||
final alphabet = e.name[0];
|
||||
final alphabet = e.name[0]?.toUpperCase();
|
||||
groupedUsers[alphabet] = [...groupedUsers[alphabet] ?? [], e];
|
||||
}
|
||||
final items = <ListItem>[];
|
||||
@@ -339,7 +339,9 @@ class _UserListViewState extends State<UserListView>
|
||||
);
|
||||
|
||||
return LazyLoadScrollView(
|
||||
onEndOfPage: () => _listenUserPagination(usersBlocState),
|
||||
onEndOfPage: () async {
|
||||
return _listenUserPagination(usersBlocState);
|
||||
},
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
|
||||
+84
-24
@@ -4,6 +4,8 @@ import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import 'stream_svg_icon.dart';
|
||||
|
||||
import '../stream_chat_flutter.dart';
|
||||
|
||||
Future<void> launchURL(BuildContext context, String url) async {
|
||||
if (await canLaunch(url)) {
|
||||
await launch(url);
|
||||
@@ -17,33 +19,76 @@ Future<void> launchURL(BuildContext context, String url) async {
|
||||
}
|
||||
|
||||
Future<bool> showConfirmationDialog(
|
||||
BuildContext context,
|
||||
BuildContext context, {
|
||||
String title,
|
||||
Widget icon,
|
||||
String question,
|
||||
) {
|
||||
return showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return AlertDialog(
|
||||
title: Text(question),
|
||||
actions: <Widget>[
|
||||
FlatButton(
|
||||
child: Text('Ok'),
|
||||
onPressed: () => Navigator.pop(
|
||||
context,
|
||||
true,
|
||||
String okText,
|
||||
String cancelText,
|
||||
}) {
|
||||
return showModalBottomSheet(
|
||||
backgroundColor: Colors.white,
|
||||
context: context,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(16.0),
|
||||
topRight: Radius.circular(16.0),
|
||||
)),
|
||||
builder: (context) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 26.0,
|
||||
),
|
||||
),
|
||||
FlatButton(
|
||||
child: Text('Cancel'),
|
||||
onPressed: () => Navigator.pop(
|
||||
context,
|
||||
false,
|
||||
if (icon != null) icon,
|
||||
SizedBox(
|
||||
height: 26.0,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16.0),
|
||||
),
|
||||
SizedBox(
|
||||
height: 7.0,
|
||||
),
|
||||
Text(question),
|
||||
SizedBox(
|
||||
height: 36.0,
|
||||
),
|
||||
Container(
|
||||
color: Color(0xffe6e6e6),
|
||||
height: 1.0,
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
FlatButton(
|
||||
child: Text(
|
||||
cancelText,
|
||||
style: TextStyle(
|
||||
color: Colors.black.withOpacity(0.5),
|
||||
fontWeight: FontWeight.w400),
|
||||
),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
FlatButton(
|
||||
child: Text(
|
||||
okText,
|
||||
style: TextStyle(
|
||||
color: Colors.red, fontWeight: FontWeight.w400),
|
||||
),
|
||||
onPressed: () {
|
||||
Navigator.pop(context, true);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/// Get random png with initials
|
||||
@@ -92,6 +137,21 @@ String getWebsiteName(String hostName) {
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
String getSizeText(int bytes) {
|
||||
if (bytes == null) {
|
||||
return 'Size N/A';
|
||||
}
|
||||
|
||||
if (bytes <= 1000) {
|
||||
return '${bytes} bytes';
|
||||
} else if (bytes <= 100000) {
|
||||
return '${(bytes / 1000).toStringAsFixed(2)} KB';
|
||||
} else {
|
||||
return '${(bytes / 1000000).toStringAsFixed(2)} MB';
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
StreamSvgIcon getFileTypeImage(String type) {
|
||||
switch (type) {
|
||||
|
||||
@@ -21,7 +21,6 @@ export 'src/message_list_view.dart';
|
||||
export 'src/message_text.dart';
|
||||
export 'src/message_widget.dart';
|
||||
export 'src/reaction_picker.dart';
|
||||
export 'src/reply_indicator.dart';
|
||||
export 'src/sending_indicator.dart';
|
||||
export 'src/stream_channel.dart';
|
||||
export 'src/stream_chat.dart';
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 11C14.2091 11 16 9.20914 16 7C16 4.79086 14.2091 3 12 3C9.79086 3 8 4.79086 8 7C8 9.20914 9.79086 11 12 11ZM12 9C13.1046 9 14 8.10457 14 7C14 5.89543 13.1046 5 12 5C10.8954 5 10 5.89543 10 7C10 8.10457 10.8954 9 12 9ZM6.5 4C4.567 4 3 5.567 3 7.5C3 9.433 4.567 11 6.5 11C7.05228 11 7.5 10.5523 7.5 10C7.5 9.44772 7.05228 9 6.5 9C5.67157 9 5 8.32843 5 7.5C5 6.67157 5.67157 6 6.5 6C7.05228 6 7.5 5.55228 7.5 5C7.5 4.44772 7.05228 4 6.5 4ZM5 19C5 15.134 8.13401 12 12 12C15.866 12 19 15.134 19 19C19 19.397 18.9669 19.7869 18.903 20.1671C18.8115 20.7118 18.2958 21.0791 17.7511 20.9876C17.2065 20.8961 16.8391 20.3804 16.9306 19.8357C16.9762 19.5646 17 19.2855 17 19C17 16.2386 14.7614 14 12 14C9.23858 14 7 16.2386 7 19C7 19.2864 7.02397 19.5664 7.06981 19.8383C7.16161 20.3829 6.79454 20.8988 6.24994 20.9906C5.70533 21.0824 5.18943 20.7154 5.09763 20.1708C5.03335 19.7894 5 19.3982 5 19ZM4.85402 13.7725C5.28304 13.4247 5.34889 12.7949 5.0011 12.3659C4.6533 11.9369 4.02357 11.8711 3.59455 12.2188C2.01345 13.5006 1 15.4618 1 17.659C1 18.0572 1.03335 18.4484 1.09763 18.8297C1.18943 19.3743 1.70533 19.7414 2.24994 19.6496C2.79454 19.5578 3.16161 19.0419 3.06981 18.4973C3.02397 18.2253 3 17.9453 3 17.659C3 16.0903 3.72123 14.6908 4.85402 13.7725ZM21.5 7.5C21.5 5.567 19.933 4 18 4C17.4477 4 17 4.44772 17 5C17 5.55228 17.4477 6 18 6C18.8284 6 19.5 6.67157 19.5 7.5C19.5 8.32843 18.8284 9 18 9C17.4477 9 17 9.44772 17 10C17 10.5523 17.4477 11 18 11C19.933 11 21.5 9.433 21.5 7.5ZM19.3703 13.7725C18.9413 13.4247 18.8755 12.7949 19.2233 12.3659C19.5711 11.9369 20.2008 11.8711 20.6298 12.2188C22.2109 13.5006 23.2244 15.4618 23.2244 17.659C23.2244 18.0572 23.191 18.4484 23.1267 18.8297C23.0349 19.3743 22.519 19.7414 21.9744 19.6496C21.4298 19.5578 21.0628 19.0419 21.1546 18.4973C21.2004 18.2253 21.2244 17.9453 21.2244 17.659C21.2244 16.0903 20.5031 14.6908 19.3703 13.7725Z" fill="#006CFF"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
@@ -0,0 +1,6 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M14 5C14 6.10457 13.1046 7 12 7C10.8954 7 10 6.10457 10 5C10 3.89543 10.8954 3 12 3C13.1046 3 14 3.89543 14 5Z" fill="black"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M16 16V11C16 8.79086 14.2091 7 12 7C9.79086 7 8 8.79086 8 11V16H16ZM12 5C8.68629 5 6 7.68629 6 11V18H18V11C18 7.68629 15.3137 5 12 5Z" fill="black"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M4 17C4 16.4477 4.44771 16 5 16H19C19.5523 16 20 16.4477 20 17C20 17.5523 19.5523 18 19 18H5C4.44771 18 4 17.5523 4 17Z" fill="black"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M10 19C10 20.1046 10.8954 21 12 21C13.1046 21 14 20.1046 14 19H10Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 795 B |
+1
-1
@@ -28,7 +28,7 @@ dependencies:
|
||||
file_picker: ^2.0.12
|
||||
image_picker: ^0.6.7+2
|
||||
flutter_keyboard_visibility: ^3.3.0
|
||||
stream_chat: ^0.2.14
|
||||
stream_chat: ^0.2.20
|
||||
mime: ^0.9.6+3
|
||||
video_compress: ^2.1.1
|
||||
visibility_detector: ^0.1.5
|
||||
|
||||
Reference in New Issue
Block a user