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
This commit is contained in:
Tommy Chow
2024-12-23 16:05:07 -05:00
committed by GitHub
parent 4f28408be6
commit 817289a01d
8 changed files with 142 additions and 2 deletions

View File

@ -6,6 +6,7 @@ import 'package:frosty/models/badges.dart';
import 'package:frosty/models/category.dart';
import 'package:frosty/models/channel.dart';
import 'package:frosty/models/emotes.dart';
import 'package:frosty/models/shared_chat_session.dart';
import 'package:frosty/models/stream.dart';
import 'package:frosty/models/user.dart';
import 'package:http/http.dart';
@ -540,6 +541,28 @@ class TwitchApi {
}
}
Future<SharedChatSession?> getSharedChatSession({
required String broadcasterId,
required Map<String, String> headers,
}) async {
final url = Uri.parse(
'https://api.twitch.tv/helix/shared_chat/session?broadcaster_id=$broadcasterId',
);
final response = await _client.get(url, headers: headers);
if (response.statusCode == 200) {
final sessionData = jsonDecode(response.body)['data'] as List;
if (sessionData.isEmpty) {
return null;
}
return SharedChatSession.fromJson(sessionData.first);
} else {
return Future.error('Failed to get shared chat session info');
}
}
// Unblocks the user with the given ID and returns true on success or false on failure.
Future<List<dynamic>> getRecentMessages({
required String userLogin,

View File

@ -1,3 +1,4 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:collection/collection.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
@ -6,6 +7,7 @@ import 'package:flutter_svg/flutter_svg.dart';
import 'package:frosty/constants.dart';
import 'package:frosty/models/badges.dart';
import 'package:frosty/models/emotes.dart';
import 'package:frosty/models/user.dart';
import 'package:frosty/screens/channel/chat/stores/chat_assets_store.dart';
import 'package:frosty/screens/settings/stores/settings_store.dart';
import 'package:frosty/utils.dart';
@ -121,6 +123,7 @@ class IRCMessage {
void Function(String)? onTapPingedUser,
bool showMessage = true,
bool useReadableColors = false,
Map<String, UserTwitch>? channelIdToUserTwitch,
TimestampType timestamp = TimestampType.disabled,
}) {
final isLightTheme = Theme.of(context).brightness == Brightness.light;
@ -167,6 +170,43 @@ class IRCMessage {
}
}
final sourceChannelId = tags['source-room-id'] ?? tags['room-id'];
final sourceChannelUser = channelIdToUserTwitch != null
? channelIdToUserTwitch[sourceChannelId]
: null;
if (sourceChannelUser != null) {
span.add(
WidgetSpan(
child: Tooltip(
triggerMode: TooltipTriggerMode.tap,
preferBelow: false,
message: sourceChannelUser.displayName,
child: CachedNetworkImage(
imageUrl: sourceChannelUser.profileImageUrl,
imageBuilder: (context, imageProvider) => Container(
width: badgeSize,
height: badgeSize,
decoration: BoxDecoration(
shape: BoxShape.circle,
image:
DecorationImage(image: imageProvider, fit: BoxFit.cover),
),
),
placeholder: (context, url) => Container(
width: badgeSize,
height: badgeSize,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.grey,
),
),
),
),
),
);
span.add(const TextSpan(text: ' '));
}
// Indicator to skip adding the bot badges later when adding the rest of FFZ badges.
var skipBot = false;

View File

@ -0,0 +1,33 @@
import 'package:json_annotation/json_annotation.dart';
part 'shared_chat_session.g.dart';
@JsonSerializable(fieldRename: FieldRename.snake, createToJson: false)
class SharedChatSession {
final String sessionId;
final String hostBroadcasterId;
final List<Participant> participants;
final String createdAt;
final String updatedAt;
SharedChatSession({
required this.sessionId,
required this.hostBroadcasterId,
required this.participants,
required this.createdAt,
required this.updatedAt,
});
factory SharedChatSession.fromJson(Map<String, dynamic> json) =>
_$SharedChatSessionFromJson(json);
}
@JsonSerializable(fieldRename: FieldRename.snake, createToJson: false)
class Participant {
final String broadcasterId;
Participant({required this.broadcasterId});
factory Participant.fromJson(Map<String, dynamic> json) =>
_$ParticipantFromJson(json);
}

View File

@ -0,0 +1,22 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'shared_chat_session.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
SharedChatSession _$SharedChatSessionFromJson(Map<String, dynamic> json) =>
SharedChatSession(
sessionId: json['session_id'] as String,
hostBroadcasterId: json['host_broadcaster_id'] as String,
participants: (json['participants'] as List<dynamic>)
.map((e) => Participant.fromJson(e as Map<String, dynamic>))
.toList(),
createdAt: json['created_at'] as String,
updatedAt: json['updated_at'] as String,
);
Participant _$ParticipantFromJson(Map<String, dynamic> json) => Participant(
broadcasterId: json['broadcaster_id'] as String,
);

View File

@ -6,6 +6,7 @@ import 'package:frosty/apis/twitch_api.dart';
import 'package:frosty/models/emotes.dart';
import 'package:frosty/models/events.dart';
import 'package:frosty/models/irc.dart';
import 'package:frosty/models/user.dart';
import 'package:frosty/screens/channel/chat/details/chat_details_store.dart';
import 'package:frosty/screens/channel/chat/stores/chat_assets_store.dart';
import 'package:frosty/screens/settings/stores/auth_store.dart';
@ -152,6 +153,8 @@ abstract class ChatStoreBase with Store {
@observable
IRCMessage? replyingToMessage;
final channelIdToUserTwitch = ObservableMap<String, UserTwitch>();
ChatStoreBase({
required this.twitchApi,
required this.auth,
@ -204,6 +207,24 @@ abstract class ChatStoreBase with Store {
assetsStore.init();
// Get the shared chat session and the profile pictures of the participants.
twitchApi
.getSharedChatSession(
broadcasterId: channelId,
headers: auth.headersTwitch,
)
.then((sharedChatSession) {
if (sharedChatSession == null) return;
for (final participant in sharedChatSession.participants) {
twitchApi
.getUser(id: participant.broadcasterId, headers: auth.headersTwitch)
.then((user) {
channelIdToUserTwitch[participant.broadcasterId] = user;
});
}
});
_messages.add(IRCMessage.createNotice(message: 'Connecting to chat...'));
if (settings.showVideo && settings.chatDelay > 0) {

View File

@ -173,6 +173,7 @@ class ChatMessage extends StatelessWidget {
useReadableColors: chatStore.settings.useReadableColors,
launchExternal: chatStore.settings.launchUrlExternal,
timestamp: chatStore.settings.timestampType,
channelIdToUserTwitch: chatStore.channelIdToUserTwitch,
),
),
);

View File

@ -487,6 +487,8 @@ abstract class VideoStoreBase with Store {
});
}
_overlayTimer.cancel();
_disposeOverlayReaction();
_disposeAndroidAutoPipReaction?.call();
}

View File

@ -71,8 +71,6 @@ class FrostyThemes {
tabAlignment: TabAlignment.start,
),
tooltipTheme: TooltipThemeData(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
margin: const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration(
color: backgroundColor,
borderRadius: const BorderRadius.all(Radius.circular(8)),