Skip to content

Commit 817289a

Browse files
authored
add shared chat session support (#424)
* dispose overlay timer * fix pip * add channel indicators to messages for shared chat sessions * add tooltip to chat message pfp * remove tooltip marign/padding * return null instead of throwing error * fix build error
1 parent 4f28408 commit 817289a

File tree

8 files changed

+142
-2
lines changed

8 files changed

+142
-2
lines changed

lib/apis/twitch_api.dart

+23
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import 'package:frosty/models/badges.dart';
66
import 'package:frosty/models/category.dart';
77
import 'package:frosty/models/channel.dart';
88
import 'package:frosty/models/emotes.dart';
9+
import 'package:frosty/models/shared_chat_session.dart';
910
import 'package:frosty/models/stream.dart';
1011
import 'package:frosty/models/user.dart';
1112
import 'package:http/http.dart';
@@ -540,6 +541,28 @@ class TwitchApi {
540541
}
541542
}
542543

544+
Future<SharedChatSession?> getSharedChatSession({
545+
required String broadcasterId,
546+
required Map<String, String> headers,
547+
}) async {
548+
final url = Uri.parse(
549+
'https://api.twitch.tv/helix/shared_chat/session?broadcaster_id=$broadcasterId',
550+
);
551+
552+
final response = await _client.get(url, headers: headers);
553+
if (response.statusCode == 200) {
554+
final sessionData = jsonDecode(response.body)['data'] as List;
555+
556+
if (sessionData.isEmpty) {
557+
return null;
558+
}
559+
560+
return SharedChatSession.fromJson(sessionData.first);
561+
} else {
562+
return Future.error('Failed to get shared chat session info');
563+
}
564+
}
565+
543566
// Unblocks the user with the given ID and returns true on success or false on failure.
544567
Future<List<dynamic>> getRecentMessages({
545568
required String userLogin,

lib/models/irc.dart

+40
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import 'package:cached_network_image/cached_network_image.dart';
12
import 'package:collection/collection.dart';
23
import 'package:flutter/gestures.dart';
34
import 'package:flutter/material.dart';
@@ -6,6 +7,7 @@ import 'package:flutter_svg/flutter_svg.dart';
67
import 'package:frosty/constants.dart';
78
import 'package:frosty/models/badges.dart';
89
import 'package:frosty/models/emotes.dart';
10+
import 'package:frosty/models/user.dart';
911
import 'package:frosty/screens/channel/chat/stores/chat_assets_store.dart';
1012
import 'package:frosty/screens/settings/stores/settings_store.dart';
1113
import 'package:frosty/utils.dart';
@@ -121,6 +123,7 @@ class IRCMessage {
121123
void Function(String)? onTapPingedUser,
122124
bool showMessage = true,
123125
bool useReadableColors = false,
126+
Map<String, UserTwitch>? channelIdToUserTwitch,
124127
TimestampType timestamp = TimestampType.disabled,
125128
}) {
126129
final isLightTheme = Theme.of(context).brightness == Brightness.light;
@@ -167,6 +170,43 @@ class IRCMessage {
167170
}
168171
}
169172

173+
final sourceChannelId = tags['source-room-id'] ?? tags['room-id'];
174+
final sourceChannelUser = channelIdToUserTwitch != null
175+
? channelIdToUserTwitch[sourceChannelId]
176+
: null;
177+
if (sourceChannelUser != null) {
178+
span.add(
179+
WidgetSpan(
180+
child: Tooltip(
181+
triggerMode: TooltipTriggerMode.tap,
182+
preferBelow: false,
183+
message: sourceChannelUser.displayName,
184+
child: CachedNetworkImage(
185+
imageUrl: sourceChannelUser.profileImageUrl,
186+
imageBuilder: (context, imageProvider) => Container(
187+
width: badgeSize,
188+
height: badgeSize,
189+
decoration: BoxDecoration(
190+
shape: BoxShape.circle,
191+
image:
192+
DecorationImage(image: imageProvider, fit: BoxFit.cover),
193+
),
194+
),
195+
placeholder: (context, url) => Container(
196+
width: badgeSize,
197+
height: badgeSize,
198+
decoration: BoxDecoration(
199+
shape: BoxShape.circle,
200+
color: Colors.grey,
201+
),
202+
),
203+
),
204+
),
205+
),
206+
);
207+
span.add(const TextSpan(text: ' '));
208+
}
209+
170210
// Indicator to skip adding the bot badges later when adding the rest of FFZ badges.
171211
var skipBot = false;
172212

lib/models/shared_chat_session.dart

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import 'package:json_annotation/json_annotation.dart';
2+
3+
part 'shared_chat_session.g.dart';
4+
5+
@JsonSerializable(fieldRename: FieldRename.snake, createToJson: false)
6+
class SharedChatSession {
7+
final String sessionId;
8+
final String hostBroadcasterId;
9+
final List<Participant> participants;
10+
final String createdAt;
11+
final String updatedAt;
12+
13+
SharedChatSession({
14+
required this.sessionId,
15+
required this.hostBroadcasterId,
16+
required this.participants,
17+
required this.createdAt,
18+
required this.updatedAt,
19+
});
20+
21+
factory SharedChatSession.fromJson(Map<String, dynamic> json) =>
22+
_$SharedChatSessionFromJson(json);
23+
}
24+
25+
@JsonSerializable(fieldRename: FieldRename.snake, createToJson: false)
26+
class Participant {
27+
final String broadcasterId;
28+
29+
Participant({required this.broadcasterId});
30+
31+
factory Participant.fromJson(Map<String, dynamic> json) =>
32+
_$ParticipantFromJson(json);
33+
}

lib/models/shared_chat_session.g.dart

+22
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lib/screens/channel/chat/stores/chat_store.dart

+21
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import 'package:frosty/apis/twitch_api.dart';
66
import 'package:frosty/models/emotes.dart';
77
import 'package:frosty/models/events.dart';
88
import 'package:frosty/models/irc.dart';
9+
import 'package:frosty/models/user.dart';
910
import 'package:frosty/screens/channel/chat/details/chat_details_store.dart';
1011
import 'package:frosty/screens/channel/chat/stores/chat_assets_store.dart';
1112
import 'package:frosty/screens/settings/stores/auth_store.dart';
@@ -152,6 +153,8 @@ abstract class ChatStoreBase with Store {
152153
@observable
153154
IRCMessage? replyingToMessage;
154155

156+
final channelIdToUserTwitch = ObservableMap<String, UserTwitch>();
157+
155158
ChatStoreBase({
156159
required this.twitchApi,
157160
required this.auth,
@@ -204,6 +207,24 @@ abstract class ChatStoreBase with Store {
204207

205208
assetsStore.init();
206209

210+
// Get the shared chat session and the profile pictures of the participants.
211+
twitchApi
212+
.getSharedChatSession(
213+
broadcasterId: channelId,
214+
headers: auth.headersTwitch,
215+
)
216+
.then((sharedChatSession) {
217+
if (sharedChatSession == null) return;
218+
219+
for (final participant in sharedChatSession.participants) {
220+
twitchApi
221+
.getUser(id: participant.broadcasterId, headers: auth.headersTwitch)
222+
.then((user) {
223+
channelIdToUserTwitch[participant.broadcasterId] = user;
224+
});
225+
}
226+
});
227+
207228
_messages.add(IRCMessage.createNotice(message: 'Connecting to chat...'));
208229

209230
if (settings.showVideo && settings.chatDelay > 0) {

lib/screens/channel/chat/widgets/chat_message.dart

+1
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,7 @@ class ChatMessage extends StatelessWidget {
173173
useReadableColors: chatStore.settings.useReadableColors,
174174
launchExternal: chatStore.settings.launchUrlExternal,
175175
timestamp: chatStore.settings.timestampType,
176+
channelIdToUserTwitch: chatStore.channelIdToUserTwitch,
176177
),
177178
),
178179
);

lib/screens/channel/video/video_store.dart

+2
Original file line numberDiff line numberDiff line change
@@ -487,6 +487,8 @@ abstract class VideoStoreBase with Store {
487487
});
488488
}
489489

490+
_overlayTimer.cancel();
491+
490492
_disposeOverlayReaction();
491493
_disposeAndroidAutoPipReaction?.call();
492494
}

lib/theme.dart

-2
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,6 @@ class FrostyThemes {
7171
tabAlignment: TabAlignment.start,
7272
),
7373
tooltipTheme: TooltipThemeData(
74-
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
75-
margin: const EdgeInsets.symmetric(horizontal: 16),
7674
decoration: BoxDecoration(
7775
color: backgroundColor,
7876
borderRadius: const BorderRadius.all(Radius.circular(8)),

0 commit comments

Comments
 (0)