Merge branch 'feature/new-ui' of https://github.com/GetStream/stream-chat-flutter into feature/group-info-screen
Conflicts: pubspec.yaml
This commit is contained in:
@@ -1 +1 @@
|
|||||||
c3e639ccf9b069e37a7d1345194e6b99
|
13a41b8138c4868054e44b5158f3bdd6
|
||||||
@@ -40,7 +40,7 @@ PODS:
|
|||||||
- Firebase/Messaging (6.33.0):
|
- Firebase/Messaging (6.33.0):
|
||||||
- Firebase/CoreOnly
|
- Firebase/CoreOnly
|
||||||
- FirebaseMessaging (~> 4.7.0)
|
- FirebaseMessaging (~> 4.7.0)
|
||||||
- firebase_core (0.5.2-1):
|
- firebase_core (0.5.3):
|
||||||
- Firebase/CoreOnly (~> 6.33.0)
|
- Firebase/CoreOnly (~> 6.33.0)
|
||||||
- Flutter
|
- Flutter
|
||||||
- firebase_messaging (7.0.3):
|
- firebase_messaging (7.0.3):
|
||||||
@@ -258,7 +258,7 @@ SPEC CHECKSUMS:
|
|||||||
esys_flutter_share: 403498dab005b36ce1f8d7aff377e81f0621b0b4
|
esys_flutter_share: 403498dab005b36ce1f8d7aff377e81f0621b0b4
|
||||||
file_picker: 3e6c3790de664ccf9b882732d9db5eaf6b8d4eb1
|
file_picker: 3e6c3790de664ccf9b882732d9db5eaf6b8d4eb1
|
||||||
Firebase: 8db6f2d1b2c5e2984efba4949a145875a8f65fe5
|
Firebase: 8db6f2d1b2c5e2984efba4949a145875a8f65fe5
|
||||||
firebase_core: 7423d688a1c6f2f2d859d64ae26991be39989781
|
firebase_core: 5d6a02f3d85acd5f8321c2d6d62877626a670659
|
||||||
firebase_messaging: 0aea2cd5885b65e19ede58ee3507f485c992cc75
|
firebase_messaging: 0aea2cd5885b65e19ede58ee3507f485c992cc75
|
||||||
FirebaseCore: d889d9e12535b7f36ac8bfbf1713a0836a3012cd
|
FirebaseCore: d889d9e12535b7f36ac8bfbf1713a0836a3012cd
|
||||||
FirebaseCoreDiagnostics: 770ac5958e1372ce67959ae4b4f31d8e127c3ac1
|
FirebaseCoreDiagnostics: 770ac5958e1372ce67959ae4b4f31d8e127c3ac1
|
||||||
|
|||||||
@@ -5,39 +5,27 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="0: Verifying fastlane version" time="0.00047">
|
<testcase classname="fastlane.lanes" name="0: Verifying fastlane version" time="0.00094">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="1: default_platform" time="0.000176">
|
<testcase classname="fastlane.lanes" name="1: default_platform" time="0.000521">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="2: is_ci" time="0.000168">
|
<testcase classname="fastlane.lanes" name="2: is_ci" time="0.000605">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="3: Switch to ios match_me lane" time="0.000157">
|
<testcase classname="fastlane.lanes" name="3: is_ci" time="0.000675">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="4: is_ci" time="0.000161">
|
<testcase classname="fastlane.lanes" name="4: match" time="48.727951">
|
||||||
|
|
||||||
</testcase>
|
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="5: match" time="32.299713">
|
|
||||||
|
|
||||||
</testcase>
|
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="6: gym" time="9.737819">
|
|
||||||
|
|
||||||
<failure message="/Library/Ruby/Gems/2.6.0/gems/fastlane-2.162.0/fastlane/lib/fastlane/actions/actions_helper.rb:67:in `execute_action' /Library/Ruby/Gems/2.6.0/gems/fastlane-2.162.0/fastlane/lib/fastlane/runner.rb:255:in `block in execute_action' /Library/Ruby/Gems/2.6.0/gems/fastlane-2.162.0/fastlane/lib/fastlane/runner.rb:229:in `chdir' /Library/Ruby/Gems/2.6.0/gems/fastlane-2.162.0/fastlane/lib/fastlane/runner.rb:229:in `execute_action' /Library/Ruby/Gems/2.6.0/gems/fastlane-2.162.0/fastlane/lib/fastlane/runner.rb:157:in `trigger_action_by_name' /Library/Ruby/Gems/2.6.0/gems/fastlane-2.162.0/fastlane/lib/fastlane/fast_file.rb:159:in `method_missing' Fastfile:28:in `block (2 levels) in parsing_binding' /Library/Ruby/Gems/2.6.0/gems/fastlane-2.162.0/fastlane/lib/fastlane/lane.rb:33:in `call' /Library/Ruby/Gems/2.6.0/gems/fastlane-2.162.0/fastlane/lib/fastlane/runner.rb:49:in `block in execute' /Library/Ruby/Gems/2.6.0/gems/fastlane-2.162.0/fastlane/lib/fastlane/runner.rb:45:in `chdir' /Library/Ruby/Gems/2.6.0/gems/fastlane-2.162.0/fastlane/lib/fastlane/runner.rb:45:in `execute' /Library/Ruby/Gems/2.6.0/gems/fastlane-2.162.0/fastlane/lib/fastlane/lane_manager.rb:47:in `cruise_lane' /Library/Ruby/Gems/2.6.0/gems/fastlane-2.162.0/fastlane/lib/fastlane/command_line_handler.rb:36:in `handle' /Library/Ruby/Gems/2.6.0/gems/fastlane-2.162.0/fastlane/lib/fastlane/commands_generator.rb:108:in `block (2 levels) in run' /Library/Ruby/Gems/2.6.0/gems/commander-fastlane-4.4.6/lib/commander/command.rb:178:in `call' /Library/Ruby/Gems/2.6.0/gems/commander-fastlane-4.4.6/lib/commander/command.rb:153:in `run' /Library/Ruby/Gems/2.6.0/gems/commander-fastlane-4.4.6/lib/commander/runner.rb:476:in `run_active_command' /Library/Ruby/Gems/2.6.0/gems/fastlane-2.162.0/fastlane_core/lib/fastlane_core/ui/fastlane_runner.rb:76:in `run!' /Library/Ruby/Gems/2.6.0/gems/commander-fastlane-4.4.6/lib/commander/delegates.rb:15:in `run!' /Library/Ruby/Gems/2.6.0/gems/fastlane-2.162.0/fastlane/lib/fastlane/commands_generator.rb:352:in `run' /Library/Ruby/Gems/2.6.0/gems/fastlane-2.162.0/fastlane/lib/fastlane/commands_generator.rb:41:in `start' /Library/Ruby/Gems/2.6.0/gems/fastlane-2.162.0/fastlane/lib/fastlane/cli_tools_distributor.rb:119:in `take_off' /Library/Ruby/Gems/2.6.0/gems/fastlane-2.162.0/bin/fastlane:23:in `<top (required)>' /usr/local/bin/fastlane:23:in `load' /usr/local/bin/fastlane:23:in `<top (required)>' /Library/Ruby/Gems/2.6.0/gems/bundler-2.0.2/lib/bundler/cli/exec.rb:74:in `load' /Library/Ruby/Gems/2.6.0/gems/bundler-2.0.2/lib/bundler/cli/exec.rb:74:in `kernel_load' /Library/Ruby/Gems/2.6.0/gems/bundler-2.0.2/lib/bundler/cli/exec.rb:28:in `run' /Library/Ruby/Gems/2.6.0/gems/bundler-2.0.2/lib/bundler/cli.rb:465:in `exec' /Library/Ruby/Gems/2.6.0/gems/bundler-2.0.2/lib/bundler/vendor/thor/lib/thor/command.rb:27:in `run' /Library/Ruby/Gems/2.6.0/gems/bundler-2.0.2/lib/bundler/vendor/thor/lib/thor/invocation.rb:126:in `invoke_command' /Library/Ruby/Gems/2.6.0/gems/bundler-2.0.2/lib/bundler/vendor/thor/lib/thor.rb:387:in `dispatch' /Library/Ruby/Gems/2.6.0/gems/bundler-2.0.2/lib/bundler/cli.rb:27:in `dispatch' /Library/Ruby/Gems/2.6.0/gems/bundler-2.0.2/lib/bundler/vendor/thor/lib/thor/base.rb:466:in `start' /Library/Ruby/Gems/2.6.0/gems/bundler-2.0.2/lib/bundler/cli.rb:18:in `start' /Library/Ruby/Gems/2.6.0/gems/bundler-2.0.2/exe/bundle:30:in `block in <top (required)>' /Library/Ruby/Gems/2.6.0/gems/bundler-2.0.2/lib/bundler/friendly_errors.rb:124:in `with_friendly_errors' /Library/Ruby/Gems/2.6.0/gems/bundler-2.0.2/exe/bundle:22:in `<top (required)>' /usr/local/bin/bundle:23:in `load' /usr/local/bin/bundle:23:in `<main>' Error building the application - see the log above" />
|
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|||||||
@@ -363,8 +363,8 @@ class __SharedGroupsScreenState extends State<_SharedGroupsScreen> {
|
|||||||
),
|
),
|
||||||
backgroundColor: StreamChatTheme.of(context).primaryColor,
|
backgroundColor: StreamChatTheme.of(context).primaryColor,
|
||||||
),
|
),
|
||||||
body: StreamBuilder<List<Channel>>(
|
body: FutureBuilder<List<Channel>>(
|
||||||
stream: chat.client.queryChannels(
|
future: chat.client.queryChannels(
|
||||||
filter: {
|
filter: {
|
||||||
r'$and': [
|
r'$and': [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ class _GroupChatDetailsScreenState extends State<GroupChatDetailsScreen> {
|
|||||||
context,
|
context,
|
||||||
Routes.CHANNEL_PAGE,
|
Routes.CHANNEL_PAGE,
|
||||||
ModalRoute.withName(Routes.HOME),
|
ModalRoute.withName(Routes.HOME),
|
||||||
arguments: channel,
|
arguments: ChannelPageArgs(channel: channel),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|||||||
+108
-112
@@ -59,7 +59,7 @@ class MyApp extends StatelessWidget {
|
|||||||
debugShowCheckedModeBanner: false,
|
debugShowCheckedModeBanner: false,
|
||||||
theme: ThemeData.light(),
|
theme: ThemeData.light(),
|
||||||
darkTheme: ThemeData.dark(),
|
darkTheme: ThemeData.dark(),
|
||||||
//TODO change to system once dark theme is implemented
|
//TODO change to system once dark theme is implemented
|
||||||
themeMode: ThemeMode.light,
|
themeMode: ThemeMode.light,
|
||||||
onGenerateRoute: AppRoutes.generateRoute,
|
onGenerateRoute: AppRoutes.generateRoute,
|
||||||
initialRoute:
|
initialRoute:
|
||||||
@@ -215,12 +215,16 @@ class _HomePageState extends State<HomePage> {
|
|||||||
alignment: Alignment.bottomCenter,
|
alignment: Alignment.bottomCenter,
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
await StreamChat.of(context).client.disconnect();
|
Navigator.pop(context);
|
||||||
|
|
||||||
final secureStorage = FlutterSecureStorage();
|
final secureStorage = FlutterSecureStorage();
|
||||||
await secureStorage.deleteAll();
|
await secureStorage.deleteAll();
|
||||||
Navigator.pop(context);
|
|
||||||
Navigator.pushReplacementNamed(
|
StreamChat.of(context).client.disconnect(
|
||||||
|
clearUser: true,
|
||||||
|
);
|
||||||
|
|
||||||
|
await Navigator.pushReplacementNamed(
|
||||||
context,
|
context,
|
||||||
Routes.CHOOSE_USER,
|
Routes.CHOOSE_USER,
|
||||||
);
|
);
|
||||||
@@ -296,130 +300,113 @@ class _ChannelListPageState extends State<ChannelListPage> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final user = StreamChat.of(context).user;
|
final user = StreamChat.of(context).user;
|
||||||
return ChannelsBloc(
|
return WillPopScope(
|
||||||
child: MessageSearchBloc(
|
onWillPop: () async {
|
||||||
child: Column(
|
if (_isSearchActive) {
|
||||||
children: [
|
_controller.clear();
|
||||||
SearchTextField(
|
setState(() => _isSearchActive = false);
|
||||||
controller: _controller,
|
return false;
|
||||||
showCloseButton: _isSearchActive,
|
}
|
||||||
),
|
return true;
|
||||||
Expanded(
|
},
|
||||||
child: AnimatedSwitcher(
|
child: ChannelsBloc(
|
||||||
duration: const Duration(milliseconds: 350),
|
child: MessageSearchBloc(
|
||||||
child: GestureDetector(
|
child: Column(
|
||||||
behavior: HitTestBehavior.opaque,
|
children: [
|
||||||
onPanDown: (_) => FocusScope.of(context).unfocus(),
|
SearchTextField(
|
||||||
child: _isSearchActive
|
controller: _controller,
|
||||||
? MessageSearchListView(
|
showCloseButton: _isSearchActive,
|
||||||
messageQuery: _channelQuery,
|
),
|
||||||
filters: {
|
Expanded(
|
||||||
'members': {
|
child: AnimatedSwitcher(
|
||||||
r'$in': [user.id]
|
duration: const Duration(milliseconds: 350),
|
||||||
}
|
child: GestureDetector(
|
||||||
},
|
behavior: HitTestBehavior.opaque,
|
||||||
sortOptions: [
|
onPanDown: (_) => FocusScope.of(context).unfocus(),
|
||||||
SortOption(
|
child: _isSearchActive
|
||||||
'created_at',
|
? MessageSearchListView(
|
||||||
direction: SortOption.ASC,
|
messageQuery: _channelQuery,
|
||||||
),
|
filters: {
|
||||||
],
|
'members': {
|
||||||
paginationParams: PaginationParams(limit: 20),
|
r'$in': [user.id]
|
||||||
onItemTap: (message) {},
|
}
|
||||||
)
|
|
||||||
: ChannelListView(
|
|
||||||
onStartChatPressed: () {
|
|
||||||
Navigator.pushNamed(context, Routes.NEW_CHAT);
|
|
||||||
},
|
|
||||||
swipeToAction: true,
|
|
||||||
filter: {
|
|
||||||
'members': {
|
|
||||||
r'$in': [user.id],
|
|
||||||
},
|
},
|
||||||
},
|
sortOptions: [
|
||||||
options: {
|
SortOption(
|
||||||
'presence': true,
|
'created_at',
|
||||||
},
|
direction: SortOption.ASC,
|
||||||
pagination: PaginationParams(
|
),
|
||||||
limit: 20,
|
],
|
||||||
|
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 {
|
class ChannelPageArgs {
|
||||||
final Stream<List<Message>> searchResultStream;
|
final Channel channel;
|
||||||
|
final Message initialMessage;
|
||||||
|
|
||||||
const ChannelQuerySearchResultPage({
|
const ChannelPageArgs({
|
||||||
Key key,
|
this.channel,
|
||||||
@required this.searchResultStream,
|
this.initialMessage,
|
||||||
}) : 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()),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class ChannelPage extends StatelessWidget {
|
class ChannelPage extends StatelessWidget {
|
||||||
|
final int initialScrollIndex;
|
||||||
|
final double initialAlignment;
|
||||||
|
final bool highlightInitialMessage;
|
||||||
|
|
||||||
const ChannelPage({
|
const ChannelPage({
|
||||||
Key key,
|
Key key,
|
||||||
|
this.initialScrollIndex,
|
||||||
|
this.initialAlignment,
|
||||||
|
this.highlightInitialMessage = false,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -473,6 +460,9 @@ class ChannelPage extends StatelessWidget {
|
|||||||
child: Stack(
|
child: Stack(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
MessageListView(
|
MessageListView(
|
||||||
|
initialScrollIndex: initialScrollIndex,
|
||||||
|
initialAlignment: initialAlignment,
|
||||||
|
highlightInitialMessage: highlightInitialMessage,
|
||||||
threadBuilder: (_, parentMessage) {
|
threadBuilder: (_, parentMessage) {
|
||||||
return ThreadPage(
|
return ThreadPage(
|
||||||
parent: parentMessage,
|
parent: parentMessage,
|
||||||
@@ -507,10 +497,14 @@ class ChannelPage extends StatelessWidget {
|
|||||||
|
|
||||||
class ThreadPage extends StatelessWidget {
|
class ThreadPage extends StatelessWidget {
|
||||||
final Message parent;
|
final Message parent;
|
||||||
|
final int initialScrollIndex;
|
||||||
|
final double initialAlignment;
|
||||||
|
|
||||||
ThreadPage({
|
ThreadPage({
|
||||||
Key key,
|
Key key,
|
||||||
this.parent,
|
this.parent,
|
||||||
|
this.initialScrollIndex,
|
||||||
|
this.initialAlignment,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -524,6 +518,8 @@ class ThreadPage extends StatelessWidget {
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: MessageListView(
|
child: MessageListView(
|
||||||
parentMessage: parent,
|
parentMessage: parent,
|
||||||
|
initialScrollIndex: initialScrollIndex,
|
||||||
|
initialAlignment: initialAlignment,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (parent.type != 'deleted')
|
if (parent.type != 'deleted')
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ class _NewChatScreenState extends State<NewChatScreen> {
|
|||||||
|
|
||||||
bool _showUserList = true;
|
bool _showUserList = true;
|
||||||
|
|
||||||
|
bool _channelExisted = false;
|
||||||
|
|
||||||
void _userNameListener() {
|
void _userNameListener() {
|
||||||
if (_debounce?.isActive ?? false) _debounce.cancel();
|
if (_debounce?.isActive ?? false) _debounce.cancel();
|
||||||
_debounce = Timer(const Duration(milliseconds: 350), () {
|
_debounce = Timer(const Duration(milliseconds: 350), () {
|
||||||
@@ -56,11 +58,6 @@ class _NewChatScreenState extends State<NewChatScreen> {
|
|||||||
|
|
||||||
_searchFocusNode.addListener(() async {
|
_searchFocusNode.addListener(() async {
|
||||||
if (_searchFocusNode.hasFocus && !_showUserList) {
|
if (_searchFocusNode.hasFocus && !_showUserList) {
|
||||||
if (channel.extraData['draft'] == true) {
|
|
||||||
await channel.stopWatching();
|
|
||||||
channel.dispose();
|
|
||||||
channel.client.state.channels.remove(channel.cid);
|
|
||||||
}
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_showUserList = true;
|
_showUserList = true;
|
||||||
});
|
});
|
||||||
@@ -71,19 +68,38 @@ class _NewChatScreenState extends State<NewChatScreen> {
|
|||||||
if (_messageInputFocusNode.hasFocus && _selectedUsers.isNotEmpty) {
|
if (_messageInputFocusNode.hasFocus && _selectedUsers.isNotEmpty) {
|
||||||
final chatState = StreamChat.of(context);
|
final chatState = StreamChat.of(context);
|
||||||
|
|
||||||
channel = chatState.client.channel(
|
final res = await chatState.client.queryChannels(
|
||||||
'messaging',
|
options: {
|
||||||
extraData: {
|
'state': false,
|
||||||
|
'watch': false,
|
||||||
|
},
|
||||||
|
filter: {
|
||||||
'members': [
|
'members': [
|
||||||
..._selectedUsers.map((e) => e.id),
|
..._selectedUsers.map((e) => e.id),
|
||||||
chatState.user.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();
|
await channel.watch();
|
||||||
|
} else {
|
||||||
|
channel = chatState.client.channel(
|
||||||
|
'messaging',
|
||||||
|
extraData: {
|
||||||
|
'members': [
|
||||||
|
..._selectedUsers.map((e) => e.id),
|
||||||
|
chatState.user.id,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
@@ -134,6 +150,7 @@ class _NewChatScreenState extends State<NewChatScreen> {
|
|||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
_chipInputTextFieldState.removeItem(user);
|
_chipInputTextFieldState.removeItem(user);
|
||||||
|
_searchFocusNode.requestFocus();
|
||||||
},
|
},
|
||||||
child: Stack(
|
child: Stack(
|
||||||
alignment: AlignmentDirectional.centerStart,
|
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(
|
MessageInput(
|
||||||
focusNode: _messageInputFocusNode,
|
focusNode: _messageInputFocusNode,
|
||||||
|
preMessageSending: (message) async {
|
||||||
|
await channel.watch();
|
||||||
|
return message;
|
||||||
|
},
|
||||||
onMessageSent: (m) {
|
onMessageSent: (m) {
|
||||||
if (!m.isEphemeral) {
|
Navigator.pushNamedAndRemoveUntil(
|
||||||
_updateChannelAndNavigate(context);
|
context,
|
||||||
} else {
|
Routes.CHANNEL_PAGE,
|
||||||
channel.on('message.new').first.then((_) {
|
ModalRoute.withName(Routes.HOME),
|
||||||
_updateChannelAndNavigate(context);
|
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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,9 +36,13 @@ class AppRoutes {
|
|||||||
return MaterialPageRoute(
|
return MaterialPageRoute(
|
||||||
settings: const RouteSettings(name: Routes.CHANNEL_PAGE),
|
settings: const RouteSettings(name: Routes.CHANNEL_PAGE),
|
||||||
builder: (_) {
|
builder: (_) {
|
||||||
|
final arg = args as ChannelPageArgs;
|
||||||
return StreamChannel(
|
return StreamChannel(
|
||||||
channel: args as Channel,
|
channel: arg.channel,
|
||||||
child: ChannelPage(),
|
initialMessageId: arg.initialMessage?.id,
|
||||||
|
child: ChannelPage(
|
||||||
|
highlightInitialMessage: arg.initialMessage != null,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
case Routes.NEW_CHAT:
|
case Routes.NEW_CHAT:
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
name: example
|
name: example
|
||||||
description: A new Flutter project.
|
description: A new Flutter project.
|
||||||
version: 1.0.88+90
|
version: 1.0.94+97
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=2.2.2 <3.0.0"
|
sdk: ">=2.2.2 <3.0.0"
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ class ChannelsBlocState extends State<ChannelsBloc>
|
|||||||
paginationParams.offset == null ||
|
paginationParams.offset == null ||
|
||||||
paginationParams.offset == 0;
|
paginationParams.offset == 0;
|
||||||
final oldChannels = List<Channel>.from(channels ?? []);
|
final oldChannels = List<Channel>.from(channels ?? []);
|
||||||
client
|
await client
|
||||||
.queryChannels(
|
.queryChannels(
|
||||||
filter: filter,
|
filter: filter,
|
||||||
sort: sortOptions,
|
sort: sortOptions,
|
||||||
@@ -93,21 +93,18 @@ class ChannelsBlocState extends State<ChannelsBloc>
|
|||||||
paginationParams: paginationParams,
|
paginationParams: paginationParams,
|
||||||
onlyOffline: onlyOffline,
|
onlyOffline: onlyOffline,
|
||||||
)
|
)
|
||||||
.listen((channels) {
|
.then((channels) {
|
||||||
if (clear) {
|
if (clear) {
|
||||||
_channelsController.add(channels);
|
_channelsController.add(channels);
|
||||||
} else {
|
} else {
|
||||||
final l = oldChannels + channels;
|
final l = oldChannels + channels;
|
||||||
_channelsController.add(l);
|
_channelsController.add(l);
|
||||||
}
|
}
|
||||||
}, onDone: () {
|
|
||||||
_queryChannelsLoadingController.sink.add(false);
|
_queryChannelsLoadingController.sink.add(false);
|
||||||
}, onError: (err, stackTrace) {
|
|
||||||
print(err);
|
|
||||||
print(stackTrace);
|
|
||||||
_queryChannelsLoadingController.addError(err, stackTrace);
|
|
||||||
});
|
});
|
||||||
} catch (err, stackTrace) {
|
} catch (err, stackTrace) {
|
||||||
|
print(err);
|
||||||
|
print(stackTrace);
|
||||||
_queryChannelsLoadingController.addError(err, stackTrace);
|
_queryChannelsLoadingController.addError(err, stackTrace);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,33 +1,47 @@
|
|||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
enum LoadingStatus { LOADING, STABLE }
|
enum _LoadingStatus { LOADING, STABLE }
|
||||||
|
|
||||||
/// Signature for EndOfPageListeners
|
/// A widget that wraps a [Widget] and will trigger [onEndOfPage]/[onStartOfPage] when it
|
||||||
typedef EndOfPageListenerCallback = void Function();
|
/// reaches the bottom/start of the list
|
||||||
|
|
||||||
/// A widget that wraps a [Widget] and will trigger [onEndOfPage] when it
|
|
||||||
/// reaches the bottom of the list
|
|
||||||
class LazyLoadScrollView extends StatefulWidget {
|
class LazyLoadScrollView extends StatefulWidget {
|
||||||
/// The [Widget] that this widget watches for changes on
|
/// The [Widget] that this widget watches for changes on
|
||||||
final Widget child;
|
final Widget child;
|
||||||
|
|
||||||
/// Called when the [child] reaches the end of the list
|
/// Called when the [child] reaches the start of the list
|
||||||
final EndOfPageListenerCallback onEndOfPage;
|
final AsyncCallback onStartOfPage;
|
||||||
|
|
||||||
/// The offset to take into account when triggering [onEndOfPage] in pixels
|
/// Called when the [child] reaches the end of the list
|
||||||
final int scrollOffset;
|
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
|
/// 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;
|
final bool isLoading;
|
||||||
|
|
||||||
|
/// Initiates a LazyLoadScrollView widget
|
||||||
LazyLoadScrollView({
|
LazyLoadScrollView({
|
||||||
Key key,
|
Key key,
|
||||||
@required this.child,
|
@required this.child,
|
||||||
@required this.onEndOfPage,
|
this.onStartOfPage,
|
||||||
|
this.onEndOfPage,
|
||||||
|
this.onPageScrollStart,
|
||||||
|
this.onPageScrollEnd,
|
||||||
|
this.onInBetweenOfPage,
|
||||||
this.isLoading = false,
|
this.isLoading = false,
|
||||||
this.scrollOffset = 100,
|
this.scrollOffset = 100,
|
||||||
}) : assert(onEndOfPage != null),
|
}) : assert(child != null),
|
||||||
assert(child != null),
|
|
||||||
super(key: key);
|
super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -35,15 +49,8 @@ class LazyLoadScrollView extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _LazyLoadScrollViewState extends State<LazyLoadScrollView> {
|
class _LazyLoadScrollViewState extends State<LazyLoadScrollView> {
|
||||||
LoadingStatus _loadMoreStatus = LoadingStatus.STABLE;
|
_LoadingStatus _loadMoreStatus = _LoadingStatus.STABLE;
|
||||||
|
double _scrollPosition = 0.0;
|
||||||
@override
|
|
||||||
void didUpdateWidget(LazyLoadScrollView oldWidget) {
|
|
||||||
super.didUpdateWidget(oldWidget);
|
|
||||||
if (!widget.isLoading) {
|
|
||||||
_loadMoreStatus = LoadingStatus.STABLE;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -54,28 +61,88 @@ class _LazyLoadScrollViewState extends State<LazyLoadScrollView> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool _onNotification(Notification notification) {
|
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 is ScrollUpdateNotification) {
|
||||||
if (notification.metrics.maxScrollExtent > notification.metrics.pixels &&
|
final pixels = notification.metrics.pixels;
|
||||||
notification.metrics.maxScrollExtent - notification.metrics.pixels <=
|
final maxScrollExtent = notification.metrics.maxScrollExtent;
|
||||||
widget.scrollOffset) {
|
final minScrollExtent = notification.metrics.minScrollExtent;
|
||||||
if (_loadMoreStatus != null &&
|
final scrollOffset = widget.scrollOffset;
|
||||||
_loadMoreStatus == LoadingStatus.STABLE) {
|
|
||||||
_loadMoreStatus = LoadingStatus.LOADING;
|
if (pixels > (minScrollExtent + scrollOffset) &&
|
||||||
widget.onEndOfPage();
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
if (notification is OverscrollNotification) {
|
if (notification is OverscrollNotification) {
|
||||||
if (notification.overscroll > 0) {
|
if (notification.overscroll > 0) {
|
||||||
if (_loadMoreStatus != null &&
|
_onEndOfPage();
|
||||||
_loadMoreStatus == LoadingStatus.STABLE) {
|
}
|
||||||
_loadMoreStatus = LoadingStatus.LOADING;
|
if (notification.overscroll < 0) {
|
||||||
widget.onEndOfPage();
|
_onStartOfPage();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+30
-19
@@ -21,7 +21,6 @@ import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
|
|||||||
import 'package:stream_chat_flutter/src/user_avatar.dart';
|
import 'package:stream_chat_flutter/src/user_avatar.dart';
|
||||||
import 'package:substring_highlight/substring_highlight.dart';
|
import 'package:substring_highlight/substring_highlight.dart';
|
||||||
import 'package:video_compress/video_compress.dart';
|
import 'package:video_compress/video_compress.dart';
|
||||||
import 'package:photo_manager/photo_manager.dart';
|
|
||||||
|
|
||||||
import '../stream_chat_flutter.dart';
|
import '../stream_chat_flutter.dart';
|
||||||
import 'stream_channel.dart';
|
import 'stream_channel.dart';
|
||||||
@@ -343,7 +342,10 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
children: [
|
children: [
|
||||||
if (!widget.disableAttachments) _buildAttachmentButton(),
|
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),
|
duration: Duration(milliseconds: 300),
|
||||||
@@ -459,6 +461,7 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Timer _debounce;
|
Timer _debounce;
|
||||||
|
|
||||||
void _onChanged(BuildContext context, String s) {
|
void _onChanged(BuildContext context, String s) {
|
||||||
if (_debounce?.isActive == true) _debounce.cancel();
|
if (_debounce?.isActive == true) _debounce.cancel();
|
||||||
_debounce = Timer(
|
_debounce = Timer(
|
||||||
@@ -543,11 +546,12 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
void _checkCommands(String s, BuildContext context) {
|
void _checkCommands(String s, BuildContext context) {
|
||||||
if (s.startsWith('/')) {
|
if (s.startsWith('/')) {
|
||||||
var matchedCommandsList = StreamChannel.of(context)
|
var matchedCommandsList = StreamChannel.of(context)
|
||||||
.channel
|
.channel
|
||||||
.config
|
.config
|
||||||
.commands
|
?.commands
|
||||||
.where((element) => element.name == s.substring(1))
|
?.where((element) => element.name == s.substring(1))
|
||||||
.toList();
|
?.toList() ??
|
||||||
|
[];
|
||||||
|
|
||||||
if (matchedCommandsList.length == 1) {
|
if (matchedCommandsList.length == 1) {
|
||||||
_chosenCommand = matchedCommandsList[0];
|
_chosenCommand = matchedCommandsList[0];
|
||||||
@@ -568,11 +572,12 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
OverlayEntry _buildCommandsOverlayEntry() {
|
OverlayEntry _buildCommandsOverlayEntry() {
|
||||||
final text = textEditingController.text.trimLeft();
|
final text = textEditingController.text.trimLeft();
|
||||||
final commands = StreamChannel.of(context)
|
final commands = StreamChannel.of(context)
|
||||||
.channel
|
.channel
|
||||||
.config
|
.config
|
||||||
.commands
|
?.commands
|
||||||
.where((c) => c.name.contains(text.replaceFirst('/', '')))
|
?.where((c) => c.name.contains(text.replaceFirst('/', '')))
|
||||||
.toList();
|
?.toList() ??
|
||||||
|
[];
|
||||||
|
|
||||||
RenderBox renderBox = context.findRenderObject();
|
RenderBox renderBox = context.findRenderObject();
|
||||||
final size = renderBox.size;
|
final size = renderBox.size;
|
||||||
@@ -1282,10 +1287,11 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
children: [
|
children: [
|
||||||
if (_attachments.any((e) => e.attachment?.type == 'file'))
|
if (_attachments.any((e) => e.attachment?.type == 'file'))
|
||||||
LimitedBox(
|
LimitedBox(
|
||||||
maxHeight: 73.0,
|
maxHeight: 136.0,
|
||||||
child: ListView(
|
child: ListView(
|
||||||
scrollDirection: Axis.horizontal,
|
reverse: true,
|
||||||
children: _attachments
|
shrinkWrap: true,
|
||||||
|
children: _attachments.reversed
|
||||||
.where((e) => e.attachment?.type == 'file')
|
.where((e) => e.attachment?.type == 'file')
|
||||||
.map(
|
.map(
|
||||||
(e) => Padding(
|
(e) => Padding(
|
||||||
@@ -1299,8 +1305,8 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
attachmentType: FileAttachmentType.local,
|
attachmentType: FileAttachmentType.local,
|
||||||
file: e.file,
|
file: e.file,
|
||||||
size: Size(
|
size: Size(
|
||||||
MediaQuery.of(context).size.width * 0.55,
|
MediaQuery.of(context).size.width * 0.65,
|
||||||
MediaQuery.of(context).size.height * 0.3,
|
56.0,
|
||||||
),
|
),
|
||||||
trailing: Padding(
|
trailing: Padding(
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding: const EdgeInsets.all(8.0),
|
||||||
@@ -1927,8 +1933,6 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
_mentionsOverlay?.remove();
|
_mentionsOverlay?.remove();
|
||||||
_mentionsOverlay = null;
|
_mentionsOverlay = null;
|
||||||
|
|
||||||
final channel = StreamChannel.of(context).channel;
|
|
||||||
|
|
||||||
Future sendingFuture;
|
Future sendingFuture;
|
||||||
Message message;
|
Message message;
|
||||||
if (widget.editMessage != null) {
|
if (widget.editMessage != null) {
|
||||||
@@ -1953,6 +1957,12 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
message = await widget.preMessageSending(message);
|
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 ||
|
if (widget.editMessage == null ||
|
||||||
widget.editMessage.status == MessageSendingStatus.FAILED) {
|
widget.editMessage.status == MessageSendingStatus.FAILED) {
|
||||||
sendingFuture = channel.sendMessage(message);
|
sendingFuture = channel.sendMessage(message);
|
||||||
@@ -2038,6 +2048,7 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool _initialized = false;
|
bool _initialized = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void didChangeDependencies() {
|
void didChangeDependencies() {
|
||||||
if (widget.editMessage != null && !_initialized) {
|
if (widget.editMessage != null && !_initialized) {
|
||||||
|
|||||||
+443
-283
@@ -1,10 +1,13 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:flutter/cupertino.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:jiffy/jiffy.dart';
|
import 'package:jiffy/jiffy.dart';
|
||||||
|
import 'package:rxdart/rxdart.dart';
|
||||||
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
|
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
|
||||||
import 'package:stream_chat/stream_chat.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/message_widget.dart';
|
||||||
import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
|
import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
|
||||||
import 'package:stream_chat_flutter/src/system_message.dart';
|
import 'package:stream_chat_flutter/src/system_message.dart';
|
||||||
@@ -108,10 +111,11 @@ class MessageListView extends StatefulWidget {
|
|||||||
this.onThreadTap,
|
this.onThreadTap,
|
||||||
this.dateDividerBuilder,
|
this.dateDividerBuilder,
|
||||||
this.scrollPhysics = const AlwaysScrollableScrollPhysics(),
|
this.scrollPhysics = const AlwaysScrollableScrollPhysics(),
|
||||||
this.initialScrollIndex = 0,
|
this.initialScrollIndex,
|
||||||
this.initialAlignment = 0,
|
this.initialAlignment,
|
||||||
this.scrollController,
|
this.scrollController,
|
||||||
this.itemPositionListener,
|
this.itemPositionListener,
|
||||||
|
this.highlightInitialMessage = false,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
/// Function used to build a custom message widget
|
/// Function used to build a custom message widget
|
||||||
@@ -153,6 +157,11 @@ class MessageListView extends StatefulWidget {
|
|||||||
/// The ScrollPhysics used by the ListView
|
/// The ScrollPhysics used by the ListView
|
||||||
final ScrollPhysics scrollPhysics;
|
final ScrollPhysics scrollPhysics;
|
||||||
|
|
||||||
|
/// If true the list will highlight the initialMessage if there is any.
|
||||||
|
///
|
||||||
|
/// Also See [StreamChannel]
|
||||||
|
final bool highlightInitialMessage;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_MessageListViewState createState() => _MessageListViewState();
|
_MessageListViewState createState() => _MessageListViewState();
|
||||||
}
|
}
|
||||||
@@ -160,10 +169,50 @@ class MessageListView extends StatefulWidget {
|
|||||||
class _MessageListViewState extends State<MessageListView> {
|
class _MessageListViewState extends State<MessageListView> {
|
||||||
ItemScrollController _scrollController;
|
ItemScrollController _scrollController;
|
||||||
bool _bottomWasVisible = false;
|
bool _bottomWasVisible = false;
|
||||||
bool _topWasVisible = false;
|
|
||||||
Function _onThreadTap;
|
Function _onThreadTap;
|
||||||
bool _showScrollToBottom = false;
|
bool _showScrollToBottom = false;
|
||||||
ItemPositionsListener _itemPositionListener;
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -173,179 +222,257 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
? streamChannel.channel.state.threadsStream
|
? streamChannel.channel.state.threadsStream
|
||||||
.where((threads) => threads.containsKey(widget.parentMessage.id))
|
.where((threads) => threads.containsKey(widget.parentMessage.id))
|
||||||
.map((threads) => threads[widget.parentMessage.id])
|
.map((threads) => threads[widget.parentMessage.id])
|
||||||
: streamChannel.channel.state.messagesStream;
|
: streamChannel.channel.state?.messagesStream;
|
||||||
|
|
||||||
return StreamBuilder<List<Message>>(
|
return WillPopScope(
|
||||||
stream: messagesStream.map((messages) => messages
|
onWillPop: () async {
|
||||||
.where((e) =>
|
if (!_upToDate) {
|
||||||
!e.isDeleted ||
|
await streamChannel.reloadChannel();
|
||||||
(e.isDeleted &&
|
}
|
||||||
e.user.id == streamChannel.channel.client.state.user.id))
|
return true;
|
||||||
.toList()),
|
},
|
||||||
builder: (context, snapshot) {
|
child: StreamBuilder<List<Message>>(
|
||||||
if (!snapshot.hasData) {
|
stream: messagesStream?.map((messages) => messages
|
||||||
return Center(
|
?.where((e) =>
|
||||||
child: CircularProgressIndicator(),
|
!e.isDeleted ||
|
||||||
);
|
(e.isDeleted &&
|
||||||
}
|
e.user.id == streamChannel.channel.client.state.user.id))
|
||||||
|
?.toList()),
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (!snapshot.hasData) {
|
||||||
|
return Center(
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
final messages = snapshot.data?.reversed?.toList() ?? [];
|
final messageList = snapshot.data?.reversed?.toList() ?? [];
|
||||||
|
|
||||||
if (messages.isEmpty) {
|
if (messageList.isEmpty) {
|
||||||
return Center(
|
if (_upToDate) {
|
||||||
child: Text(
|
return Center(
|
||||||
'No chats here yet...',
|
child: Text(
|
||||||
style: TextStyle(
|
'No chats here yet...',
|
||||||
fontSize: 12,
|
style: TextStyle(
|
||||||
color: Colors.black.withOpacity(.5),
|
fontSize: 12,
|
||||||
),
|
color: Colors.black.withOpacity(.5),
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
}
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
messages = messageList;
|
||||||
|
}
|
||||||
|
|
||||||
return Stack(
|
final newMessagesListLength = messages.length;
|
||||||
alignment: Alignment.center,
|
|
||||||
children: [
|
if (_messageListLength != null) {
|
||||||
ScrollablePositionedList.builder(
|
if (_bottomPaginationActive || (_inBetweenList && _upToDate)) {
|
||||||
itemPositionsListener: _itemPositionListener,
|
if (_itemPositionListener.itemPositions.value?.isNotEmpty ==
|
||||||
addAutomaticKeepAlives: true,
|
true) {
|
||||||
key: Key('messageListView'),
|
final first = _itemPositionListener.itemPositions.value.first;
|
||||||
initialScrollIndex: widget.initialScrollIndex,
|
final diff = newMessagesListLength - _messageListLength;
|
||||||
initialAlignment: widget.initialAlignment,
|
if (diff > 0) {
|
||||||
physics: widget.scrollPhysics,
|
initialIndex = first.index + diff;
|
||||||
itemScrollController: _scrollController,
|
initialAlignment = first.itemLeadingEdge;
|
||||||
reverse: true,
|
}
|
||||||
itemCount: messages.length +
|
}
|
||||||
1 +
|
} else if (!_topPaginationActive && _upToDate) {
|
||||||
(widget.parentMessage != null ? 1 : 0),
|
// Reset the index in-case we send any new message
|
||||||
itemBuilder: (context, i) {
|
initialIndex = 0;
|
||||||
if (i == messages.length + 1) {
|
initialAlignment = 0;
|
||||||
if (widget.parentMessageBuilder != null) {
|
}
|
||||||
return widget.parentMessageBuilder(
|
}
|
||||||
context,
|
|
||||||
widget.parentMessage,
|
_messageListLength = newMessagesListLength;
|
||||||
);
|
|
||||||
} else {
|
return Stack(
|
||||||
return Column(
|
alignment: Alignment.center,
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
children: [
|
||||||
children: <Widget>[
|
LazyLoadScrollView(
|
||||||
buildParentMessage(widget.parentMessage),
|
onStartOfPage: () async {
|
||||||
Padding(
|
_inBetweenList = false;
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 32),
|
if (!_upToDate) {
|
||||||
child: Container(
|
_topPaginationActive = false;
|
||||||
padding: const EdgeInsets.all(8),
|
_bottomPaginationActive = true;
|
||||||
child: Text(
|
return _paginateData(
|
||||||
'Start of thread',
|
streamChannel,
|
||||||
textAlign: TextAlign.center,
|
QueryDirection.bottom,
|
||||||
),
|
|
||||||
color:
|
|
||||||
Theme.of(context).accentColor.withAlpha(50),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
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: (_) => 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),
|
||||||
|
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),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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) {
|
ItemPosition _getTopElement(Iterable<ItemPosition> values) {
|
||||||
@@ -357,91 +484,120 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
|
|
||||||
Widget _buildScrollToBottom() {
|
Widget _buildScrollToBottom() {
|
||||||
final streamChannel = StreamChannel.of(context);
|
final streamChannel = StreamChannel.of(context);
|
||||||
return Positioned(
|
return StreamBuilder<Tuple2<bool, int>>(
|
||||||
bottom: 8,
|
stream: Rx.combineLatest2(
|
||||||
right: 8,
|
streamChannel.channel.state.isUpToDateStream,
|
||||||
width: 40,
|
streamChannel.channel.state.unreadCountStream,
|
||||||
height: 40,
|
(bool isUpToDate, int unreadCount) => Tuple2(isUpToDate, unreadCount),
|
||||||
child: Stack(
|
),
|
||||||
clipBehavior: Clip.none,
|
builder: (_, snapshot) {
|
||||||
children: [
|
if (snapshot.hasError) {
|
||||||
FloatingActionButton(
|
return Offstage();
|
||||||
backgroundColor: Colors.white,
|
} else if (!snapshot.hasData) {
|
||||||
child: StreamSvgIcon.down(
|
return Offstage();
|
||||||
color: Colors.black,
|
}
|
||||||
),
|
final isUpToDate = snapshot.data.item1;
|
||||||
onPressed: () {
|
final showScrollToBottom = !isUpToDate || _showScrollToBottom;
|
||||||
setState(() {
|
if (!showScrollToBottom) {
|
||||||
_showScrollToBottom = false;
|
return Offstage();
|
||||||
});
|
}
|
||||||
_scrollController.scrollTo(
|
final unreadCount = snapshot.data.item2;
|
||||||
index: 0,
|
final showUnreadCount = unreadCount > 0 &&
|
||||||
duration: Duration(seconds: 1),
|
streamChannel.channel.state.members.any(
|
||||||
curve: Curves.easeInOut,
|
(e) => e.userId == streamChannel.channel.client.state.user.id);
|
||||||
);
|
return Positioned(
|
||||||
},
|
bottom: 8,
|
||||||
),
|
right: 8,
|
||||||
if (streamChannel.channel.state.members.any((Member e) =>
|
width: 40,
|
||||||
e.userId == streamChannel.channel.client.state.user.id))
|
height: 40,
|
||||||
StreamBuilder<int>(
|
child: Stack(
|
||||||
stream: streamChannel.channel.state.unreadCountStream,
|
clipBehavior: Clip.none,
|
||||||
initialData: streamChannel.channel.state.unreadCount,
|
children: [
|
||||||
builder: (context, snapshot) {
|
FloatingActionButton(
|
||||||
if (!snapshot.hasData || snapshot.data <= 0) {
|
backgroundColor: Colors.white,
|
||||||
return Offstage();
|
child: StreamSvgIcon.down(
|
||||||
|
color: Colors.black,
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
if (unreadCount > 0) {
|
||||||
|
streamChannel.channel.markRead();
|
||||||
}
|
}
|
||||||
return Positioned(
|
if (!_upToDate) {
|
||||||
width: 20,
|
_bottomPaginationActive = false;
|
||||||
height: 20,
|
_topPaginationActive = false;
|
||||||
left: 10,
|
streamChannel.reloadChannel();
|
||||||
top: -10,
|
} else {
|
||||||
child: CircleAvatar(
|
setState(() => _showScrollToBottom = false);
|
||||||
child: Padding(
|
_scrollController.scrollTo(
|
||||||
padding: const EdgeInsets.all(3.0),
|
index: 0,
|
||||||
child: Text(
|
duration: Duration(seconds: 1),
|
||||||
snapshot.data.toString(),
|
curve: Curves.easeInOut,
|
||||||
style: TextStyle(
|
);
|
||||||
fontSize: 11,
|
}
|
||||||
fontWeight: FontWeight.bold,
|
},
|
||||||
),
|
),
|
||||||
|
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) {
|
Widget _buildLoadingIndicator(
|
||||||
return Container(
|
StreamChannelState streamChannel,
|
||||||
key: Key('LOADING-INDICATOR'),
|
QueryDirection direction,
|
||||||
height: 50,
|
) {
|
||||||
width: double.infinity,
|
final stream = direction == QueryDirection.top
|
||||||
child: StreamBuilder<bool>(
|
? streamChannel.queryTopMessages
|
||||||
stream: streamChannel.queryMessage,
|
: streamChannel.queryBottomMessages;
|
||||||
initialData: false,
|
return StreamBuilder<bool>(
|
||||||
builder: (context, snapshot) {
|
key: Key('LOADING-INDICATOR'),
|
||||||
if (snapshot.hasError) {
|
stream: stream,
|
||||||
return Container(
|
initialData: false,
|
||||||
color: Color(0xffd0021B).withAlpha(26),
|
builder: (context, snapshot) {
|
||||||
child: Center(
|
if (snapshot.hasError) {
|
||||||
child: Text('Error loading messages'),
|
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(),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}),
|
}
|
||||||
);
|
if (!snapshot.data) {
|
||||||
|
if (direction == QueryDirection.top) {
|
||||||
|
return Container(
|
||||||
|
height: 50,
|
||||||
|
width: double.infinity,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Offstage();
|
||||||
|
}
|
||||||
|
return Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: const CircularProgressIndicator(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildTopMessage(
|
Widget _buildTopMessage(
|
||||||
@@ -468,22 +624,7 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
} else {
|
} else {
|
||||||
messageWidget = buildMessage(message, messages, messages.length - 1);
|
messageWidget = buildMessage(message, messages, messages.length - 1);
|
||||||
}
|
}
|
||||||
|
return messageWidget;
|
||||||
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;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildBottomMessage(
|
Widget _buildBottomMessage(
|
||||||
@@ -515,18 +656,17 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
key: ValueKey<String>('BOTTOM-MESSAGE'),
|
key: ValueKey<String>('BOTTOM-MESSAGE'),
|
||||||
onVisibilityChanged: (visibility) {
|
onVisibilityChanged: (visibility) {
|
||||||
final isVisible = visibility.visibleBounds != Rect.zero;
|
final isVisible = visibility.visibleBounds != Rect.zero;
|
||||||
if (isVisible &&
|
if (isVisible && !_bottomWasVisible) {
|
||||||
!_bottomWasVisible &&
|
final channel = streamChannel.channel;
|
||||||
streamChannel.channel.config?.readEvents == true) {
|
if (_upToDate &&
|
||||||
if (streamChannel.channel.state.unreadCount > 0) {
|
channel.config?.readEvents == true &&
|
||||||
|
channel.state.unreadCount > 0) {
|
||||||
streamChannel.channel.markRead();
|
streamChannel.channel.markRead();
|
||||||
}
|
}
|
||||||
|
_bottomWasVisible = !isVisible;
|
||||||
}
|
}
|
||||||
_bottomWasVisible = isVisible;
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() {
|
setState(() => _showScrollToBottom = !isVisible);
|
||||||
_showScrollToBottom = !isVisible;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: messageWidget,
|
child: messageWidget,
|
||||||
@@ -582,7 +722,7 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
final userId = StreamChat.of(context).user.id;
|
final userId = StreamChat.of(context).user.id;
|
||||||
final isMyMessage = message.user.id == userId;
|
final isMyMessage = message.user.id == userId;
|
||||||
final isNextUser =
|
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 channel = StreamChannel.of(context).channel;
|
||||||
final readList = channel.state?.read
|
final readList = channel.state?.read
|
||||||
@@ -597,7 +737,7 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
|
|
||||||
final allRead = readList.length >= (channel.memberCount ?? 0) - 1;
|
final allRead = readList.length >= (channel.memberCount ?? 0) - 1;
|
||||||
|
|
||||||
return MessageWidget(
|
Widget child = MessageWidget(
|
||||||
key: ValueKey<String>('MESSAGE-${message.id}'),
|
key: ValueKey<String>('MESSAGE-${message.id}'),
|
||||||
message: message,
|
message: message,
|
||||||
reverse: isMyMessage,
|
reverse: isMyMessage,
|
||||||
@@ -605,7 +745,8 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
padding: EdgeInsets.only(
|
padding: EdgeInsets.only(
|
||||||
left: 8.0,
|
left: 8.0,
|
||||||
right: 8.0,
|
right: 8.0,
|
||||||
bottom: index == 0 ? 30 : (isNextUser ? 5 : 10),
|
bottom: index == 0 ? 30 : (isNextUser ? 2 : 7),
|
||||||
|
top: 3,
|
||||||
),
|
),
|
||||||
showUsername: !isMyMessage && !isNextUser,
|
showUsername: !isMyMessage && !isNextUser,
|
||||||
showSendingIndicator: isMyMessage &&
|
showSendingIndicator: isMyMessage &&
|
||||||
@@ -639,6 +780,30 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
readList: readList,
|
readList: readList,
|
||||||
allRead: allRead,
|
allRead: allRead,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
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;
|
StreamSubscription _messageNewListener;
|
||||||
@@ -651,26 +816,21 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
|
|
||||||
final streamChannel = StreamChannel.of(context);
|
final streamChannel = StreamChannel.of(context);
|
||||||
|
|
||||||
|
initialIndex = _initialIndex;
|
||||||
|
initialAlignment = _initialAlignment;
|
||||||
|
|
||||||
_messageNewListener =
|
_messageNewListener =
|
||||||
streamChannel.channel.on(EventType.messageNew).listen((event) {
|
streamChannel.channel.on(EventType.messageNew).listen((event) {
|
||||||
final firstElementInViewport =
|
if (_upToDate) {
|
||||||
_itemPositionListener.itemPositions.value.first;
|
_bottomPaginationActive = false;
|
||||||
|
_topPaginationActive = false;
|
||||||
|
}
|
||||||
if (event.message.user.id == streamChannel.channel.client.state.user.id) {
|
if (event.message.user.id == streamChannel.channel.client.state.user.id) {
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
_scrollController.jumpTo(
|
_scrollController.jumpTo(
|
||||||
index: 0,
|
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) {
|
Widget _buildDate(BuildContext context, Message message) {
|
||||||
final lastUpdatedAt = message.updatedAt;
|
final createdAt = message.createdAt;
|
||||||
String stringDate;
|
String stringDate;
|
||||||
final now = DateTime.now();
|
final now = DateTime.now();
|
||||||
|
|
||||||
if (now.year != lastUpdatedAt.year ||
|
if (now.year != createdAt.year ||
|
||||||
now.month != lastUpdatedAt.month ||
|
now.month != createdAt.month ||
|
||||||
now.day != lastUpdatedAt.day) {
|
now.day != createdAt.day) {
|
||||||
stringDate = Jiffy(lastUpdatedAt.toLocal()).format('dd/MM/yyyy');
|
stringDate = Jiffy(createdAt.toLocal()).format('dd/MM/yyyy');
|
||||||
} else {
|
} else {
|
||||||
stringDate = Jiffy(lastUpdatedAt.toLocal()).format('HH:mm');
|
stringDate = Jiffy(createdAt.toLocal()).format('HH:mm');
|
||||||
}
|
}
|
||||||
|
|
||||||
return Text(
|
return Text(
|
||||||
|
|||||||
@@ -14,6 +14,10 @@ typedef MessageSearchItemTapCallback = void Function(GetMessageResponse);
|
|||||||
typedef MessageSearchItemBuilder = Widget Function(
|
typedef MessageSearchItemBuilder = Widget Function(
|
||||||
BuildContext, GetMessageResponse);
|
BuildContext, GetMessageResponse);
|
||||||
|
|
||||||
|
/// Builder used when [MessageSearchListView] is empty
|
||||||
|
typedef EmptyMessageSearchBuilder = Widget Function(
|
||||||
|
BuildContext context, String searchQuery);
|
||||||
|
|
||||||
///
|
///
|
||||||
/// It shows the list of searched messages.
|
/// It shows the list of searched messages.
|
||||||
///
|
///
|
||||||
@@ -85,7 +89,7 @@ class MessageSearchListView extends StatefulWidget {
|
|||||||
final MessageSearchItemTapCallback onItemTap;
|
final MessageSearchItemTapCallback onItemTap;
|
||||||
|
|
||||||
/// The builder used when the channel list is empty.
|
/// 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
|
/// The builder that will be used in case of error
|
||||||
final Widget Function(Error error) errorBuilder;
|
final Widget Function(Error error) errorBuilder;
|
||||||
@@ -250,11 +254,10 @@ class _MessageSearchListViewState extends State<MessageSearchListView> {
|
|||||||
|
|
||||||
final items = snapshot.data;
|
final items = snapshot.data;
|
||||||
|
|
||||||
if (items.isEmpty && widget.emptyBuilder != null) {
|
if (items.isEmpty) {
|
||||||
return widget.emptyBuilder(context);
|
if (widget.emptyBuilder != null) {
|
||||||
}
|
return widget.emptyBuilder(context, widget.messageQuery);
|
||||||
|
}
|
||||||
if (items.isEmpty && widget.emptyBuilder == null) {
|
|
||||||
return LayoutBuilder(
|
return LayoutBuilder(
|
||||||
builder: (context, viewportConstraints) {
|
builder: (context, viewportConstraints) {
|
||||||
return SingleChildScrollView(
|
return SingleChildScrollView(
|
||||||
|
|||||||
+247
-85
@@ -4,23 +4,29 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:rxdart/rxdart.dart';
|
import 'package:rxdart/rxdart.dart';
|
||||||
import 'package:stream_chat/stream_chat.dart';
|
import 'package:stream_chat/stream_chat.dart';
|
||||||
|
|
||||||
|
enum QueryDirection { top, bottom }
|
||||||
|
|
||||||
/// Widget used to provide information about the channel to the widget tree
|
/// Widget used to provide information about the channel to the widget tree
|
||||||
///
|
///
|
||||||
/// Use [StreamChannel.of] to get the current [StreamChannelState] instance.
|
/// Use [StreamChannel.of] to get the current [StreamChannelState] instance.
|
||||||
class StreamChannel extends StatefulWidget {
|
class StreamChannel extends StatefulWidget {
|
||||||
StreamChannel({
|
const StreamChannel({
|
||||||
Key key,
|
Key key,
|
||||||
@required this.child,
|
@required this.child,
|
||||||
@required this.channel,
|
@required this.channel,
|
||||||
this.showLoading = true,
|
this.showLoading = true,
|
||||||
}) : super(
|
this.initialMessageId,
|
||||||
key: key,
|
}) : assert(child != null),
|
||||||
);
|
assert(channel != null),
|
||||||
|
super(key: key);
|
||||||
|
|
||||||
final Widget child;
|
final Widget child;
|
||||||
final Channel channel;
|
final Channel channel;
|
||||||
final bool showLoading;
|
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
|
/// Use this method to get the current [StreamChannelState] instance
|
||||||
static StreamChannelState of(BuildContext context) {
|
static StreamChannelState of(BuildContext context) {
|
||||||
StreamChannelState streamChannelState;
|
StreamChannelState streamChannelState;
|
||||||
@@ -43,87 +49,123 @@ class StreamChannelState extends State<StreamChannel> {
|
|||||||
/// Current channel
|
/// Current channel
|
||||||
Channel get channel => widget.channel;
|
Channel get channel => widget.channel;
|
||||||
|
|
||||||
|
/// InitialMessageId
|
||||||
|
String get initialMessageId => widget.initialMessageId;
|
||||||
|
|
||||||
/// Current channel state stream
|
/// Current channel state stream
|
||||||
Stream<ChannelState> get channelStateStream =>
|
Stream<ChannelState> get channelStateStream =>
|
||||||
widget.channel.state.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
|
/// The stream notifying the state of [_queryTopMessages] call
|
||||||
Stream<bool> get queryMessage => _queryMessageController.stream;
|
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
|
bool _topPaginationEnded = false;
|
||||||
void queryMessages() {
|
bool _bottomPaginationEnded = false;
|
||||||
if (_queryMessageController.value == true || _paginationEnded) {
|
|
||||||
|
Future<void> _queryTopMessages({
|
||||||
|
int limit = 20,
|
||||||
|
bool preferOffline = false,
|
||||||
|
}) async {
|
||||||
|
if (_topPaginationEnded || _queryTopMessagesController?.value == true) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
_queryTopMessagesController.add(true);
|
||||||
|
|
||||||
_queryMessageController.add(true);
|
if (channel.state.messages.isEmpty) {
|
||||||
|
return _queryTopMessagesController.add(false);
|
||||||
String firstId;
|
|
||||||
if (channel.state.messages.isNotEmpty) {
|
|
||||||
firstId = channel.state.messages.first.id;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final messageLimit = 50;
|
final oldestMessage = channel.state.messages.first;
|
||||||
|
|
||||||
widget.channel
|
try {
|
||||||
.query(
|
final state = await queryBeforeMessage(
|
||||||
messagesPagination: PaginationParams(
|
oldestMessage.id,
|
||||||
lessThan: firstId,
|
limit: limit,
|
||||||
limit: messageLimit,
|
preferOffline: preferOffline,
|
||||||
),
|
);
|
||||||
preferOffline: true,
|
if (state.messages.isEmpty || state.messages.length < limit) {
|
||||||
)
|
_topPaginationEnded = true;
|
||||||
.then((res) {
|
|
||||||
if (res.messages.isEmpty || res.messages.length < messageLimit) {
|
|
||||||
_paginationEnded = true;
|
|
||||||
}
|
}
|
||||||
_queryMessageController.add(false);
|
_queryTopMessagesController.add(false);
|
||||||
}).catchError((e, stack) {
|
} catch (e, stk) {
|
||||||
if (!_queryMessageController.isClosed) {
|
_queryTopMessagesController.addError(e, stk);
|
||||||
_queryMessageController.addError(e, stack);
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
/// Calls [channel.getReplies] updating [queryMessage] stream
|
||||||
Future<void> getReplies(String parentId) async {
|
Future<void> getReplies(
|
||||||
if (_queryMessageController.value == true || _paginationEnded) {
|
String parentId, {
|
||||||
return;
|
int limit = 50,
|
||||||
|
bool preferOffline = false,
|
||||||
|
}) async {
|
||||||
|
if (_topPaginationEnded || _queryTopMessagesController.value) return;
|
||||||
|
_queryTopMessagesController.add(true);
|
||||||
|
|
||||||
|
if (!channel.state.threads.containsKey(parentId)) {
|
||||||
|
return _queryTopMessagesController.add(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
_queryMessageController.add(true);
|
final thread = channel.state.threads[parentId];
|
||||||
|
|
||||||
String firstId;
|
if (thread.isEmpty) return _queryTopMessagesController.add(false);
|
||||||
if (widget.channel.state.threads.containsKey(parentId)) {
|
|
||||||
final thread = widget.channel.state.threads[parentId];
|
|
||||||
|
|
||||||
if (thread != null && thread.isNotEmpty) {
|
final message = thread.first;
|
||||||
firstId = thread?.first?.id;
|
|
||||||
|
try {
|
||||||
|
final state = await queryBeforeMessage(
|
||||||
|
message.id,
|
||||||
|
limit: limit,
|
||||||
|
preferOffline: preferOffline,
|
||||||
|
);
|
||||||
|
if (state.messages.isEmpty || state.messages.length < limit) {
|
||||||
|
_topPaginationEnded = true;
|
||||||
}
|
}
|
||||||
|
_queryTopMessagesController.add(false);
|
||||||
|
} catch (e, stk) {
|
||||||
|
_queryTopMessagesController.addError(e, stk);
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
_queryMessageController.add(false);
|
|
||||||
}).catchError((e, stack) {
|
|
||||||
_queryMessageController.addError(e, stack);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Query the channel members and watchers
|
/// Query the channel members and watchers
|
||||||
@@ -140,41 +182,161 @@ 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_queryMessageController.close();
|
_queryTopMessagesController.close();
|
||||||
|
_queryBottomMessagesController.close();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (widget.channel == null) {
|
Widget child = FutureBuilder<List<bool>>(
|
||||||
return Center(
|
future: Future.wait(_futures),
|
||||||
child: CircularProgressIndicator(),
|
initialData: [
|
||||||
);
|
channel.state != null,
|
||||||
}
|
if (initialMessageId != null) false,
|
||||||
return FutureBuilder<bool>(
|
],
|
||||||
future: widget.channel.initialized,
|
|
||||||
initialData: widget.channel.state != null,
|
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
if (widget.showLoading && (!snapshot.hasData || !snapshot.data)) {
|
if (snapshot.hasError) {
|
||||||
return Container(
|
if (snapshot.error is Error) {
|
||||||
height: 30,
|
print((snapshot.error as Error).stackTrace);
|
||||||
child: Center(
|
}
|
||||||
child: CircularProgressIndicator(),
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,6 +90,8 @@ class UserItem extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildLastActive(context) {
|
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>>{};
|
final groupedUsers = <String, List<User>>{};
|
||||||
for (var e in temp) {
|
for (var e in temp) {
|
||||||
final alphabet = e.name[0];
|
final alphabet = e.name[0]?.toUpperCase();
|
||||||
groupedUsers[alphabet] = [...groupedUsers[alphabet] ?? [], e];
|
groupedUsers[alphabet] = [...groupedUsers[alphabet] ?? [], e];
|
||||||
}
|
}
|
||||||
final items = <ListItem>[];
|
final items = <ListItem>[];
|
||||||
@@ -339,7 +339,9 @@ class _UserListViewState extends State<UserListView>
|
|||||||
);
|
);
|
||||||
|
|
||||||
return LazyLoadScrollView(
|
return LazyLoadScrollView(
|
||||||
onEndOfPage: () => _listenUserPagination(usersBlocState),
|
onEndOfPage: () async {
|
||||||
|
return _listenUserPagination(usersBlocState);
|
||||||
|
},
|
||||||
child: child,
|
child: child,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
+1
-1
@@ -28,7 +28,7 @@ dependencies:
|
|||||||
file_picker: ^2.0.12
|
file_picker: ^2.0.12
|
||||||
image_picker: ^0.6.7+2
|
image_picker: ^0.6.7+2
|
||||||
flutter_keyboard_visibility: ^3.3.0
|
flutter_keyboard_visibility: ^3.3.0
|
||||||
stream_chat: ^0.2.19
|
stream_chat: ^0.2.20
|
||||||
mime: ^0.9.6+3
|
mime: ^0.9.6+3
|
||||||
video_compress: ^2.1.1
|
video_compress: ^2.1.1
|
||||||
visibility_detector: ^0.1.5
|
visibility_detector: ^0.1.5
|
||||||
|
|||||||
Reference in New Issue
Block a user