Merge branch 'master' into feature/new-ui
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class StreamBackButton extends StatelessWidget {
|
||||
const StreamBackButton({
|
||||
Key key,
|
||||
this.onPressed,
|
||||
this.icon = Icons.arrow_back_ios_outlined,
|
||||
}) : super(key: key);
|
||||
|
||||
final VoidCallback onPressed;
|
||||
final IconData icon;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(14.0),
|
||||
child: RawMaterialButton(
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)),
|
||||
elevation: 0,
|
||||
highlightElevation: 0,
|
||||
focusElevation: 0,
|
||||
disabledElevation: 0,
|
||||
hoverElevation: 0,
|
||||
onPressed: () {
|
||||
if (onPressed != null) {
|
||||
onPressed();
|
||||
} else {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
},
|
||||
fillColor: Theme.of(context).brightness == Brightness.dark
|
||||
? Colors.white.withOpacity(.1)
|
||||
: Colors.black.withOpacity(.1),
|
||||
child: Icon(
|
||||
icon ?? Icons.arrow_back_ios_outlined,
|
||||
size: 15,
|
||||
color: Theme.of(context).brightness == Brightness.dark
|
||||
? Colors.white
|
||||
: Colors.black,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat/stream_chat.dart';
|
||||
import 'package:stream_chat_flutter/src/back_button.dart';
|
||||
import 'package:stream_chat_flutter/src/channel_info.dart';
|
||||
import 'package:stream_chat_flutter/src/channel_name.dart';
|
||||
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
|
||||
@@ -78,7 +79,9 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||
final channel = StreamChannel.of(context).channel;
|
||||
return AppBar(
|
||||
elevation: 1,
|
||||
leading: showBackButton ? _buildBackButton(context) : Container(),
|
||||
leading: showBackButton
|
||||
? StreamBackButton(onPressed: onBackPressed)
|
||||
: SizedBox(),
|
||||
backgroundColor:
|
||||
StreamChatTheme.of(context).channelTheme.channelHeaderTheme.color,
|
||||
actions: <Widget>[
|
||||
@@ -119,38 +122,6 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Padding _buildBackButton(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(14.0),
|
||||
child: RawMaterialButton(
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)),
|
||||
elevation: 0,
|
||||
highlightElevation: 0,
|
||||
focusElevation: 0,
|
||||
disabledElevation: 0,
|
||||
hoverElevation: 0,
|
||||
onPressed: () {
|
||||
if (onBackPressed != null) {
|
||||
onBackPressed();
|
||||
} else {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
},
|
||||
fillColor: Theme.of(context).brightness == Brightness.dark
|
||||
? Colors.white.withOpacity(.1)
|
||||
: Colors.black.withOpacity(.1),
|
||||
padding: EdgeInsets.all(4),
|
||||
child: Icon(
|
||||
Icons.arrow_back_ios,
|
||||
size: 15,
|
||||
color: Theme.of(context).brightness == Brightness.dark
|
||||
? Colors.white
|
||||
: Colors.black,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
final Size preferredSize;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_slidable/flutter_slidable.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
@@ -528,9 +531,10 @@ class _ChannelListViewState extends State<ChannelListView>
|
||||
super.didUpdateWidget(oldWidget);
|
||||
|
||||
if (widget.filter?.toString() != oldWidget.filter?.toString() ||
|
||||
widget.sort != oldWidget.sort ||
|
||||
widget.pagination != oldWidget.pagination ||
|
||||
widget.options != oldWidget.options) {
|
||||
jsonEncode(widget.sort) != jsonEncode(oldWidget.sort) ||
|
||||
widget.pagination?.toJson()?.toString() !=
|
||||
oldWidget.pagination?.toJson()?.toString() ||
|
||||
widget.options?.toString() != oldWidget.options?.toString()) {
|
||||
final channelsBloc = ChannelsBloc.of(context);
|
||||
channelsBloc.queryChannels(
|
||||
filter: widget.filter,
|
||||
|
||||
@@ -135,7 +135,8 @@ class ChannelsBlocState extends State<ChannelsBloc>
|
||||
newChannels.insert(0, _hiddenChannels[hiddenIndex]);
|
||||
_hiddenChannels.removeAt(hiddenIndex);
|
||||
} else {
|
||||
if (client.state.channels[e.cid] != null) {
|
||||
if (client.state?.channels != null &&
|
||||
client.state?.channels[e.cid] != null) {
|
||||
newChannels.insert(0, client.state.channels[e.cid]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -283,6 +283,7 @@ class MessageInputState extends State<MessageInput> {
|
||||
prefixText: ' ',
|
||||
border: InputBorder.none,
|
||||
),
|
||||
textCapitalization: TextCapitalization.sentences,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -139,7 +139,7 @@ class MessageListView extends StatefulWidget {
|
||||
class _MessageListViewState extends State<MessageListView> {
|
||||
static const _newMessageLoadingOffset = 100;
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
bool _isBottom = true;
|
||||
bool _bottomWasVisible = true;
|
||||
bool _topWasVisible = false;
|
||||
List<Message> _messages = [];
|
||||
List<Message> _newMessageList = [];
|
||||
@@ -333,7 +333,11 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
onVisibilityChanged: (visibility) {
|
||||
final topIsVisible = visibility.visibleBounds != Rect.zero;
|
||||
if (topIsVisible && !_topWasVisible) {
|
||||
streamChannel.queryMessages();
|
||||
if (widget.parentMessage == null) {
|
||||
streamChannel.queryMessages();
|
||||
} else {
|
||||
streamChannel.getReplies(widget.parentMessage.id);
|
||||
}
|
||||
}
|
||||
_topWasVisible = topIsVisible;
|
||||
},
|
||||
@@ -368,12 +372,15 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
return VisibilityDetector(
|
||||
key: ValueKey<String>('BOTTOM-MESSAGE'),
|
||||
onVisibilityChanged: (visibility) {
|
||||
_isBottom = visibility.visibleBounds != Rect.zero;
|
||||
if (_isBottom && streamChannel.channel.config?.readEvents == true) {
|
||||
final isVisible = visibility.visibleBounds != Rect.zero;
|
||||
if (isVisible &&
|
||||
!_bottomWasVisible &&
|
||||
streamChannel.channel.config?.readEvents == true) {
|
||||
if (streamChannel.channel.state.unreadCount > 0) {
|
||||
streamChannel.channel.markRead();
|
||||
}
|
||||
}
|
||||
_bottomWasVisible = isVisible;
|
||||
},
|
||||
child: messageWidget,
|
||||
);
|
||||
@@ -433,7 +440,8 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
final readList = channel.state?.read
|
||||
?.where((element) => element.user.id != userId)
|
||||
?.where((read) =>
|
||||
read.lastRead.isAfter(message.createdAt) &&
|
||||
(read.lastRead.isAfter(message.createdAt) ||
|
||||
read.lastRead.isAtSameMomentAs(message.createdAt)) &&
|
||||
(index == 0 ||
|
||||
read.lastRead.isBefore(messages[index - 1].createdAt)))
|
||||
?.toList();
|
||||
@@ -485,9 +493,6 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
super.initState();
|
||||
|
||||
final streamChannel = StreamChannel.of(context);
|
||||
if (streamChannel.channel.state.unreadCount > 0) {
|
||||
streamChannel.channel.markRead();
|
||||
}
|
||||
|
||||
Stream<List<Message>> stream;
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ class ReactionPicker extends StatelessWidget {
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: reactionAssets.map((reactionAsset) {
|
||||
final ownReactionIndex = message.ownReactions?.indexWhere(
|
||||
final ownReactionIndex = message.latestReactions?.indexWhere(
|
||||
(reaction) => reaction.type == reactionAsset.type) ??
|
||||
-1;
|
||||
return IconButton(
|
||||
|
||||
@@ -54,7 +54,7 @@ class StreamChannelState extends State<StreamChannel> {
|
||||
|
||||
/// Calls [channel.query] updating [queryMessage] stream
|
||||
void queryMessages() {
|
||||
if (_paginationEnded) {
|
||||
if (_queryMessageController.value == true || _paginationEnded) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -65,15 +65,18 @@ class StreamChannelState extends State<StreamChannel> {
|
||||
firstId = channel.state.messages.first.id;
|
||||
}
|
||||
|
||||
final messageLimit = 50;
|
||||
|
||||
widget.channel
|
||||
.query(
|
||||
messagesPagination: PaginationParams(
|
||||
lessThan: firstId,
|
||||
limit: 100,
|
||||
limit: messageLimit,
|
||||
),
|
||||
preferOffline: true,
|
||||
)
|
||||
.then((res) {
|
||||
if (res.messages.isEmpty) {
|
||||
if (res.messages.isEmpty || res.messages.length < messageLimit) {
|
||||
_paginationEnded = true;
|
||||
}
|
||||
_queryMessageController.add(false);
|
||||
@@ -84,7 +87,7 @@ class StreamChannelState extends State<StreamChannel> {
|
||||
|
||||
/// Calls [channel.getReplies] updating [queryMessage] stream
|
||||
Future<void> getReplies(String parentId) async {
|
||||
if (_paginationEnded) {
|
||||
if (_queryMessageController.value == true || _paginationEnded) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -99,16 +102,18 @@ class StreamChannelState extends State<StreamChannel> {
|
||||
}
|
||||
}
|
||||
|
||||
final messageLimit = 50;
|
||||
return widget.channel
|
||||
.getReplies(
|
||||
parentId,
|
||||
PaginationParams(
|
||||
lessThan: firstId,
|
||||
limit: 100,
|
||||
limit: messageLimit,
|
||||
),
|
||||
preferOffline: true,
|
||||
)
|
||||
.then((res) {
|
||||
if (res.messages.isEmpty) {
|
||||
if (res.messages.isEmpty || res.messages.length < messageLimit) {
|
||||
_paginationEnded = true;
|
||||
}
|
||||
_queryMessageController.add(false);
|
||||
|
||||
+10
-33
@@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat/stream_chat.dart';
|
||||
import 'package:stream_chat_flutter/src/back_button.dart';
|
||||
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
|
||||
|
||||
/// 
|
||||
@@ -80,7 +81,15 @@ class ThreadHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||
StreamChatTheme.of(context).channelTheme.channelHeaderTheme.color,
|
||||
actions: <Widget>[
|
||||
Container(
|
||||
child: showBackButton ? _buildBackButton(context) : Container(),
|
||||
child: showBackButton
|
||||
? AspectRatio(
|
||||
aspectRatio: 1,
|
||||
child: StreamBackButton(
|
||||
onPressed: onBackPressed,
|
||||
icon: Icons.close,
|
||||
),
|
||||
)
|
||||
: SizedBox(),
|
||||
),
|
||||
],
|
||||
centerTitle: false,
|
||||
@@ -104,38 +113,6 @@ class ThreadHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Padding _buildBackButton(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(14.0),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 1,
|
||||
child: RawMaterialButton(
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)),
|
||||
elevation: 0,
|
||||
highlightElevation: 0,
|
||||
focusElevation: 0,
|
||||
disabledElevation: 0,
|
||||
hoverElevation: 0,
|
||||
onPressed: () {
|
||||
if (onBackPressed != null) {
|
||||
onBackPressed();
|
||||
} else {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
},
|
||||
fillColor: Theme.of(context).brightness == Brightness.dark
|
||||
? Colors.white.withOpacity(.1)
|
||||
: Colors.black.withOpacity(.1),
|
||||
padding: EdgeInsets.all(4),
|
||||
child: Icon(
|
||||
Icons.close,
|
||||
size: 15,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
final Size preferredSize;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat/stream_chat.dart';
|
||||
import 'package:stream_chat_flutter/src/user_avatar.dart';
|
||||
|
||||
class UserReactionDisplay extends StatelessWidget {
|
||||
const UserReactionDisplay({
|
||||
Key key,
|
||||
@required this.reactionToEmoji,
|
||||
@required this.message,
|
||||
this.size = 30,
|
||||
}) : super(key: key);
|
||||
|
||||
final Map<String, String> reactionToEmoji;
|
||||
final Message message;
|
||||
final double size;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
color: Colors.black87,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: reactionToEmoji.keys.map((reactionType) {
|
||||
var firstUserReaction = message.latestReactions.firstWhere(
|
||||
(element) => element.type == reactionType, orElse: () {
|
||||
return null;
|
||||
});
|
||||
|
||||
if (firstUserReaction == null) {
|
||||
return IconButton(
|
||||
iconSize: size,
|
||||
icon: Container(),
|
||||
onPressed: null,
|
||||
);
|
||||
}
|
||||
|
||||
return IconButton(
|
||||
iconSize: size,
|
||||
icon: UserAvatar(
|
||||
user: firstUserReaction.user,
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: size - 5,
|
||||
maxWidth: size - 5,
|
||||
),
|
||||
onTap: (user) {},
|
||||
),
|
||||
onPressed: () {},
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user