Merge branch 'feature/new-ui' into feature/emojiPicker

This commit is contained in:
Salvatore Giordano
2020-11-06 12:23:40 +01:00
committed by GitHub
7 changed files with 496 additions and 416 deletions
+3 -1
View File
@@ -98,7 +98,9 @@ class MyApp extends StatelessWidget {
return MaterialApp( return MaterialApp(
theme: ThemeData.light(), theme: ThemeData.light(),
darkTheme: ThemeData.dark(), darkTheme: ThemeData.dark(),
themeMode: ThemeMode.system, themeMode: ThemeMode.light,
///TODO change to system once dark theme is implemented
builder: (context, widget) { builder: (context, widget) {
return StreamChat( return StreamChat(
child: widget, child: widget,
+2 -1
View File
@@ -1,6 +1,7 @@
name: example name: example
description: A new Flutter project. description: A new Flutter project.
version: 1.0.40+42
version: 1.0.41+43
environment: environment:
sdk: ">=2.2.2 <3.0.0" sdk: ">=2.2.2 <3.0.0"
+188 -169
View File
@@ -41,195 +41,211 @@ class GiphyAttachment extends StatelessWidget {
return Column( return Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Card( DecoratedBox(
clipBehavior: Clip.antiAlias, decoration: BoxDecoration(),
shape: RoundedRectangleBorder( child: Card(
borderRadius: BorderRadius.only( elevation: 2,
topRight: Radius.circular(16.0), clipBehavior: Clip.antiAlias,
bottomRight: Radius.circular(0.0), shape: RoundedRectangleBorder(
topLeft: Radius.circular(16.0), borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(16.0), topRight: Radius.circular(16.0),
bottomRight: Radius.circular(0.0),
topLeft: Radius.circular(16.0),
bottomLeft: Radius.circular(16.0),
),
), ),
), child: Column(
child: Column( mainAxisSize: MainAxisSize.min,
mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch,
crossAxisAlignment: CrossAxisAlignment.stretch, children: <Widget>[
children: <Widget>[ Stack(
Padding( children: [
padding: const EdgeInsets.all(8.0), Padding(
child: GestureDetector( padding: const EdgeInsets.all(8.0),
onTap: () { child: GestureDetector(
Navigator.push(context, MaterialPageRoute(builder: (_) { onTap: () {
return FullScreenImage( Navigator.push(context,
url: attachment.imageUrl ?? MaterialPageRoute(builder: (_) {
attachment.assetUrl ?? return FullScreenImage(
attachment.thumbUrl, url: attachment.imageUrl ??
); attachment.assetUrl ??
})); attachment.thumbUrl,
}, );
child: CachedNetworkImage( }));
height: size?.height, },
width: size?.width, child: CachedNetworkImage(
placeholder: (_, __) { height: size?.height,
return Container( width: size?.width,
width: size?.width, placeholder: (_, __) {
height: size?.height, return Container(
child: Center( width: size?.width,
child: CircularProgressIndicator(), height: size?.height,
child: Center(
child: CircularProgressIndicator(),
),
);
},
imageUrl: attachment.thumbUrl ??
attachment.imageUrl ??
attachment.assetUrl,
errorWidget: (context, url, error) => AttachmentError(
attachment: attachment,
size: size,
),
fit: BoxFit.cover,
), ),
); ),
},
imageUrl: attachment.thumbUrl ??
attachment.imageUrl ??
attachment.assetUrl,
errorWidget: (context, url, error) => AttachmentError(
attachment: attachment,
size: size,
), ),
fit: BoxFit.cover, Positioned(
), left: 0,
), top: 0,
), child: Container(
Positioned( decoration: BoxDecoration(
left: 0, color: Colors.white,
top: 0, borderRadius: BorderRadius.only(
child: Container( bottomRight: Radius.circular(16.0),
decoration: BoxDecoration( )),
color: Colors.white, child: Padding(
borderRadius: BorderRadius.only( padding: const EdgeInsets.only(
bottomRight: Radius.circular(16.0), left: 8.0,
)), right: 8.0,
child: Padding( top: 8.0,
padding: const EdgeInsets.only( bottom: 4.0,
left: 8.0, ),
right: 8.0, child: Row(
top: 8.0, children: [
bottom: 4.0, Icon(
), StreamIcons.lightning,
child: Row( color: StreamChatTheme.of(context).accentColor,
children: [ size: 16.0,
Icon( ),
StreamIcons.lightning, Text(
color: StreamChatTheme.of(context).accentColor, 'GIPHY',
size: 16.0, style: TextStyle(
), color:
Text( StreamChatTheme.of(context).accentColor,
'GIPHY', fontWeight: FontWeight.bold,
style: TextStyle( fontSize: 11.0,
color: StreamChatTheme.of(context).accentColor, ),
fontWeight: FontWeight.bold, ),
fontSize: 11.0, ],
), ),
), ),
], ),
), ),
), ],
), ),
), if (attachment.title != null)
if (attachment.title != null) Container(
Container( alignment: Alignment.bottomCenter,
alignment: Alignment.bottomCenter, child: Padding(
child: Material( padding: const EdgeInsets.symmetric(horizontal: 8.0),
color: Colors.white, child: Row(
child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
children: [ Card(
Card( elevation: 2,
child: IconButton( child: IconButton(
icon: Icon( padding: const EdgeInsets.all(0),
StreamIcons.left, constraints: BoxConstraints.tight(Size(32, 32)),
size: 24.0, icon: Icon(
StreamIcons.left,
size: 24.0,
),
splashRadius: 16,
onPressed: () {
streamChannel.channel.sendAction(message, {
'image_action': 'shuffle',
});
},
), ),
splashRadius: 24, shape: CircleBorder(),
onPressed: () {
streamChannel.channel.sendAction(message, {
'image_action': 'shuffle',
});
},
), ),
shape: CircleBorder(), Expanded(
), child: Center(
Expanded( child: Text(
child: Center( '"${attachment.title}"',
child: Text( style: TextStyle(
'"${attachment.title}"', fontStyle: FontStyle.italic,
style: TextStyle( ),
fontStyle: FontStyle.italic,
), ),
), ),
), ),
), Card(
Card( elevation: 2,
child: IconButton( child: IconButton(
icon: Icon( padding: const EdgeInsets.all(0),
StreamIcons.right, constraints: BoxConstraints.tight(Size(32, 32)),
size: 24.0, icon: Icon(
StreamIcons.right,
size: 24.0,
),
splashRadius: 16,
onPressed: () {
streamChannel.channel.sendAction(message, {
'image_action': 'shuffle',
});
},
), ),
splashRadius: 24, shape: CircleBorder(),
onPressed: () {
streamChannel.channel.sendAction(message, {
'image_action': 'shuffle',
});
},
), ),
shape: CircleBorder(), ],
), ),
],
), ),
), ),
SizedBox(
height: 4.0,
), ),
SizedBox( Container(
height: 4.0, color: Colors.black.withOpacity(0.2),
), width: double.infinity,
Container( height: 0.5,
color: Colors.black.withOpacity(0.2), ),
width: double.infinity, Row(
height: 0.5, mainAxisAlignment: MainAxisAlignment.start,
), crossAxisAlignment: CrossAxisAlignment.center,
Row( children: [
mainAxisAlignment: MainAxisAlignment.start, Expanded(
crossAxisAlignment: CrossAxisAlignment.center, child: FlatButton(
children: [ height: 50,
Expanded( onPressed: () {
child: FlatButton( streamChannel.channel.sendAction(message, {
height: 50, 'image_action': 'cancel',
onPressed: () { });
streamChannel.channel.sendAction(message, { },
'image_action': 'cancel', child: Text(
}); 'Cancel',
}, style: TextStyle(
child: Text( fontWeight: FontWeight.bold,
'Cancel', color: Colors.black.withOpacity(0.5)),
style: TextStyle( ),
fontWeight: FontWeight.bold,
color: Colors.black.withOpacity(0.5)),
), ),
), ),
), Container(
Container( width: 0.5,
width: 0.5, color: Colors.black.withOpacity(0.2),
color: Colors.black.withOpacity(0.2), height: 50.0,
height: 50.0, ),
), Expanded(
Expanded( child: FlatButton(
child: FlatButton( height: 50,
height: 50, onPressed: () {
onPressed: () { streamChannel.channel.sendAction(message, {
streamChannel.channel.sendAction(message, { 'image_action': 'send',
'image_action': 'send', });
}); },
}, child: Text(
child: Text( 'Send',
'Send', style: TextStyle(
style: TextStyle( color: StreamChatTheme.of(context).accentColor,
color: StreamChatTheme.of(context).accentColor, fontWeight: FontWeight.bold),
fontWeight: FontWeight.bold), ),
), ),
), ),
), ],
], ),
), ],
], ),
), ),
), ),
SizedBox( SizedBox(
@@ -313,7 +329,10 @@ class GiphyAttachment extends StatelessWidget {
), ),
), ),
Padding( Padding(
padding: const EdgeInsets.only(top: 8.0), padding: const EdgeInsets.only(
top: 8.0,
bottom: 8,
),
child: Row( child: Row(
children: [ children: [
Row( Row(
+98 -35
View File
@@ -7,6 +7,7 @@ import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart'; import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:http_parser/http_parser.dart'; import 'package:http_parser/http_parser.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import 'package:mime/mime.dart'; import 'package:mime/mime.dart';
@@ -223,22 +224,66 @@ class MessageInputState extends State<MessageInput> {
} }
Widget _buildDmCheckbox() { Widget _buildDmCheckbox() {
return Row( return Container(
children: [ height: 36,
Checkbox( padding: const EdgeInsets.only(
value: _sendAsDm, left: 12,
onChanged: (val) => setState( bottom: 12,
() { top: 8,
_sendAsDm = val; ),
}, child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Container(
height: 16,
width: 16,
foregroundDecoration: BoxDecoration(
border: _sendAsDm
? null
: Border.all(
color: Colors.black.withOpacity(.5),
width: 2,
),
borderRadius: BorderRadius.circular(3),
),
child: Center(
child: Material(
borderRadius: BorderRadius.circular(3),
color: _sendAsDm
? StreamChatTheme.of(context).accentColor
: Colors.white,
child: InkWell(
onTap: () {
setState(() {
_sendAsDm = !_sendAsDm;
});
},
child: AnimatedCrossFade(
duration: Duration(milliseconds: 300),
reverseDuration: Duration(milliseconds: 300),
crossFadeState: _sendAsDm
? CrossFadeState.showFirst
: CrossFadeState.showSecond,
firstChild: Icon(
StreamIcons.check,
size: 16.0,
color: Colors.white,
),
secondChild: SizedBox(
height: 16,
width: 16,
),
),
),
),
),
), ),
activeColor: StreamChatTheme.of(context).accentColor, Padding(
), padding: const EdgeInsets.symmetric(horizontal: 16.0),
Padding( child: Text('Send also as direct message'),
padding: const EdgeInsets.symmetric(horizontal: 8.0), ),
child: Text('Send also as direct message'), ],
), ),
],
); );
} }
@@ -506,25 +551,19 @@ class MessageInputState extends State<MessageInput> {
child: Icon(StreamIcons.lightning, child: Icon(StreamIcons.lightning,
color: StreamChatTheme.of(context).accentColor), color: StreamChatTheme.of(context).accentColor),
), ),
Text('Instant Commands') Text(
'Instant Commands',
style: TextStyle(
color: Colors.black.withOpacity(.5),
),
)
], ],
), ),
), ),
...commands ...commands
.map( .map(
(c) => ListTile( (c) => ListTile(
leading: c.name == 'giphy' leading: c.name == 'giphy' ? _buildGiphyIcon() : null,
? CircleAvatar(
backgroundColor: Colors.black,
child: Image.asset(
'images/giphy_icon.png',
package: 'stream_chat_flutter',
width: 16.0,
height: 16.0,
),
maxRadius: 12.0,
)
: null,
title: Text.rich( title: Text.rich(
TextSpan( TextSpan(
text: '${c.name.capitalize()}', text: '${c.name.capitalize()}',
@@ -565,6 +604,31 @@ class MessageInputState extends State<MessageInput> {
}); });
} }
CircleAvatar _buildGiphyIcon() {
if (kIsWeb) {
return CircleAvatar(
backgroundColor: Colors.black,
child: Image.asset(
'images/giphy_icon.png',
package: 'stream_chat_flutter',
width: 24.0,
height: 24.0,
),
radius: 12,
);
} else {
return CircleAvatar(
child: SvgPicture.asset(
'svgs/giphy_icon.svg',
package: 'stream_chat_flutter',
width: 24.0,
height: 24.0,
),
radius: 12,
);
}
}
OverlayEntry _buildMentionsOverlayEntry() { OverlayEntry _buildMentionsOverlayEntry() {
final splits = textEditingController.text final splits = textEditingController.text
.substring(0, textEditingController.value.selection.start) .substring(0, textEditingController.value.selection.start)
@@ -605,13 +669,6 @@ class MessageInputState extends State<MessageInput> {
child: Container( child: Container(
constraints: BoxConstraints.loose(Size.fromHeight(400)), constraints: BoxConstraints.loose(Size.fromHeight(400)),
decoration: BoxDecoration( decoration: BoxDecoration(
boxShadow: [
BoxShadow(
spreadRadius: -8,
blurRadius: 5.0,
offset: Offset(0, -4),
),
],
color: StreamChatTheme.of(context).primaryColor, color: StreamChatTheme.of(context).primaryColor,
), ),
child: FutureBuilder<List<Member>>( child: FutureBuilder<List<Member>>(
@@ -624,6 +681,12 @@ class MessageInputState extends State<MessageInput> {
children: snapshot.data children: snapshot.data
.map((m) => ListTile( .map((m) => ListTile(
leading: UserAvatar( leading: UserAvatar(
constraints: BoxConstraints.tight(
Size(
40,
40,
),
),
user: m.user, user: m.user,
), ),
title: Text( title: Text(
+193 -208
View File
@@ -1,12 +1,13 @@
import 'dart:async'; import 'dart:async';
import 'dart:math';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:jiffy/jiffy.dart'; import 'package:jiffy/jiffy.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/message_widget.dart'; import 'package:stream_chat_flutter/src/message_widget.dart';
import 'package:stream_chat_flutter/src/system_message.dart'; import 'package:stream_chat_flutter/src/system_message.dart';
import 'package:visibility_detector/visibility_detector.dart'; import 'package:visibility_detector/visibility_detector.dart';
import 'package:widgets_visibility_provider/widgets_visibility_provider.dart';
import '../stream_chat_flutter.dart'; import '../stream_chat_flutter.dart';
import 'date_divider.dart'; import 'date_divider.dart';
@@ -106,7 +107,10 @@ class MessageListView extends StatefulWidget {
this.onThreadTap, this.onThreadTap,
this.dateDividerBuilder, this.dateDividerBuilder,
this.scrollPhysics = const AlwaysScrollableScrollPhysics(), this.scrollPhysics = const AlwaysScrollableScrollPhysics(),
this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual, this.initialScrollIndex = 0,
this.initialAlignment = 0,
this.scrollController,
this.itemPositionListener,
}) : super(key: key); }) : super(key: key);
/// Function used to build a custom message widget /// Function used to build a custom message widget
@@ -131,95 +135,101 @@ class MessageListView extends StatefulWidget {
/// Builder used to render date dividers /// Builder used to render date dividers
final Widget Function(DateTime) dateDividerBuilder; final Widget Function(DateTime) dateDividerBuilder;
/// Index of an item to initially align within the viewport.
final int initialScrollIndex;
/// Determines where the leading edge of the item at [initialScrollIndex]
/// should be placed.
final double initialAlignment;
/// Controller for jumping or scrolling to an item.
final ItemScrollController scrollController;
/// Provides a listenable iterable of [itemPositions] of items that are on
/// screen and their locations.
final ItemPositionsListener itemPositionListener;
/// The ScrollPhysics used by the ListView /// The ScrollPhysics used by the ListView
final ScrollPhysics scrollPhysics; final ScrollPhysics scrollPhysics;
/// The [ScrollViewKeyboardDismissBehavior] used by the ListView
final ScrollViewKeyboardDismissBehavior keyboardDismissBehavior;
@override @override
_MessageListViewState createState() => _MessageListViewState(); _MessageListViewState createState() => _MessageListViewState();
} }
class _MessageListViewState extends State<MessageListView> { class _MessageListViewState extends State<MessageListView> {
static const _newMessageLoadingOffset = 100; ItemScrollController _scrollController;
final ScrollController _scrollController = ScrollController(); bool _bottomWasVisible = false;
bool _bottomWasVisible = true;
bool _topWasVisible = false; bool _topWasVisible = false;
List<Message> _messages = [];
List<Message> _newMessageList = [];
Function _onThreadTap; Function _onThreadTap;
bool _showScrollToBottom = false; bool _showScrollToBottom = false;
ItemPositionsListener _itemPositionListener;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final streamChannel = StreamChannel.of(context); final streamChannel = StreamChannel.of(context);
/// TODO: find a better solution when (https://github.com/flutter/flutter/issues/21023) is fixed final messagesStream = widget.parentMessage != null
return WidgetsVisibilityProvider( ? streamChannel.channel.state.threadsStream
condition: (positionData) => .where((threads) => threads.containsKey(widget.parentMessage.id))
positionData.endPosition >= 20 && .map((threads) => threads[widget.parentMessage.id])
positionData.startPosition <= positionData.viewportSize, : streamChannel.channel.state.messagesStream;
child: Stack(
alignment: Alignment.center, return StreamBuilder<List<Message>>(
children: [ stream: messagesStream,
NotificationListener<ScrollNotification>( initialData: widget.parentMessage != null
onNotification: (_) { ? streamChannel.channel.state.threads[widget.parentMessage.id]
if (_scrollController.offset < 150 && : streamChannel.channel.state.messages,
_newMessageList.isNotEmpty) { builder: (context, snapshot) {
setState(() { final messages = snapshot.data?.reversed?.toList() ?? [];
_messages.insertAll(0, _newMessageList); return Stack(
_newMessageList.clear(); alignment: Alignment.center,
}); children: [
} ScrollablePositionedList.builder(
return true; itemPositionsListener: _itemPositionListener,
}, addAutomaticKeepAlives: true,
child: ListView.custom( key: Key('messageListView'),
key: Key('messageListView'), initialScrollIndex: widget.initialScrollIndex,
physics: widget.scrollPhysics, initialAlignment: widget.initialAlignment,
keyboardDismissBehavior: widget.keyboardDismissBehavior, physics: widget.scrollPhysics,
controller: _scrollController, itemScrollController: _scrollController,
reverse: true, reverse: true,
childrenDelegate: SliverChildBuilderDelegate( itemCount: messages.length +
(context, i) { 1 +
if (i == _messages.length + 1) { (widget.parentMessage != null ? 1 : 0),
if (widget.parentMessage != null) { itemBuilder: (context, i) {
if (widget.parentMessageBuilder != null) { if (i == messages.length + 1) {
return widget.parentMessageBuilder( if (widget.parentMessageBuilder != null) {
context, return widget.parentMessageBuilder(
widget.parentMessage, 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),
),
),
],
);
}
} else { } else {
return SizedBox(); 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) { if (i == messages.length) {
return _buildLoadingIndicator(streamChannel); return _buildLoadingIndicator(streamChannel);
} }
final message = _messages[i]; final message = messages[i];
final nextMessage = i > 0 ? _messages[i - 1] : null; final nextMessage = i > 0 ? messages[i - 1] : null;
Widget messageWidget; Widget messageWidget;
@@ -227,14 +237,14 @@ class _MessageListViewState extends State<MessageListView> {
messageWidget = _buildBottomMessage( messageWidget = _buildBottomMessage(
context, context,
message, message,
_messages, messages,
streamChannel, streamChannel,
); );
} else if (i == _messages.length - 1) { } else if (i == messages.length - 1) {
messageWidget = _buildTopMessage( messageWidget = _buildTopMessage(
context, context,
message, message,
_messages, messages,
streamChannel, streamChannel,
); );
} else { } else {
@@ -246,13 +256,13 @@ class _MessageListViewState extends State<MessageListView> {
MessageDetails( MessageDetails(
context, context,
message, message,
_messages, messages,
i, i,
), ),
_messages), messages),
); );
} else { } else {
messageWidget = buildMessage(message, _messages, i); messageWidget = buildMessage(message, messages, i);
} }
} }
@@ -265,84 +275,74 @@ class _MessageListViewState extends State<MessageListView> {
messageWidget, messageWidget,
Padding( Padding(
padding: const EdgeInsets.symmetric(vertical: 12.0), padding: const EdgeInsets.symmetric(vertical: 12.0),
child: VisibleNotifierWidget( child: widget.dateDividerBuilder != null
condition: ( ? widget.dateDividerBuilder(
ScrollNotification previousNotification, nextMessage.createdAt.toLocal())
PositionData previousPositionData, : DateDivider(
ScrollNotification currentNotification, dateTime: nextMessage.createdAt.toLocal(),
PositionData currentPositionData, ),
) {
print(
'currentPositionData.endPosition: ${currentPositionData.endPosition}');
return true;
},
data: i,
child: widget.dateDividerBuilder != null
? widget.dateDividerBuilder(
nextMessage.createdAt.toLocal())
: DateDivider(
dateTime: nextMessage.createdAt.toLocal(),
),
),
), ),
], ],
); );
} }
return VisibleNotifierWidget( return messageWidget;
child: messageWidget,
data: i,
);
},
childCount: _messages.length + 2,
findChildIndexCallback: (key) {
final ValueKey<String> valueKey = key;
final index = _messages
.indexWhere((m) => 'MESSAGE-${m.id}' == valueKey.value);
return index != -1 ? index : null;
}, },
), ),
), if (widget.showScrollToBottom)
), StreamBuilder<int>(
if (widget.showScrollToBottom) stream: streamChannel.channel.state.unreadCountStream,
StreamBuilder<Event>( builder: (context, snapshot) {
stream: streamChannel.channel.on( if (!_showScrollToBottom ||
EventType.messageNew, !snapshot.hasData ||
), snapshot.data == 0) {
builder: (context, _) { return SizedBox();
if (!_showScrollToBottom || }
streamChannel.channel.state.unreadCount == 0) { return _buildScrollToBottom(snapshot.data);
return SizedBox(); }),
} Positioned(
return _buildScrollToBottom(streamChannel); top: 20.0,
}), child: ValueListenableBuilder<Iterable<ItemPosition>>(
Positioned( valueListenable: _itemPositionListener.itemPositions,
top: 20.0, builder: (context, values, _) {
child: WidgetsVisibilityConsumer( final items = _itemPositionListener.itemPositions?.value;
listener: ( if (items.isEmpty || messages.isEmpty) {
context, return SizedBox();
event, }
) {},
builder: (context, event) {
if (event.positionDataList == null ||
event.positionDataList.isEmpty) {
return Container();
}
return DateDivider( var index = _getTopElement(values).index;
dateTime: _messages[event.positionDataList.last.data]
.createdAt if (index > messages.length) {
.toLocal(), 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(),
);
},
),
),
],
);
});
} }
Widget _buildScrollToBottom(StreamChannelState streamChannel) { ItemPosition _getTopElement(Iterable<ItemPosition> values) {
return values
.where((ItemPosition position) => position.itemLeadingEdge < 0.9)
.reduce((ItemPosition max, ItemPosition position) =>
position.itemLeadingEdge > max.itemLeadingEdge ? position : max);
}
Widget _buildScrollToBottom(int unreadCount) {
return Positioned( return Positioned(
bottom: 8, bottom: 8,
right: 8, right: 8,
@@ -358,13 +358,11 @@ class _MessageListViewState extends State<MessageListView> {
color: Colors.black, color: Colors.black,
), ),
onPressed: () { onPressed: () {
Future.delayed(Duration(milliseconds: 500), () { setState(() {
setState(() { _showScrollToBottom = false;
_showScrollToBottom = false;
});
}); });
_scrollController.animateTo( _scrollController.scrollTo(
0, index: 0,
duration: Duration(seconds: 1), duration: Duration(seconds: 1),
curve: Curves.easeInOut, curve: Curves.easeInOut,
); );
@@ -377,7 +375,7 @@ class _MessageListViewState extends State<MessageListView> {
top: -10, top: -10,
child: CircleAvatar( child: CircleAvatar(
radius: 20, radius: 20,
child: Text(streamChannel.channel.state.unreadCount.toString()), child: Text(unreadCount.toString()),
), ),
), ),
], ],
@@ -387,7 +385,9 @@ class _MessageListViewState extends State<MessageListView> {
Container _buildLoadingIndicator(StreamChannelState streamChannel) { Container _buildLoadingIndicator(StreamChannelState streamChannel) {
return Container( return Container(
key: Key('LOADING-INDICATOR'),
height: 50, height: 50,
width: double.infinity,
child: StreamBuilder<bool>( child: StreamBuilder<bool>(
stream: streamChannel.queryMessage, stream: streamChannel.queryMessage,
initialData: false, initialData: false,
@@ -401,7 +401,7 @@ class _MessageListViewState extends State<MessageListView> {
); );
} }
if (!snapshot.data) { if (!snapshot.data) {
return Container(); return SizedBox();
} }
return Center( return Center(
child: Padding( child: Padding(
@@ -428,14 +428,14 @@ class _MessageListViewState extends State<MessageListView> {
MessageDetails( MessageDetails(
context, context,
message, message,
_messages, messages,
_messages.length - 1, messages.length - 1,
), ),
_messages, messages,
), ),
); );
} else { } else {
messageWidget = buildMessage(message, messages, _messages.length - 1); messageWidget = buildMessage(message, messages, messages.length - 1);
} }
return VisibilityDetector( return VisibilityDetector(
@@ -470,10 +470,10 @@ class _MessageListViewState extends State<MessageListView> {
MessageDetails( MessageDetails(
context, context,
message, message,
_messages, messages,
0, 0,
), ),
_messages, messages,
), ),
); );
} else { } else {
@@ -492,9 +492,11 @@ class _MessageListViewState extends State<MessageListView> {
} }
} }
_bottomWasVisible = isVisible; _bottomWasVisible = isVisible;
setState(() { if (mounted) {
_showScrollToBottom = !isVisible; setState(() {
}); _showScrollToBottom = !isVisible;
});
}
}, },
child: messageWidget, child: messageWidget,
); );
@@ -541,6 +543,7 @@ class _MessageListViewState extends State<MessageListView> {
) { ) {
if (message.type == 'system' && message.text?.isNotEmpty == true) { if (message.type == 'system' && message.text?.isNotEmpty == true) {
return SystemMessage( return SystemMessage(
key: ValueKey<String>('MESSAGE-${message.id}'),
message: message, message: message,
); );
} }
@@ -563,6 +566,7 @@ class _MessageListViewState extends State<MessageListView> {
final allRead = readList.length >= (channel.memberCount ?? 0) - 1; final allRead = readList.length >= (channel.memberCount ?? 0) - 1;
return MessageWidget( return MessageWidget(
key: ValueKey<String>('MESSAGE-${message.id}'),
message: message, message: message,
reverse: isMyMessage, reverse: isMyMessage,
showReactions: !message.isDeleted, showReactions: !message.isDeleted,
@@ -600,64 +604,45 @@ class _MessageListViewState extends State<MessageListView> {
); );
} }
StreamSubscription _streamListener; StreamSubscription _messageNewListener;
@override @override
void initState() { void initState() {
super.initState(); _scrollController = widget.scrollController ?? ItemScrollController();
_itemPositionListener =
widget.itemPositionListener ?? ItemPositionsListener.create();
final streamChannel = StreamChannel.of(context); final streamChannel = StreamChannel.of(context);
Stream<List<Message>> stream; _messageNewListener =
streamChannel.channel.on(EventType.messageNew).listen((event) {
if (widget.parentMessage == null) { final firstElementInViewport =
stream = streamChannel.channel.state.messagesStream; _itemPositionListener.itemPositions.value.first;
} else { if (event.message.user.id == streamChannel.channel.client.state.user.id) {
streamChannel.getReplies(widget.parentMessage.id); WidgetsBinding.instance.addPostFrameCallback((_) {
stream = streamChannel.channel.state.threadsStream _scrollController.jumpTo(
.where((threads) => threads.containsKey(widget.parentMessage.id)) index: 0,
.map((threads) => threads[widget.parentMessage.id]); );
}
_streamListener = stream
.map((messages) =>
messages
?.where((m) =>
!(m.status == MessageSendingStatus.FAILED && m.isDeleted))
?.toList() ??
[])
.listen((newMessages) {
newMessages = newMessages.reversed.toList();
if (_messages.isEmpty ||
newMessages.isEmpty ||
newMessages.first.id != _messages.first.id) {
if (!_scrollController.hasClients ||
_scrollController.offset < _newMessageLoadingOffset) {
if (streamChannel.channel.state.unreadCount > 0) {
streamChannel.channel.markRead();
}
setState(() {
_messages = newMessages;
});
} else if (newMessages.first.user.id ==
streamChannel.channel.client.state.user.id) {
_scrollController.jumpTo(0);
WidgetsBinding.instance.addPostFrameCallback((_) {
setState(() {
_messages = newMessages;
});
});
} else {
_newMessageList = newMessages;
}
} else {
setState(() {
_messages = newMessages;
}); });
} else {
if (firstElementInViewport.index != 0) {
_scrollController.jumpTo(
index: firstElementInViewport.index + 1,
alignment: firstElementInViewport.itemLeadingEdge,
);
}
}
if (firstElementInViewport.index == 0) {
streamChannel.channel.markRead();
} }
}); });
if (widget.parentMessage != null) {
streamChannel.getReplies(widget.parentMessage.id);
}
_getOnThreadTap(); _getOnThreadTap();
super.initState();
} }
void _getOnThreadTap() { void _getOnThreadTap() {
@@ -696,7 +681,7 @@ class _MessageListViewState extends State<MessageListView> {
@override @override
void dispose() { void dispose() {
_streamListener.cancel(); _messageNewListener?.cancel();
super.dispose(); super.dispose();
} }
} }
+4 -2
View File
@@ -14,7 +14,9 @@ dependencies:
flutter_app_badger: ^1.1.2 flutter_app_badger: ^1.1.2
photo_view: ^0.10.1 photo_view: ^0.10.1
rxdart: ^0.24.1 rxdart: ^0.24.1
scrollable_positioned_list: ^0.1.8
jiffy: ^3.0.1 jiffy: ^3.0.1
flutter_svg: ^0.19.1
flutter_portal: ^0.3.0 flutter_portal: ^0.3.0
cached_network_image: ^2.2.0+1 cached_network_image: ^2.2.0+1
flutter_markdown: ^0.5.0 flutter_markdown: ^0.5.0
@@ -33,11 +35,11 @@ dependencies:
flutter_slidable: ^0.5.4 flutter_slidable: ^0.5.4
carousel_slider: ^2.2.1 carousel_slider: ^2.2.1
clipboard: ^0.1.2+8 clipboard: ^0.1.2+8
widgets_visibility_provider: ^2.0.2
flutter: flutter:
assets: assets:
- images/giphy_icon.png - images/
- svgs/
fonts: fonts:
- family: stream-icons - family: stream-icons
fonts: fonts:
+8
View File
@@ -0,0 +1,8 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 24C18.6274 24 24 18.6274 24 12C24 5.37258 18.6274 0 12 0C5.37258 0 0 5.37258 0 12C0 18.6274 5.37258 24 12 24Z" fill="black"/>
<rect x="7" y="7" width="1" height="10" fill="#00FF99"/>
<rect x="16" y="10" width="1" height="8" fill="#9D34FF"/>
<rect x="7" y="6" width="7" height="1" fill="#FFFF9C"/>
<rect x="7" y="17" width="9" height="1" fill="#00CCFF"/>
<path d="M14 6H15V7.5H16V9H17V10H14V6Z" fill="#FF6666"/>
</svg>

After

Width:  |  Height:  |  Size: 527 B