mirror of
https://github.com/tommyxchow/frosty.git
synced 2025-05-17 14:36:05 +08:00
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:
@ -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,
|
||||
|
@ -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;
|
||||
|
||||
|
33
lib/models/shared_chat_session.dart
Normal file
33
lib/models/shared_chat_session.dart
Normal 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);
|
||||
}
|
22
lib/models/shared_chat_session.g.dart
Normal file
22
lib/models/shared_chat_session.g.dart
Normal 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,
|
||||
);
|
@ -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) {
|
||||
|
@ -173,6 +173,7 @@ class ChatMessage extends StatelessWidget {
|
||||
useReadableColors: chatStore.settings.useReadableColors,
|
||||
launchExternal: chatStore.settings.launchUrlExternal,
|
||||
timestamp: chatStore.settings.timestampType,
|
||||
channelIdToUserTwitch: chatStore.channelIdToUserTwitch,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
@ -487,6 +487,8 @@ abstract class VideoStoreBase with Store {
|
||||
});
|
||||
}
|
||||
|
||||
_overlayTimer.cancel();
|
||||
|
||||
_disposeOverlayReaction();
|
||||
_disposeAndroidAutoPipReaction?.call();
|
||||
}
|
||||
|
@ -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)),
|
||||
|
Reference in New Issue
Block a user