Merge branch 'master' into deprecate-setUser

This commit is contained in:
Salvatore Giordano
2021-02-03 17:00:36 +01:00
12 changed files with 274 additions and 231 deletions
+115 -142
View File
@@ -274,28 +274,111 @@ class Channel {
bool enforceUnique = false,
}) async {
final messageId = message.id;
final now = DateTime.now();
final user = _client.state.user;
final latestReactions = [...message.latestReactions ?? <Reaction>[]];
if (enforceUnique) {
latestReactions.removeWhere((it) => it.userId == user.id);
}
final newReaction = Reaction(
messageId: messageId,
createdAt: now,
type: type,
user: user,
score: 1,
extraData: extraData,
);
// Inserting at the 0th index as it's the latest reaction
latestReactions.insert(0, newReaction);
final ownReactions = [...latestReactions]
..removeWhere((it) => it.userId != user.id);
final newMessage = message.copyWith(
reactionCounts: {...message?.reactionCounts ?? <String, int>{}}
..update(type, (value) {
if (enforceUnique) return value;
return value + 1;
}, ifAbsent: () => 1),
reactionScores: {...message.reactionScores ?? <String, int>{}}
..update(type, (value) {
if (enforceUnique) return value;
return value + 1;
}, ifAbsent: () => 1),
latestReactions: latestReactions,
ownReactions: ownReactions,
);
state?.addMessage(newMessage);
final data = Map<String, dynamic>.from(extraData)
..addAll({
'type': type,
});
final res = await _client.post(
'/messages/$messageId/reaction',
data: {
'reaction': data,
'enforce_unique': enforceUnique,
},
);
return _client.decode(res.data, SendReactionResponse.fromJson);
try {
final res = await _client.post(
'/messages/$messageId/reaction',
data: {
'reaction': data,
'enforce_unique': enforceUnique,
},
);
final reactionResp =
_client.decode(res.data, SendReactionResponse.fromJson);
return reactionResp;
} catch (_) {
// Reset the message if the update fails
state?.addMessage(message);
rethrow;
}
}
/// Delete a reaction from this channel
Future<EmptyResponse> deleteReaction(Message message, Reaction reaction) {
_checkInitialized();
Future<EmptyResponse> deleteReaction(
Message message, Reaction reaction) async {
final type = reaction.type;
final user = _client.state.user;
return client
.delete('/messages/${message.id}/reaction/${reaction.type}')
.then((res) => _client.decode(res.data, EmptyResponse.fromJson));
final reactionCounts = {...message.reactionCounts ?? <String, int>{}};
if (reactionCounts.containsKey(type)) {
reactionCounts.update(type, (value) => value - 1);
}
final reactionScores = {...message.reactionScores ?? <String, int>{}};
if (reactionScores.containsKey(type)) {
reactionScores.update(type, (value) => value - 1);
}
final latestReactions = [...message.latestReactions ?? <Reaction>[]]
..removeWhere((r) {
return r.userId == reaction.userId &&
r.type == reaction.type &&
r.messageId == reaction.messageId;
});
final ownReactions = [...latestReactions ?? <Reaction>[]]
..removeWhere((it) => it.userId != user.id);
final newMessage = message.copyWith(
reactionCounts: reactionCounts..removeWhere((_, value) => value == 0),
reactionScores: reactionScores..removeWhere((_, value) => value == 0),
latestReactions: latestReactions,
ownReactions: ownReactions,
);
state?.addMessage(newMessage);
try {
final res = await client
.delete('/messages/${message.id}/reaction/${reaction.type}');
return _client.decode(res.data, EmptyResponse.fromJson);
} catch (_) {
// Reset the message if the update fails
state?.addMessage(message);
rethrow;
}
}
/// Edit the channel custom data
@@ -998,73 +1081,24 @@ class ChannelClientState {
void _listenReactionDeleted() {
_subscriptions.add(_channel.on(EventType.reactionDeleted).listen((event) {
final reaction = event.reaction;
final message = event.message;
_removeMessageReaction(message, reaction);
}));
}
void _removeMessageReaction(Message message, Reaction reaction) {
if (message.parentId == null || message.showInChannel == true) {
_channelState = _channelState.copyWith(
messages: _channelState?.messages?.map((m) {
if (m.id == message.id) {
return _removeReactionFromMessage(m, reaction);
}
return m;
})?.toList(),
final userId = _channel.client.state.user.id;
final message = event.message.copyWith(
ownReactions: [...event.message.latestReactions]
..removeWhere((it) => it.userId != userId),
);
}
if (message.parentId != null) {
final newThreads = threads;
if (newThreads.containsKey(message.parentId)) {
newThreads[message.parentId] = newThreads[message.parentId].map((m) {
if (m.id == message.id) {
return _removeReactionFromMessage(m, reaction);
}
return m;
}).toList();
_threads = newThreads;
}
}
addMessage(message);
}));
}
void _listenReactions() {
_subscriptions.add(_channel
.on(
EventType.reactionNew,
)
.listen((event) {
final message = event.message;
_addMessageReaction(message, event.reaction);
}));
}
void _addMessageReaction(Message message, Reaction reaction) {
if (message.parentId == null || message.showInChannel == true) {
_channelState = _channelState.copyWith(
messages: _channelState.messages.map((m) {
if (message.id == m.id) {
return _addReactionToMessage(m, reaction);
}
return m;
}).toList(),
_subscriptions.add(_channel.on(EventType.reactionNew).listen((event) {
final userId = _channel.client.state.user.id;
final message = event.message.copyWith(
ownReactions: [...event.message.latestReactions]
..removeWhere((it) => it.userId != userId),
);
}
if (message.parentId != null) {
final newThreads = threads;
if (newThreads.containsKey(message.parentId)) {
newThreads[message.parentId] = newThreads[message.parentId].map((m) {
if (message.id == m.id) {
return _addReactionToMessage(m, reaction);
}
return m;
}).toList();
_threads = newThreads;
}
}
addMessage(message);
}));
}
void _listenMessageUpdated() {
@@ -1074,13 +1108,12 @@ class ChannelClientState {
EventType.reactionUpdated,
)
.listen((event) {
final message = event.message;
addMessage(message.copyWith(
ownReactions: message.latestReactions
.where(
(element) => element.user?.id == _channel._client.state.user.id)
.toList(),
));
final userId = _channel.client.state.user.id;
final message = event.message.copyWith(
ownReactions: [...event.message.latestReactions]
..removeWhere((it) => it.userId != userId),
);
addMessage(message);
}));
}
@@ -1163,66 +1196,6 @@ class ChannelClientState {
}));
}
Message _addReactionToMessage(Message message, Reaction reaction) {
final newMessage = message.copyWith(
latestReactions: message.latestReactions..add(reaction),
reactionCounts: {
...message.reactionCounts ?? {},
reaction.type: (message.reactionCounts == null
? 0
: message.reactionCounts[reaction.type] ?? 0) +
1,
},
reactionScores: {
...message.reactionScores ?? {},
reaction.type: (message.reactionScores == null
? 0
: message.reactionScores[reaction.type] ?? 0) +
reaction.score,
},
);
if (reaction.user.id == _channel.client.state.user.id) {
return newMessage.copyWith(
ownReactions: message.ownReactions..add(reaction),
);
}
return newMessage;
}
Message _removeReactionFromMessage(Message message, Reaction reaction) {
final newMessage = message.copyWith(
latestReactions: message.latestReactions
..removeWhere(
(r) => r.type == reaction.type && r.userId == reaction.userId),
reactionCounts: {
...message.reactionCounts,
reaction.type: (message.reactionCounts[reaction.type] ?? 0) - 1,
},
reactionScores: {
...message.reactionScores ?? {},
reaction.type: max(
(message.reactionScores == null
? 0
: message.reactionScores[reaction.type] ?? 0) -
reaction.score,
0),
},
);
newMessage.reactionCounts.removeWhere((_, v) => v <= 0);
if (reaction.user.id == _channel.client.state.user.id) {
return newMessage.copyWith(
ownReactions: message.ownReactions
..removeWhere((r) => r.type == reaction.type),
);
}
return newMessage;
}
/// Channel message list
List<Message> get messages => _channelState.messages;
@@ -49,10 +49,10 @@ class Reaction {
this.createdAt,
this.type,
this.user,
this.userId,
String userId,
this.score,
this.extraData,
});
}) : userId = userId ?? user?.id;
/// Create a new instance from a json
factory Reaction.fromJson(Map<String, dynamic> json) {
@@ -65,4 +65,40 @@ class Reaction {
return Serialization.moveFromExtraDataToRoot(
_$ReactionToJson(this), topLevelFields);
}
/// Creates a copy of [Reaction] with specified attributes overridden.
Reaction copyWith({
String messageId,
DateTime createdAt,
String type,
User user,
String userId,
int score,
Map<String, dynamic> extraData,
}) {
return Reaction(
messageId: messageId ?? this.messageId,
createdAt: createdAt ?? this.createdAt,
type: type ?? this.type,
user: user ?? this.user,
userId: userId ?? this.userId,
score: score ?? this.score,
extraData: extraData ?? this.extraData,
);
}
/// Returns a new [Reaction] that is a combination of this reaction and the given
/// [other] reaction.
Reaction merge(Reaction other) {
if (other == null) return this;
return copyWith(
messageId: other.messageId,
createdAt: other.createdAt,
type: other.type,
user: other.user,
userId: other.userId,
score: other.score,
extraData: other.extraData,
);
}
}
@@ -7,6 +7,7 @@ import 'package:stream_chat/src/event_type.dart';
import 'package:stream_chat/src/models/event.dart';
import 'package:stream_chat/src/models/message.dart';
import 'package:stream_chat/src/models/reaction.dart';
import 'package:stream_chat/src/models/own_user.dart';
import 'package:test/test.dart';
class MockDio extends Mock implements DioForNative {}
@@ -379,7 +380,8 @@ void main() {
'api-key',
httpClient: mockDio,
tokenProvider: (_) async => '',
);
)..state.user = OwnUser(id: 'test-id');
final channelClient = client.channel('messaging', id: 'testid');
final reactionType = 'test';
@@ -396,6 +398,10 @@ void main() {
await channelClient.sendReaction(
Message(
id: 'messageid',
reactionCounts: const <String, int>{},
reactionScores: const <String, int>{},
latestReactions: const <Reaction>[],
ownReactions: const <Reaction>[],
),
reactionType,
);
@@ -418,23 +424,21 @@ void main() {
'api-key',
httpClient: mockDio,
tokenProvider: (_) async => '',
);
final channelClient = client.channel('messaging', id: 'testid');
)..state.user = OwnUser(id: 'test-id');
when(mockDio.post<String>(
any,
data: anyNamed('data'),
)).thenAnswer((_) async => Response(
data: '{}',
statusCode: 200,
));
await channelClient.watch();
final channelClient = client.channel('messaging', id: 'testid');
when(mockDio.delete<String>('/messages/messageid/reaction/test'))
.thenAnswer((_) async => Response(data: '{}', statusCode: 200));
await channelClient.deleteReaction(
Message(id: 'messageid'),
Message(
id: 'messageid',
reactionCounts: const <String, int>{},
reactionScores: const <String, int>{},
latestReactions: const <Reaction>[],
ownReactions: const <Reaction>[],
),
Reaction(type: 'test'),
);
@@ -33,6 +33,8 @@ class ChannelMediaDisplayScreen extends StatefulWidget {
}
class _ChannelMediaDisplayScreenState extends State<ChannelMediaDisplayScreen> {
Map<String, VideoPlayerController> controllerCache = {};
@override
void initState() {
super.initState();
@@ -147,8 +149,15 @@ class _ChannelMediaDisplayScreenState extends State<ChannelMediaDisplayScreen> {
.forEach((e) {
VideoPlayerController controller;
if (e.type == 'video') {
controller = VideoPlayerController.network(e.assetUrl);
controller.initialize();
var cachedController = controllerCache[e.assetUrl];
if (cachedController == null) {
controller = VideoPlayerController.network(e.assetUrl);
controller.initialize();
controllerCache[e.assetUrl] = controller;
} else {
controller = cachedController;
}
}
media.add(_AssetPackage(e, item.message, controller));
});
@@ -221,6 +230,14 @@ class _ChannelMediaDisplayScreenState extends State<ChannelMediaDisplayScreen> {
stream: messageSearchBloc.messagesStream,
);
}
@override
void dispose() {
super.dispose();
for (var c in controllerCache.values) {
c.dispose();
}
}
}
class _AssetPackage {
@@ -61,9 +61,14 @@ class _FullScreenMediaState extends State<FullScreenMedia>
.where((element) => element.type == 'video')
.toList()
.forEach((element) {
videoPackages.add(VideoPackage(context, element, () {
setState(() {});
}));
videoPackages.add(VideoPackage(
context,
element,
() {
setState(() {});
},
showControls: true,
));
});
}
@@ -233,15 +238,18 @@ class VideoPackage {
bool initialised = false;
VoidCallback onInit;
BuildContext context;
bool showControls;
///
VideoPackage(this.context, Attachment attachment, this.onInit) {
VideoPackage(this.context, Attachment attachment, this.onInit,
{this.showControls = false}) {
_videoPlayerController = VideoPlayerController.network(attachment.assetUrl);
_videoPlayerController.initialize().whenComplete(() {
initialised = true;
_chewieController = ChewieController(
videoPlayerController: _videoPlayerController,
autoInitialize: false,
autoInitialize: true,
showControls: showControls,
aspectRatio: _videoPlayerController.value.aspectRatio,
);
onInit();
@@ -4,10 +4,10 @@ import 'dart:ui';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:stream_chat_flutter/src/reaction_picker.dart';
import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
import 'package:stream_chat_flutter/src/utils.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
import 'extension.dart';
import 'message_input.dart';
@@ -33,6 +33,7 @@ class MessageActionsModal extends StatefulWidget {
final ShapeBorder messageShape;
final ShapeBorder attachmentShape;
final DisplayWidget showUserAvatar;
final Map<String, VideoPackage> videoPackages;
const MessageActionsModal({
Key key,
@@ -53,6 +54,7 @@ class MessageActionsModal extends StatefulWidget {
this.messageShape,
this.attachmentShape,
this.reverse = false,
this.videoPackages,
}) : super(key: key);
@override
@@ -180,6 +182,7 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
showSendingIndicator: false,
shape: widget.messageShape,
attachmentShape: widget.attachmentShape,
videoPackages: widget.videoPackages,
),
),
SizedBox(height: 8),
@@ -239,6 +239,8 @@ class _MessageListViewState extends State<MessageListView> {
final MessageListController _messageListController = MessageListController();
final Map<String, VideoPackage> videoPackages = {};
@override
Widget build(BuildContext context) {
return MessageListCore(
@@ -776,6 +778,7 @@ class _MessageListViewState extends State<MessageListView> {
break;
}
},
videoPackages: videoPackages,
);
}
@@ -936,6 +939,7 @@ class _MessageListViewState extends State<MessageListView> {
break;
}
},
videoPackages: videoPackages,
);
if (!message.isDeleted && !message.isSystem && !message.isEphemeral) {
@@ -1052,6 +1056,7 @@ class _MessageListViewState extends State<MessageListView> {
streamChannel.reloadChannel();
}
_messageNewListener?.cancel();
videoPackages.values.forEach((e) => e.dispose());
super.dispose();
}
}
@@ -1,6 +1,7 @@
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
import 'package:stream_chat_flutter/src/reaction_bubble.dart';
import 'package:stream_chat_flutter/src/reaction_picker.dart';
@@ -22,6 +23,7 @@ class MessageReactionsModal extends StatelessWidget {
final ShapeBorder messageShape;
final ShapeBorder attachmentShape;
final void Function(User) onUserAvatarTap;
final Map<String, VideoPackage> videoPackages;
const MessageReactionsModal({
Key key,
@@ -35,6 +37,7 @@ class MessageReactionsModal extends StatelessWidget {
this.reverse = false,
this.showUserAvatar = DisplayWidget.show,
this.onUserAvatarTap,
this.videoPackages,
}) : super(key: key);
@override
@@ -143,6 +146,7 @@ class MessageReactionsModal extends StatelessWidget {
(message.status ==
MessageSendingStatus.sent ||
message.status == null),
videoPackages: videoPackages,
),
),
if (message.latestReactions?.isNotEmpty == true) ...[
@@ -257,7 +261,7 @@ class MessageReactionsModal extends StatelessWidget {
),
const SizedBox(height: 8),
Text(
reaction.user.name,
reaction.user.name.split(' ')[0],
style: StreamChatTheme.of(context).textTheme.footnoteBold,
textAlign: TextAlign.center,
),
@@ -142,6 +142,9 @@ class MessageWidget extends StatefulWidget {
/// Function called when quotedMessage is tapped
final OnQuotedMessageTap onQuotedMessageTap;
/// The cache for the video controllers of attachments IDed as message ID + attachment index
final Map<String, VideoPackage> videoPackages;
///
MessageWidget({
Key key,
@@ -190,6 +193,7 @@ class MessageWidget extends StatefulWidget {
this.attachmentPadding = EdgeInsets.zero,
this.allRead = false,
this.onQuotedMessageTap,
this.videoPackages,
}) : attachmentBuilders = {
'image': (context, message, attachment) {
return ImageAttachment(
@@ -740,6 +744,7 @@ class _MessageWidgetState extends State<MessageWidget> {
!isFailedState &&
widget.onThreadTap != null,
showFlagButton: widget.showFlagButton,
videoPackages: widget.videoPackages,
),
);
});
@@ -768,6 +773,7 @@ class _MessageWidgetState extends State<MessageWidget> {
editMessageInputBuilder: widget.editMessageInputBuilder,
onThreadTap: widget.onThreadTap,
showReactions: widget.showReactions,
videoPackages: widget.videoPackages,
),
);
});
@@ -832,6 +838,41 @@ class _MessageWidgetState extends State<MessageWidget> {
children: widget.message.attachments
?.where((element) => element.ogScrapeUrl == null)
?.map((attachment) {
if (attachment.type == 'video') {
VideoPackage package;
if (widget.videoPackages == null) {
package = VideoPackage(context, attachment, () {});
} else {
package = widget?.videoPackages[
'${widget.message.id}${widget.message.attachments.indexOf(attachment)}'] ??
VideoPackage(context, attachment, () {});
}
if (widget.videoPackages != null) {
widget.videoPackages[
'${widget.message.id}${widget.message.attachments.indexOf(attachment)}'] =
package;
}
return Transform(
transform: Matrix4.rotationY(widget.reverse ? pi : 0),
alignment: Alignment.center,
child: VideoAttachment(
attachment: attachment,
messageTheme: widget.messageTheme,
size: Size(
MediaQuery.of(context).size.width * 0.8,
MediaQuery.of(context).size.height * 0.3,
),
message: widget.message,
onShowMessage: widget.onShowMessage,
onReturnAction: widget.onReturnAction,
videoPackage: package,
),
);
}
final attachmentBuilder =
widget.attachmentBuilders[attachment.type];
@@ -1,12 +1,8 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:chewie/chewie.dart';
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/full_screen_media.dart';
import 'package:stream_chat_flutter/src/utils.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:video_player/video_player.dart';
import 'attachment_error.dart';
import 'attachment_title.dart';
class VideoAttachment extends StatefulWidget {
@@ -16,11 +12,13 @@ class VideoAttachment extends StatefulWidget {
final Message message;
final ShowMessageCallback onShowMessage;
final ValueChanged<ReturnActionType> onReturnAction;
final VideoPackage videoPackage;
VideoAttachment({
Key key,
@required this.attachment,
@required this.messageTheme,
this.videoPackage,
this.message,
this.size,
this.onShowMessage,
@@ -32,13 +30,21 @@ class VideoAttachment extends StatefulWidget {
}
class _VideoAttachmentState extends State<VideoAttachment> {
ChewieController _chewieController;
VideoPlayerController _videoPlayerController;
bool initialized = false;
@override
void initState() {
super.initState();
widget.videoPackage.onInit = () {
setState(() {
initialized = true;
});
};
}
@override
Widget build(BuildContext context) {
if (!initialized) {
if (!widget.videoPackage.initialised) {
return Container(
height: widget.size?.height ?? 100,
width: widget.size?.width ?? 100,
@@ -47,43 +53,6 @@ class _VideoAttachmentState extends State<VideoAttachment> {
),
);
}
_chewieController = ChewieController(
videoPlayerController: _videoPlayerController,
autoInitialize: true,
showControls: false,
aspectRatio: _videoPlayerController.value.aspectRatio,
errorBuilder: (_, e) {
if (widget.attachment.thumbUrl != null) {
return Stack(
children: <Widget>[
Container(
height: widget.size?.height,
width: widget.size?.width,
decoration: BoxDecoration(
image: DecorationImage(
fit: BoxFit.cover,
image: CachedNetworkImageProvider(
widget.attachment.thumbUrl,
),
),
),
),
if (widget.attachment.titleLink != null)
Material(
color: Colors.transparent,
child: InkWell(
onTap: () =>
launchURL(context, widget.attachment.titleLink),
),
),
],
);
}
return AttachmentError(
attachment: widget.attachment,
size: widget.size,
);
});
return GestureDetector(
onTap: () async {
@@ -123,7 +92,7 @@ class _VideoAttachmentState extends State<VideoAttachment> {
child: Stack(
children: <Widget>[
Chewie(
controller: _chewieController,
controller: widget.videoPackage.chewieController,
),
Positioned.fill(
child: Center(
@@ -153,23 +122,4 @@ class _VideoAttachmentState extends State<VideoAttachment> {
),
);
}
@override
void initState() {
super.initState();
_videoPlayerController =
VideoPlayerController.network(widget.attachment.assetUrl);
_videoPlayerController.initialize().whenComplete(() {
setState(() {
initialized = true;
});
});
}
@override
void dispose() {
_videoPlayerController?.dispose();
_chewieController?.dispose();
super.dispose();
}
}
@@ -96,7 +96,7 @@ void main() {
findsNWidgets(2));
expect(find.byKey(Key('StreamSvgIcon-Icon_love_reaction.svg')),
findsNWidgets(2));
expect(find.text(testUserId), findsNWidgets(2));
expect(find.text(testUserId.split(' ')[0]), findsNWidgets(2));
},
);
}
@@ -86,7 +86,7 @@ void main() {
testWidgets(
'StreamChatCore should disconnect on background',
(WidgetTester tester) async {
await fakeAsync((_async) {
fakeAsync((_async) {
final client = MockClient();
final clientState = MockClientState();
final channel = MockChannel();
@@ -123,6 +123,7 @@ void main() {
_async.elapse(Duration(seconds: 5));
verify(client.disconnect()).called(1);
eventStreamController.close();
});
},
);
@@ -174,6 +175,7 @@ void main() {
await untilCalled(showLocalNotificationMock(event));
verify(showLocalNotificationMock(event)).called(1);
eventStreamController.close();
},
);
}