Skip to content

Commit cba38f2

Browse files
committed
Handle websocket events
1 parent 4151875 commit cba38f2

18 files changed

+410
-96
lines changed

packages/stream_feeds/lib/src/feeds_client.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import 'dart:async';
22

3+
import 'package:rxdart/rxdart.dart';
34
import 'package:stream_core/stream_core.dart';
45
import 'package:uuid/uuid.dart';
56

@@ -59,6 +60,8 @@ class FeedsClient {
5960
static final endpointConfig = EndpointConfig.production;
6061
late final WebSocketClient webSocketClient;
6162
ConnectionRecoveryHandler? connectionRecoveryHandler;
63+
Stream<FeedsWsEvent> get feedsEvents =>
64+
webSocketClient.events.asStream().whereType<FeedsWsEvent>();
6265

6366
Completer<void>? _connectionCompleter;
6467
StreamSubscription<WebSocketConnectionState>? _connectionSubscription;

packages/stream_feeds/lib/src/generated/api/api_helper.g.dart

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,15 @@ T? mapValueOfType<T>(dynamic map, String key, [T? defaultValue]) {
9696
/// Returns a valid Map<K, V> found at the specified Map [key], null otherwise.
9797
Map<K, V>? mapCastOfType<K, V>(dynamic map, String key) {
9898
final dynamic value = map is Map ? map[key] : null;
99+
if (value is! Map) {
100+
return null;
101+
}
102+
// TODO: use json_serializable for json mapping
103+
if (V == ReactionGroupResponse) {
104+
return value.map(
105+
(key, value) => MapEntry(key, ReactionGroupResponse.fromJson(value) as V),
106+
);
107+
}
99108
return value is Map ? value.cast<K, V>() : null;
100109
}
101110

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1-
export 'package:stream_core/stream_core.dart' show User;
1+
export 'package:stream_core/stream_core.dart'
2+
show PaginationData, PaginationResult, User;
23

4+
export 'models/activity_data.dart';
35
export 'models/feed_id.dart';
6+
export 'models/feeds_reaction_data.dart';
7+
export 'models/identifiable.dart';
8+
export 'models/reaction_group_data.dart';
9+
export 'models/user_data.dart';
10+
export 'models/get_or_create_feed_data.dart';
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import '../models.dart';
2+
import '../utils/list_extensions.dart';
3+
4+
// TODO: incomplete
5+
class ActivityData implements Identifiable {
6+
const ActivityData({
7+
required this.id,
8+
required this.user,
9+
required this.text,
10+
required this.reactionCount,
11+
required this.latestReactions,
12+
required this.ownReactions,
13+
required this.reactionGroups,
14+
});
15+
16+
@override
17+
final String id;
18+
final UserData user;
19+
final String? text;
20+
final int reactionCount;
21+
final List<FeedsReactionData> latestReactions;
22+
final List<FeedsReactionData> ownReactions;
23+
final Map<String, ReactionGroupData> reactionGroups;
24+
25+
ActivityData copyWith({
26+
String? text,
27+
int? reactionCount,
28+
List<FeedsReactionData>? latestReactions,
29+
List<FeedsReactionData>? ownReactions,
30+
Map<String, ReactionGroupData>? reactionGroups,
31+
}) {
32+
return ActivityData(
33+
id: id,
34+
user: user,
35+
text: text ?? this.text,
36+
reactionCount: reactionCount ?? this.reactionCount,
37+
latestReactions: latestReactions ?? this.latestReactions,
38+
ownReactions: ownReactions ?? this.ownReactions,
39+
reactionGroups: reactionGroups ?? this.reactionGroups,
40+
);
41+
}
42+
43+
ActivityData addReaction(FeedsReactionData reaction,
44+
{required String currentUserId}) {
45+
final newLatestReactions = [...latestReactions];
46+
final newReactionGroups =
47+
Map<String, ReactionGroupData>.from(reactionGroups);
48+
var newOwnReactions = ownReactions;
49+
50+
reaction.updateByAdding(
51+
to: [...latestReactions],
52+
reactionGroups: newReactionGroups,
53+
);
54+
if (reaction.user.id == currentUserId) {
55+
newOwnReactions = ownReactions.insertById(reaction);
56+
}
57+
58+
return copyWith(
59+
reactionCount: reactionCount + 1,
60+
latestReactions: List.unmodifiable(newLatestReactions),
61+
ownReactions: newOwnReactions,
62+
reactionGroups: Map.unmodifiable(newReactionGroups),
63+
);
64+
}
65+
66+
ActivityData removeReaction(FeedsReactionData reaction,
67+
{required String currentUserId}) {
68+
final newLatestReactions = [...latestReactions];
69+
final newReactionGroups =
70+
Map<String, ReactionGroupData>.from(reactionGroups);
71+
var newOwnReactions = ownReactions;
72+
73+
reaction.updateByRemoving(
74+
to: [...latestReactions],
75+
reactionGroups: newReactionGroups,
76+
);
77+
if (reaction.user.id == currentUserId) {
78+
newOwnReactions = ownReactions.removeById(reaction);
79+
}
80+
81+
return copyWith(
82+
reactionCount: reactionCount - 1,
83+
latestReactions: List.unmodifiable(newLatestReactions),
84+
ownReactions: newOwnReactions,
85+
reactionGroups: Map.unmodifiable(newReactionGroups),
86+
);
87+
}
88+
}
89+
90+
extension on FeedsReactionData {
91+
void updateByAdding({
92+
required List<FeedsReactionData> to,
93+
required Map<String, ReactionGroupData> reactionGroups,
94+
}) {
95+
to.insertById(this, createNewList: false);
96+
97+
final reactionGroup = reactionGroups[type]?.increment(createdAt) ??
98+
ReactionGroupData(
99+
count: 1,
100+
firstReactionAt: createdAt,
101+
lastReactionAt: createdAt,
102+
);
103+
reactionGroups[type] = reactionGroup;
104+
}
105+
106+
void updateByRemoving({
107+
required List<FeedsReactionData> to,
108+
required Map<String, ReactionGroupData> reactionGroups,
109+
}) {
110+
to.removeById(this, createNewList: false);
111+
var reactionGroup = reactionGroups[type];
112+
if (reactionGroup != null) {
113+
reactionGroup = reactionGroup.decrement(createdAt);
114+
if (reactionGroup.isEmpty) {
115+
reactionGroups.remove(type);
116+
} else {
117+
reactionGroups[type] = reactionGroup;
118+
}
119+
}
120+
}
121+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import '../models.dart';
2+
3+
class FeedsReactionData implements Identifiable {
4+
FeedsReactionData({
5+
required this.activityId,
6+
required this.createdAt,
7+
required this.customData,
8+
required this.type,
9+
required this.updatedAt,
10+
required this.user,
11+
});
12+
13+
@override
14+
String get id => '$activityId${user.id}';
15+
16+
final String activityId;
17+
final DateTime createdAt;
18+
final Map<String, dynamic>? customData;
19+
final String type;
20+
final DateTime updatedAt;
21+
final UserData user;
22+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// TODO: incomplete
2+
import '../models.dart';
3+
4+
class GetOrCreateFeedData {
5+
const GetOrCreateFeedData({
6+
required this.activities,
7+
});
8+
9+
final PaginationResult<ActivityData> activities;
10+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
abstract interface class Identifiable {
2+
String get id;
3+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
class ReactionGroupData {
2+
const ReactionGroupData({
3+
required this.count,
4+
required this.firstReactionAt,
5+
required this.lastReactionAt,
6+
});
7+
8+
bool get isEmpty => count <= 0;
9+
final int count;
10+
final DateTime firstReactionAt;
11+
final DateTime lastReactionAt;
12+
13+
ReactionGroupData increment(DateTime reactionCreatedAt) {
14+
if (!reactionCreatedAt.isAfter(lastReactionAt)) return this;
15+
16+
return ReactionGroupData(
17+
count: count + 1,
18+
firstReactionAt: firstReactionAt,
19+
lastReactionAt: reactionCreatedAt,
20+
);
21+
}
22+
23+
ReactionGroupData decrement(DateTime reactionCreatedAt) {
24+
if (reactionCreatedAt.isBefore(firstReactionAt) ||
25+
reactionCreatedAt.isAfter(lastReactionAt) ||
26+
count == 0) {
27+
return this;
28+
}
29+
30+
return ReactionGroupData(
31+
count: count - 1,
32+
firstReactionAt: firstReactionAt,
33+
lastReactionAt: lastReactionAt,
34+
);
35+
}
36+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
class UserData {
2+
const UserData({
3+
required this.id,
4+
this.name,
5+
this.imageUrl,
6+
});
7+
8+
final String id;
9+
final String? name;
10+
final String? imageUrl;
11+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import '../../stream_feeds.dart';
2+
import '../generated/api/api.g.dart' as api;
3+
import '../models.dart';
4+
5+
extension FeedsReactionResponseMapper on api.FeedsReactionResponse {
6+
FeedsReactionData toModel() {
7+
return FeedsReactionData(
8+
activityId: activityId,
9+
createdAt: createdAt,
10+
customData: custom,
11+
type: type,
12+
updatedAt: updatedAt,
13+
user: user.toModel(),
14+
);
15+
}
16+
}
17+
18+
extension UserResponseMapper on api.UserResponse {
19+
UserData toModel() {
20+
return UserData(id: id, name: name, imageUrl: image);
21+
}
22+
}
23+
24+
extension ReactionGroupResponseMapper on api.ReactionGroupResponse {
25+
ReactionGroupData toModel() {
26+
return ReactionGroupData(
27+
count: count,
28+
firstReactionAt: firstReactionAt,
29+
lastReactionAt: lastReactionAt,
30+
);
31+
}
32+
}
33+
34+
extension GetOrCreateFeedResponseMapper on api.GetOrCreateFeedResponse {
35+
PaginationResult<ActivityData> toPaginatedActivityData() {
36+
return PaginationResult(
37+
items: activities.map(
38+
(e) {
39+
return ActivityData(
40+
id: e.id,
41+
user: e.user.toModel(),
42+
text: e.text,
43+
reactionCount: e.reactionCount,
44+
latestReactions: e.latestReactions.map((e) => e.toModel()).toList(),
45+
ownReactions: e.ownReactions.map((e) => e.toModel()).toList(),
46+
reactionGroups: e.reactionGroups.map<String, ReactionGroupData>(
47+
(key, value) => MapEntry(key, value.toModel()),
48+
),
49+
);
50+
},
51+
).toList(),
52+
pagination: PaginationData(next: next, previous: prev),
53+
);
54+
}
55+
}

0 commit comments

Comments
 (0)