Merge branch 'master' into deprecate-setUser
This commit is contained in:
@@ -274,28 +274,111 @@ class Channel {
|
|||||||
bool enforceUnique = false,
|
bool enforceUnique = false,
|
||||||
}) async {
|
}) async {
|
||||||
final messageId = message.id;
|
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)
|
final data = Map<String, dynamic>.from(extraData)
|
||||||
..addAll({
|
..addAll({
|
||||||
'type': type,
|
'type': type,
|
||||||
});
|
});
|
||||||
|
|
||||||
final res = await _client.post(
|
try {
|
||||||
'/messages/$messageId/reaction',
|
final res = await _client.post(
|
||||||
data: {
|
'/messages/$messageId/reaction',
|
||||||
'reaction': data,
|
data: {
|
||||||
'enforce_unique': enforceUnique,
|
'reaction': data,
|
||||||
},
|
'enforce_unique': enforceUnique,
|
||||||
);
|
},
|
||||||
return _client.decode(res.data, SendReactionResponse.fromJson);
|
);
|
||||||
|
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
|
/// Delete a reaction from this channel
|
||||||
Future<EmptyResponse> deleteReaction(Message message, Reaction reaction) {
|
Future<EmptyResponse> deleteReaction(
|
||||||
_checkInitialized();
|
Message message, Reaction reaction) async {
|
||||||
|
final type = reaction.type;
|
||||||
|
final user = _client.state.user;
|
||||||
|
|
||||||
return client
|
final reactionCounts = {...message.reactionCounts ?? <String, int>{}};
|
||||||
.delete('/messages/${message.id}/reaction/${reaction.type}')
|
if (reactionCounts.containsKey(type)) {
|
||||||
.then((res) => _client.decode(res.data, EmptyResponse.fromJson));
|
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
|
/// Edit the channel custom data
|
||||||
@@ -998,73 +1081,24 @@ class ChannelClientState {
|
|||||||
|
|
||||||
void _listenReactionDeleted() {
|
void _listenReactionDeleted() {
|
||||||
_subscriptions.add(_channel.on(EventType.reactionDeleted).listen((event) {
|
_subscriptions.add(_channel.on(EventType.reactionDeleted).listen((event) {
|
||||||
final reaction = event.reaction;
|
final userId = _channel.client.state.user.id;
|
||||||
final message = event.message;
|
final message = event.message.copyWith(
|
||||||
_removeMessageReaction(message, reaction);
|
ownReactions: [...event.message.latestReactions]
|
||||||
}));
|
..removeWhere((it) => it.userId != userId),
|
||||||
}
|
|
||||||
|
|
||||||
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(),
|
|
||||||
);
|
);
|
||||||
}
|
addMessage(message);
|
||||||
|
}));
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _listenReactions() {
|
void _listenReactions() {
|
||||||
_subscriptions.add(_channel
|
_subscriptions.add(_channel.on(EventType.reactionNew).listen((event) {
|
||||||
.on(
|
final userId = _channel.client.state.user.id;
|
||||||
EventType.reactionNew,
|
final message = event.message.copyWith(
|
||||||
)
|
ownReactions: [...event.message.latestReactions]
|
||||||
.listen((event) {
|
..removeWhere((it) => it.userId != userId),
|
||||||
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(),
|
|
||||||
);
|
);
|
||||||
}
|
addMessage(message);
|
||||||
|
}));
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _listenMessageUpdated() {
|
void _listenMessageUpdated() {
|
||||||
@@ -1074,13 +1108,12 @@ class ChannelClientState {
|
|||||||
EventType.reactionUpdated,
|
EventType.reactionUpdated,
|
||||||
)
|
)
|
||||||
.listen((event) {
|
.listen((event) {
|
||||||
final message = event.message;
|
final userId = _channel.client.state.user.id;
|
||||||
addMessage(message.copyWith(
|
final message = event.message.copyWith(
|
||||||
ownReactions: message.latestReactions
|
ownReactions: [...event.message.latestReactions]
|
||||||
.where(
|
..removeWhere((it) => it.userId != userId),
|
||||||
(element) => element.user?.id == _channel._client.state.user.id)
|
);
|
||||||
.toList(),
|
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
|
/// Channel message list
|
||||||
List<Message> get messages => _channelState.messages;
|
List<Message> get messages => _channelState.messages;
|
||||||
|
|
||||||
|
|||||||
@@ -49,10 +49,10 @@ class Reaction {
|
|||||||
this.createdAt,
|
this.createdAt,
|
||||||
this.type,
|
this.type,
|
||||||
this.user,
|
this.user,
|
||||||
this.userId,
|
String userId,
|
||||||
this.score,
|
this.score,
|
||||||
this.extraData,
|
this.extraData,
|
||||||
});
|
}) : userId = userId ?? user?.id;
|
||||||
|
|
||||||
/// Create a new instance from a json
|
/// Create a new instance from a json
|
||||||
factory Reaction.fromJson(Map<String, dynamic> json) {
|
factory Reaction.fromJson(Map<String, dynamic> json) {
|
||||||
@@ -65,4 +65,40 @@ class Reaction {
|
|||||||
return Serialization.moveFromExtraDataToRoot(
|
return Serialization.moveFromExtraDataToRoot(
|
||||||
_$ReactionToJson(this), topLevelFields);
|
_$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/event.dart';
|
||||||
import 'package:stream_chat/src/models/message.dart';
|
import 'package:stream_chat/src/models/message.dart';
|
||||||
import 'package:stream_chat/src/models/reaction.dart';
|
import 'package:stream_chat/src/models/reaction.dart';
|
||||||
|
import 'package:stream_chat/src/models/own_user.dart';
|
||||||
import 'package:test/test.dart';
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
class MockDio extends Mock implements DioForNative {}
|
class MockDio extends Mock implements DioForNative {}
|
||||||
@@ -379,7 +380,8 @@ void main() {
|
|||||||
'api-key',
|
'api-key',
|
||||||
httpClient: mockDio,
|
httpClient: mockDio,
|
||||||
tokenProvider: (_) async => '',
|
tokenProvider: (_) async => '',
|
||||||
);
|
)..state.user = OwnUser(id: 'test-id');
|
||||||
|
|
||||||
final channelClient = client.channel('messaging', id: 'testid');
|
final channelClient = client.channel('messaging', id: 'testid');
|
||||||
final reactionType = 'test';
|
final reactionType = 'test';
|
||||||
|
|
||||||
@@ -396,6 +398,10 @@ void main() {
|
|||||||
await channelClient.sendReaction(
|
await channelClient.sendReaction(
|
||||||
Message(
|
Message(
|
||||||
id: 'messageid',
|
id: 'messageid',
|
||||||
|
reactionCounts: const <String, int>{},
|
||||||
|
reactionScores: const <String, int>{},
|
||||||
|
latestReactions: const <Reaction>[],
|
||||||
|
ownReactions: const <Reaction>[],
|
||||||
),
|
),
|
||||||
reactionType,
|
reactionType,
|
||||||
);
|
);
|
||||||
@@ -418,23 +424,21 @@ void main() {
|
|||||||
'api-key',
|
'api-key',
|
||||||
httpClient: mockDio,
|
httpClient: mockDio,
|
||||||
tokenProvider: (_) async => '',
|
tokenProvider: (_) async => '',
|
||||||
);
|
)..state.user = OwnUser(id: 'test-id');
|
||||||
final channelClient = client.channel('messaging', id: 'testid');
|
|
||||||
|
|
||||||
when(mockDio.post<String>(
|
final channelClient = client.channel('messaging', id: 'testid');
|
||||||
any,
|
|
||||||
data: anyNamed('data'),
|
|
||||||
)).thenAnswer((_) async => Response(
|
|
||||||
data: '{}',
|
|
||||||
statusCode: 200,
|
|
||||||
));
|
|
||||||
await channelClient.watch();
|
|
||||||
|
|
||||||
when(mockDio.delete<String>('/messages/messageid/reaction/test'))
|
when(mockDio.delete<String>('/messages/messageid/reaction/test'))
|
||||||
.thenAnswer((_) async => Response(data: '{}', statusCode: 200));
|
.thenAnswer((_) async => Response(data: '{}', statusCode: 200));
|
||||||
|
|
||||||
await channelClient.deleteReaction(
|
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'),
|
Reaction(type: 'test'),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ class ChannelMediaDisplayScreen extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _ChannelMediaDisplayScreenState extends State<ChannelMediaDisplayScreen> {
|
class _ChannelMediaDisplayScreenState extends State<ChannelMediaDisplayScreen> {
|
||||||
|
Map<String, VideoPlayerController> controllerCache = {};
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@@ -147,8 +149,15 @@ class _ChannelMediaDisplayScreenState extends State<ChannelMediaDisplayScreen> {
|
|||||||
.forEach((e) {
|
.forEach((e) {
|
||||||
VideoPlayerController controller;
|
VideoPlayerController controller;
|
||||||
if (e.type == 'video') {
|
if (e.type == 'video') {
|
||||||
controller = VideoPlayerController.network(e.assetUrl);
|
var cachedController = controllerCache[e.assetUrl];
|
||||||
controller.initialize();
|
|
||||||
|
if (cachedController == null) {
|
||||||
|
controller = VideoPlayerController.network(e.assetUrl);
|
||||||
|
controller.initialize();
|
||||||
|
controllerCache[e.assetUrl] = controller;
|
||||||
|
} else {
|
||||||
|
controller = cachedController;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
media.add(_AssetPackage(e, item.message, controller));
|
media.add(_AssetPackage(e, item.message, controller));
|
||||||
});
|
});
|
||||||
@@ -221,6 +230,14 @@ class _ChannelMediaDisplayScreenState extends State<ChannelMediaDisplayScreen> {
|
|||||||
stream: messageSearchBloc.messagesStream,
|
stream: messageSearchBloc.messagesStream,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
super.dispose();
|
||||||
|
for (var c in controllerCache.values) {
|
||||||
|
c.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _AssetPackage {
|
class _AssetPackage {
|
||||||
|
|||||||
@@ -61,9 +61,14 @@ class _FullScreenMediaState extends State<FullScreenMedia>
|
|||||||
.where((element) => element.type == 'video')
|
.where((element) => element.type == 'video')
|
||||||
.toList()
|
.toList()
|
||||||
.forEach((element) {
|
.forEach((element) {
|
||||||
videoPackages.add(VideoPackage(context, element, () {
|
videoPackages.add(VideoPackage(
|
||||||
setState(() {});
|
context,
|
||||||
}));
|
element,
|
||||||
|
() {
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
|
showControls: true,
|
||||||
|
));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -233,15 +238,18 @@ class VideoPackage {
|
|||||||
bool initialised = false;
|
bool initialised = false;
|
||||||
VoidCallback onInit;
|
VoidCallback onInit;
|
||||||
BuildContext context;
|
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 = VideoPlayerController.network(attachment.assetUrl);
|
||||||
_videoPlayerController.initialize().whenComplete(() {
|
_videoPlayerController.initialize().whenComplete(() {
|
||||||
initialised = true;
|
initialised = true;
|
||||||
_chewieController = ChewieController(
|
_chewieController = ChewieController(
|
||||||
videoPlayerController: _videoPlayerController,
|
videoPlayerController: _videoPlayerController,
|
||||||
autoInitialize: false,
|
autoInitialize: true,
|
||||||
|
showControls: showControls,
|
||||||
aspectRatio: _videoPlayerController.value.aspectRatio,
|
aspectRatio: _videoPlayerController.value.aspectRatio,
|
||||||
);
|
);
|
||||||
onInit();
|
onInit();
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ import 'dart:ui';
|
|||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.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/reaction_picker.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/utils.dart';
|
import 'package:stream_chat_flutter/src/utils.dart';
|
||||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
|
||||||
|
|
||||||
import 'extension.dart';
|
import 'extension.dart';
|
||||||
import 'message_input.dart';
|
import 'message_input.dart';
|
||||||
@@ -33,6 +33,7 @@ class MessageActionsModal extends StatefulWidget {
|
|||||||
final ShapeBorder messageShape;
|
final ShapeBorder messageShape;
|
||||||
final ShapeBorder attachmentShape;
|
final ShapeBorder attachmentShape;
|
||||||
final DisplayWidget showUserAvatar;
|
final DisplayWidget showUserAvatar;
|
||||||
|
final Map<String, VideoPackage> videoPackages;
|
||||||
|
|
||||||
const MessageActionsModal({
|
const MessageActionsModal({
|
||||||
Key key,
|
Key key,
|
||||||
@@ -53,6 +54,7 @@ class MessageActionsModal extends StatefulWidget {
|
|||||||
this.messageShape,
|
this.messageShape,
|
||||||
this.attachmentShape,
|
this.attachmentShape,
|
||||||
this.reverse = false,
|
this.reverse = false,
|
||||||
|
this.videoPackages,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -180,6 +182,7 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
|
|||||||
showSendingIndicator: false,
|
showSendingIndicator: false,
|
||||||
shape: widget.messageShape,
|
shape: widget.messageShape,
|
||||||
attachmentShape: widget.attachmentShape,
|
attachmentShape: widget.attachmentShape,
|
||||||
|
videoPackages: widget.videoPackages,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SizedBox(height: 8),
|
SizedBox(height: 8),
|
||||||
|
|||||||
@@ -239,6 +239,8 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
|
|
||||||
final MessageListController _messageListController = MessageListController();
|
final MessageListController _messageListController = MessageListController();
|
||||||
|
|
||||||
|
final Map<String, VideoPackage> videoPackages = {};
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return MessageListCore(
|
return MessageListCore(
|
||||||
@@ -776,6 +778,7 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
videoPackages: videoPackages,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -936,6 +939,7 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
videoPackages: videoPackages,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!message.isDeleted && !message.isSystem && !message.isEphemeral) {
|
if (!message.isDeleted && !message.isSystem && !message.isEphemeral) {
|
||||||
@@ -1052,6 +1056,7 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
streamChannel.reloadChannel();
|
streamChannel.reloadChannel();
|
||||||
}
|
}
|
||||||
_messageNewListener?.cancel();
|
_messageNewListener?.cancel();
|
||||||
|
videoPackages.values.forEach((e) => e.dispose());
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
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_core/stream_chat_flutter_core.dart';
|
||||||
import 'package:stream_chat_flutter/src/reaction_bubble.dart';
|
import 'package:stream_chat_flutter/src/reaction_bubble.dart';
|
||||||
import 'package:stream_chat_flutter/src/reaction_picker.dart';
|
import 'package:stream_chat_flutter/src/reaction_picker.dart';
|
||||||
@@ -22,6 +23,7 @@ class MessageReactionsModal extends StatelessWidget {
|
|||||||
final ShapeBorder messageShape;
|
final ShapeBorder messageShape;
|
||||||
final ShapeBorder attachmentShape;
|
final ShapeBorder attachmentShape;
|
||||||
final void Function(User) onUserAvatarTap;
|
final void Function(User) onUserAvatarTap;
|
||||||
|
final Map<String, VideoPackage> videoPackages;
|
||||||
|
|
||||||
const MessageReactionsModal({
|
const MessageReactionsModal({
|
||||||
Key key,
|
Key key,
|
||||||
@@ -35,6 +37,7 @@ class MessageReactionsModal extends StatelessWidget {
|
|||||||
this.reverse = false,
|
this.reverse = false,
|
||||||
this.showUserAvatar = DisplayWidget.show,
|
this.showUserAvatar = DisplayWidget.show,
|
||||||
this.onUserAvatarTap,
|
this.onUserAvatarTap,
|
||||||
|
this.videoPackages,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -143,6 +146,7 @@ class MessageReactionsModal extends StatelessWidget {
|
|||||||
(message.status ==
|
(message.status ==
|
||||||
MessageSendingStatus.sent ||
|
MessageSendingStatus.sent ||
|
||||||
message.status == null),
|
message.status == null),
|
||||||
|
videoPackages: videoPackages,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (message.latestReactions?.isNotEmpty == true) ...[
|
if (message.latestReactions?.isNotEmpty == true) ...[
|
||||||
@@ -257,7 +261,7 @@ class MessageReactionsModal extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
reaction.user.name,
|
reaction.user.name.split(' ')[0],
|
||||||
style: StreamChatTheme.of(context).textTheme.footnoteBold,
|
style: StreamChatTheme.of(context).textTheme.footnoteBold,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -142,6 +142,9 @@ class MessageWidget extends StatefulWidget {
|
|||||||
/// Function called when quotedMessage is tapped
|
/// Function called when quotedMessage is tapped
|
||||||
final OnQuotedMessageTap onQuotedMessageTap;
|
final OnQuotedMessageTap onQuotedMessageTap;
|
||||||
|
|
||||||
|
/// The cache for the video controllers of attachments IDed as message ID + attachment index
|
||||||
|
final Map<String, VideoPackage> videoPackages;
|
||||||
|
|
||||||
///
|
///
|
||||||
MessageWidget({
|
MessageWidget({
|
||||||
Key key,
|
Key key,
|
||||||
@@ -190,6 +193,7 @@ class MessageWidget extends StatefulWidget {
|
|||||||
this.attachmentPadding = EdgeInsets.zero,
|
this.attachmentPadding = EdgeInsets.zero,
|
||||||
this.allRead = false,
|
this.allRead = false,
|
||||||
this.onQuotedMessageTap,
|
this.onQuotedMessageTap,
|
||||||
|
this.videoPackages,
|
||||||
}) : attachmentBuilders = {
|
}) : attachmentBuilders = {
|
||||||
'image': (context, message, attachment) {
|
'image': (context, message, attachment) {
|
||||||
return ImageAttachment(
|
return ImageAttachment(
|
||||||
@@ -740,6 +744,7 @@ class _MessageWidgetState extends State<MessageWidget> {
|
|||||||
!isFailedState &&
|
!isFailedState &&
|
||||||
widget.onThreadTap != null,
|
widget.onThreadTap != null,
|
||||||
showFlagButton: widget.showFlagButton,
|
showFlagButton: widget.showFlagButton,
|
||||||
|
videoPackages: widget.videoPackages,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -768,6 +773,7 @@ class _MessageWidgetState extends State<MessageWidget> {
|
|||||||
editMessageInputBuilder: widget.editMessageInputBuilder,
|
editMessageInputBuilder: widget.editMessageInputBuilder,
|
||||||
onThreadTap: widget.onThreadTap,
|
onThreadTap: widget.onThreadTap,
|
||||||
showReactions: widget.showReactions,
|
showReactions: widget.showReactions,
|
||||||
|
videoPackages: widget.videoPackages,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -832,6 +838,41 @@ class _MessageWidgetState extends State<MessageWidget> {
|
|||||||
children: widget.message.attachments
|
children: widget.message.attachments
|
||||||
?.where((element) => element.ogScrapeUrl == null)
|
?.where((element) => element.ogScrapeUrl == null)
|
||||||
?.map((attachment) {
|
?.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 =
|
final attachmentBuilder =
|
||||||
widget.attachmentBuilders[attachment.type];
|
widget.attachmentBuilders[attachment.type];
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,8 @@
|
|||||||
import 'package:cached_network_image/cached_network_image.dart';
|
|
||||||
import 'package:chewie/chewie.dart';
|
import 'package:chewie/chewie.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:stream_chat_flutter/src/full_screen_media.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:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
import 'package:video_player/video_player.dart';
|
|
||||||
|
|
||||||
import 'attachment_error.dart';
|
|
||||||
import 'attachment_title.dart';
|
import 'attachment_title.dart';
|
||||||
|
|
||||||
class VideoAttachment extends StatefulWidget {
|
class VideoAttachment extends StatefulWidget {
|
||||||
@@ -16,11 +12,13 @@ class VideoAttachment extends StatefulWidget {
|
|||||||
final Message message;
|
final Message message;
|
||||||
final ShowMessageCallback onShowMessage;
|
final ShowMessageCallback onShowMessage;
|
||||||
final ValueChanged<ReturnActionType> onReturnAction;
|
final ValueChanged<ReturnActionType> onReturnAction;
|
||||||
|
final VideoPackage videoPackage;
|
||||||
|
|
||||||
VideoAttachment({
|
VideoAttachment({
|
||||||
Key key,
|
Key key,
|
||||||
@required this.attachment,
|
@required this.attachment,
|
||||||
@required this.messageTheme,
|
@required this.messageTheme,
|
||||||
|
this.videoPackage,
|
||||||
this.message,
|
this.message,
|
||||||
this.size,
|
this.size,
|
||||||
this.onShowMessage,
|
this.onShowMessage,
|
||||||
@@ -32,13 +30,21 @@ class VideoAttachment extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _VideoAttachmentState extends State<VideoAttachment> {
|
class _VideoAttachmentState extends State<VideoAttachment> {
|
||||||
ChewieController _chewieController;
|
|
||||||
VideoPlayerController _videoPlayerController;
|
|
||||||
bool initialized = false;
|
bool initialized = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
widget.videoPackage.onInit = () {
|
||||||
|
setState(() {
|
||||||
|
initialized = true;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (!initialized) {
|
if (!widget.videoPackage.initialised) {
|
||||||
return Container(
|
return Container(
|
||||||
height: widget.size?.height ?? 100,
|
height: widget.size?.height ?? 100,
|
||||||
width: widget.size?.width ?? 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(
|
return GestureDetector(
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
@@ -123,7 +92,7 @@ class _VideoAttachmentState extends State<VideoAttachment> {
|
|||||||
child: Stack(
|
child: Stack(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Chewie(
|
Chewie(
|
||||||
controller: _chewieController,
|
controller: widget.videoPackage.chewieController,
|
||||||
),
|
),
|
||||||
Positioned.fill(
|
Positioned.fill(
|
||||||
child: Center(
|
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));
|
findsNWidgets(2));
|
||||||
expect(find.byKey(Key('StreamSvgIcon-Icon_love_reaction.svg')),
|
expect(find.byKey(Key('StreamSvgIcon-Icon_love_reaction.svg')),
|
||||||
findsNWidgets(2));
|
findsNWidgets(2));
|
||||||
expect(find.text(testUserId), findsNWidgets(2));
|
expect(find.text(testUserId.split(' ')[0]), findsNWidgets(2));
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ void main() {
|
|||||||
testWidgets(
|
testWidgets(
|
||||||
'StreamChatCore should disconnect on background',
|
'StreamChatCore should disconnect on background',
|
||||||
(WidgetTester tester) async {
|
(WidgetTester tester) async {
|
||||||
await fakeAsync((_async) {
|
fakeAsync((_async) {
|
||||||
final client = MockClient();
|
final client = MockClient();
|
||||||
final clientState = MockClientState();
|
final clientState = MockClientState();
|
||||||
final channel = MockChannel();
|
final channel = MockChannel();
|
||||||
@@ -123,6 +123,7 @@ void main() {
|
|||||||
_async.elapse(Duration(seconds: 5));
|
_async.elapse(Duration(seconds: 5));
|
||||||
|
|
||||||
verify(client.disconnect()).called(1);
|
verify(client.disconnect()).called(1);
|
||||||
|
eventStreamController.close();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -174,6 +175,7 @@ void main() {
|
|||||||
await untilCalled(showLocalNotificationMock(event));
|
await untilCalled(showLocalNotificationMock(event));
|
||||||
|
|
||||||
verify(showLocalNotificationMock(event)).called(1);
|
verify(showLocalNotificationMock(event)).called(1);
|
||||||
|
eventStreamController.close();
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user