Add support for 7TV emote update events (#305)
* Get 7TV emote set id * Add initial support for 7TV emote set events in chat * Add 7TV emote add/remove alerts in chat * Add author to 7TV events
This commit is contained in:
parent
466adea731
commit
8fab6bdbc9
19 changed files with 335 additions and 62 deletions
|
|
@ -26,18 +26,24 @@ class SevenTVApi {
|
|||
}
|
||||
}
|
||||
|
||||
/// Returns a map of a channel's 7TV emotes to their URL.
|
||||
Future<List<Emote>> getEmotesChannel({required String id}) async {
|
||||
/// Returns a tuple containing the emote set ID and a map of a channel's 7TV
|
||||
/// emotes to their URL.
|
||||
Future<(String, List<Emote>)> getEmotesChannel({required String id}) async {
|
||||
final url = Uri.parse('https://7tv.io/v3/users/twitch/$id');
|
||||
|
||||
final response = await _client.get(url);
|
||||
if (response.statusCode == 200) {
|
||||
final decoded = jsonDecode(response.body)['emote_set']['emotes'] as List;
|
||||
final emotes = decoded.map((emote) => Emote7TV.fromJson(emote));
|
||||
final decoded = jsonDecode(response.body);
|
||||
final emoteSetId = decoded['emote_set']['id'] as String;
|
||||
final emotes = (decoded['emote_set']['emotes'] as List)
|
||||
.map((emote) => Emote7TV.fromJson(emote));
|
||||
|
||||
return emotes
|
||||
.map((emote) => Emote.from7TV(emote, EmoteType.sevenTVChannel))
|
||||
.toList();
|
||||
return (
|
||||
emoteSetId,
|
||||
emotes
|
||||
.map((emote) => Emote.from7TV(emote, EmoteType.sevenTVChannel))
|
||||
.toList()
|
||||
);
|
||||
} else {
|
||||
return Future.error('Failed to get 7TV channel emotes');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -109,7 +109,7 @@ class EmoteFFZ {
|
|||
class Emote7TV {
|
||||
final String id;
|
||||
final String name;
|
||||
final Emote7TVData data;
|
||||
final Emote7TVData? data;
|
||||
|
||||
const Emote7TV(
|
||||
this.id,
|
||||
|
|
@ -121,6 +121,24 @@ class Emote7TV {
|
|||
_$Emote7TVFromJson(json);
|
||||
}
|
||||
|
||||
@JsonSerializable(createToJson: false, fieldRename: FieldRename.snake)
|
||||
class Emote7TVUser {
|
||||
final String id;
|
||||
final String username;
|
||||
final String displayName;
|
||||
final String avatarUrl;
|
||||
|
||||
const Emote7TVUser({
|
||||
required this.id,
|
||||
required this.username,
|
||||
required this.displayName,
|
||||
required this.avatarUrl,
|
||||
});
|
||||
|
||||
factory Emote7TVUser.fromJson(Map<String, dynamic> json) =>
|
||||
_$Emote7TVUserFromJson(json);
|
||||
}
|
||||
|
||||
@JsonSerializable(createToJson: false, fieldRename: FieldRename.snake)
|
||||
class Emote7TVData {
|
||||
final String id;
|
||||
|
|
@ -220,20 +238,22 @@ class Emote {
|
|||
);
|
||||
|
||||
factory Emote.from7TV(Emote7TV emote, EmoteType type) {
|
||||
final url = emote.data.host.url;
|
||||
final emoteData = emote.data;
|
||||
|
||||
final url = emoteData!.host.url;
|
||||
// Flutter doesn't support AVIF yet.
|
||||
final file = emote.data.host.files.reversed.firstWhere(
|
||||
final file = emoteData.host.files.reversed.firstWhere(
|
||||
(file) => file.format != 'AVIF' && file.name.contains('4x'),
|
||||
);
|
||||
|
||||
// Check if the flag has 1 at the 8th bit.
|
||||
final isZeroWidth = (emote.data.flags & 256) == 256;
|
||||
final isZeroWidth = (emoteData.flags & 256) == 256;
|
||||
|
||||
return Emote(
|
||||
name: emote.name,
|
||||
realName: emote.name != emote.data.name ? emote.data.name : null,
|
||||
width: emote.data.host.files.first.width,
|
||||
height: emote.data.host.files.first.height,
|
||||
realName: emote.name != emoteData.name ? emoteData.name : null,
|
||||
width: emoteData.host.files.first.width,
|
||||
height: emoteData.host.files.first.height,
|
||||
zeroWidth: isZeroWidth,
|
||||
url: 'https:$url/${file.name}',
|
||||
type: type,
|
||||
|
|
|
|||
|
|
@ -54,7 +54,16 @@ EmoteFFZ _$EmoteFFZFromJson(Map<String, dynamic> json) => EmoteFFZ(
|
|||
Emote7TV _$Emote7TVFromJson(Map<String, dynamic> json) => Emote7TV(
|
||||
json['id'] as String,
|
||||
json['name'] as String,
|
||||
Emote7TVData.fromJson(json['data'] as Map<String, dynamic>),
|
||||
json['data'] == null
|
||||
? null
|
||||
: Emote7TVData.fromJson(json['data'] as Map<String, dynamic>),
|
||||
);
|
||||
|
||||
Emote7TVUser _$Emote7TVUserFromJson(Map<String, dynamic> json) => Emote7TVUser(
|
||||
id: json['id'] as String,
|
||||
username: json['username'] as String,
|
||||
displayName: json['display_name'] as String,
|
||||
avatarUrl: json['avatar_url'] as String,
|
||||
);
|
||||
|
||||
Emote7TVData _$Emote7TVDataFromJson(Map<String, dynamic> json) => Emote7TVData(
|
||||
|
|
|
|||
72
lib/models/events.dart
Normal file
72
lib/models/events.dart
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
import 'package:frosty/models/emotes.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'events.g.dart';
|
||||
|
||||
@JsonSerializable()
|
||||
class SevenTVEvent {
|
||||
final int op;
|
||||
@JsonKey(includeIfNull: false)
|
||||
final int? t;
|
||||
final SevenTVEventData d;
|
||||
|
||||
const SevenTVEvent({
|
||||
required this.op,
|
||||
this.t,
|
||||
required this.d,
|
||||
});
|
||||
|
||||
factory SevenTVEvent.fromJson(Map<String, dynamic> json) =>
|
||||
_$SevenTVEventFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$SevenTVEventToJson(this);
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
class SevenTVEventData {
|
||||
final String? type;
|
||||
final Map<String, String>? condition;
|
||||
@JsonKey(includeToJson: false)
|
||||
final SevenTVEventEmoteSetBody? body;
|
||||
|
||||
const SevenTVEventData({
|
||||
required this.type,
|
||||
this.condition,
|
||||
this.body,
|
||||
});
|
||||
|
||||
factory SevenTVEventData.fromJson(Map<String, dynamic> json) =>
|
||||
_$SevenTVEventDataFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$SevenTVEventDataToJson(this);
|
||||
}
|
||||
|
||||
@JsonSerializable(createToJson: false, fieldRename: FieldRename.snake)
|
||||
class SevenTVEventUpdatedEmote {
|
||||
final Emote7TV? value;
|
||||
final Emote7TV? oldValue;
|
||||
|
||||
const SevenTVEventUpdatedEmote({
|
||||
this.value,
|
||||
this.oldValue,
|
||||
});
|
||||
|
||||
factory SevenTVEventUpdatedEmote.fromJson(Map<String, dynamic> json) =>
|
||||
_$SevenTVEventUpdatedEmoteFromJson(json);
|
||||
}
|
||||
|
||||
@JsonSerializable(createToJson: false)
|
||||
class SevenTVEventEmoteSetBody {
|
||||
final Emote7TVUser actor;
|
||||
final List<SevenTVEventUpdatedEmote>? pushed;
|
||||
final List<SevenTVEventUpdatedEmote>? pulled;
|
||||
|
||||
const SevenTVEventEmoteSetBody({
|
||||
required this.actor,
|
||||
this.pushed,
|
||||
this.pulled,
|
||||
});
|
||||
|
||||
factory SevenTVEventEmoteSetBody.fromJson(Map<String, dynamic> json) =>
|
||||
_$SevenTVEventEmoteSetBodyFromJson(json);
|
||||
}
|
||||
72
lib/models/events.g.dart
Normal file
72
lib/models/events.g.dart
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'events.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
SevenTVEvent _$SevenTVEventFromJson(Map<String, dynamic> json) => SevenTVEvent(
|
||||
op: json['op'] as int,
|
||||
t: json['t'] as int?,
|
||||
d: SevenTVEventData.fromJson(json['d'] as Map<String, dynamic>),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$SevenTVEventToJson(SevenTVEvent instance) {
|
||||
final val = <String, dynamic>{
|
||||
'op': instance.op,
|
||||
};
|
||||
|
||||
void writeNotNull(String key, dynamic value) {
|
||||
if (value != null) {
|
||||
val[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
writeNotNull('t', instance.t);
|
||||
val['d'] = instance.d;
|
||||
return val;
|
||||
}
|
||||
|
||||
SevenTVEventData _$SevenTVEventDataFromJson(Map<String, dynamic> json) =>
|
||||
SevenTVEventData(
|
||||
type: json['type'] as String?,
|
||||
condition: (json['condition'] as Map<String, dynamic>?)?.map(
|
||||
(k, e) => MapEntry(k, e as String),
|
||||
),
|
||||
body: json['body'] == null
|
||||
? null
|
||||
: SevenTVEventEmoteSetBody.fromJson(
|
||||
json['body'] as Map<String, dynamic>),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$SevenTVEventDataToJson(SevenTVEventData instance) =>
|
||||
<String, dynamic>{
|
||||
'type': instance.type,
|
||||
'condition': instance.condition,
|
||||
};
|
||||
|
||||
SevenTVEventUpdatedEmote _$SevenTVEventUpdatedEmoteFromJson(
|
||||
Map<String, dynamic> json) =>
|
||||
SevenTVEventUpdatedEmote(
|
||||
value: json['value'] == null
|
||||
? null
|
||||
: Emote7TV.fromJson(json['value'] as Map<String, dynamic>),
|
||||
oldValue: json['old_value'] == null
|
||||
? null
|
||||
: Emote7TV.fromJson(json['old_value'] as Map<String, dynamic>),
|
||||
);
|
||||
|
||||
SevenTVEventEmoteSetBody _$SevenTVEventEmoteSetBodyFromJson(
|
||||
Map<String, dynamic> json) =>
|
||||
SevenTVEventEmoteSetBody(
|
||||
actor: Emote7TVUser.fromJson(json['actor'] as Map<String, dynamic>),
|
||||
pushed: (json['pushed'] as List<dynamic>?)
|
||||
?.map((e) =>
|
||||
SevenTVEventUpdatedEmote.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
pulled: (json['pulled'] as List<dynamic>?)
|
||||
?.map((e) =>
|
||||
SevenTVEventUpdatedEmote.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
);
|
||||
|
|
@ -8,6 +8,7 @@ import 'package:frosty/models/badges.dart';
|
|||
import 'package:frosty/models/emotes.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';
|
||||
import 'package:frosty/widgets/cached_image.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
|
@ -322,9 +323,7 @@ class IRCMessage {
|
|||
final displayName = tags['display-name']!;
|
||||
span.add(
|
||||
TextSpan(
|
||||
text: regexEnglish.hasMatch(displayName)
|
||||
? displayName
|
||||
: '$displayName ($user)',
|
||||
text: getReadableName(displayName, user!),
|
||||
style: TextStyle(
|
||||
color: color,
|
||||
fontWeight: FontWeight.bold,
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import 'package:frosty/apis/bttv_api.dart';
|
|||
import 'package:frosty/apis/ffz_api.dart';
|
||||
import 'package:frosty/apis/seventv_api.dart';
|
||||
import 'package:frosty/apis/twitch_api.dart';
|
||||
import 'package:frosty/constants.dart';
|
||||
import 'package:frosty/screens/channel/chat/chat.dart';
|
||||
import 'package:frosty/screens/channel/chat/details/chat_details_store.dart';
|
||||
import 'package:frosty/screens/channel/chat/stores/chat_assets_store.dart';
|
||||
|
|
@ -19,6 +18,7 @@ import 'package:frosty/screens/channel/video/video_store.dart';
|
|||
import 'package:frosty/screens/settings/stores/auth_store.dart';
|
||||
import 'package:frosty/screens/settings/stores/settings_store.dart';
|
||||
import 'package:frosty/theme.dart';
|
||||
import 'package:frosty/utils.dart';
|
||||
import 'package:frosty/widgets/app_bar.dart';
|
||||
import 'package:frosty/widgets/notification.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
|
@ -79,9 +79,7 @@ class _VideoChatState extends State<VideoChat> {
|
|||
|
||||
final appBar = FrostyAppBar(
|
||||
title: Text(
|
||||
regexEnglish.hasMatch(_chatStore.displayName)
|
||||
? _chatStore.displayName
|
||||
: '${_chatStore.displayName} (${_chatStore.channelName})',
|
||||
getReadableName(_chatStore.displayName, _chatStore.channelName),
|
||||
),
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -22,6 +22,8 @@ abstract class ChatAssetsStoreBase with Store {
|
|||
/// Contains any custom FFZ mod and vip badges for the channel.
|
||||
RoomFFZ? ffzRoomInfo;
|
||||
|
||||
String? sevenTvEmoteSetId;
|
||||
|
||||
@computed
|
||||
List<Emote> get bttvEmotes =>
|
||||
_emoteToObject.values.where((emote) => isBTTV(emote)).toList();
|
||||
|
|
@ -42,7 +44,7 @@ abstract class ChatAssetsStoreBase with Store {
|
|||
|
||||
/// The map of emote words to their image or GIF URL. May be used by anyone in the chat.
|
||||
@readonly
|
||||
var _emoteToObject = <String, Emote>{};
|
||||
var _emoteToObject = ObservableMap<String, Emote>();
|
||||
|
||||
/// The emotes that are "owned" and may be used by the current user.
|
||||
@readonly
|
||||
|
|
@ -165,7 +167,11 @@ abstract class ChatAssetsStoreBase with Store {
|
|||
return emotes;
|
||||
}).catchError(onError),
|
||||
sevenTVApi.getEmotesGlobal().catchError(onError),
|
||||
sevenTVApi.getEmotesChannel(id: channelId).catchError(onError),
|
||||
sevenTVApi.getEmotesChannel(id: channelId).then((data) {
|
||||
final (setId, emotes) = data;
|
||||
sevenTvEmoteSetId = setId;
|
||||
return emotes;
|
||||
}).catchError(onError),
|
||||
ffzApi.getRoomInfo(id: channelId).then((ffzRoom) {
|
||||
final (roomInfo, emotes) = ffzRoom;
|
||||
|
||||
|
|
@ -175,7 +181,7 @@ abstract class ChatAssetsStoreBase with Store {
|
|||
]).then((assets) => assets.expand((list) => list)).then(
|
||||
(emotes) => _emoteToObject = {
|
||||
for (final emote in emotes) emote.name: emote,
|
||||
},
|
||||
}.asObservable(),
|
||||
);
|
||||
|
||||
@action
|
||||
|
|
|
|||
|
|
@ -52,16 +52,16 @@ mixin _$ChatAssetsStore on ChatAssetsStoreBase, Store {
|
|||
late final _$_emoteToObjectAtom =
|
||||
Atom(name: 'ChatAssetsStoreBase._emoteToObject', context: context);
|
||||
|
||||
Map<String, Emote> get emoteToObject {
|
||||
ObservableMap<String, Emote> get emoteToObject {
|
||||
_$_emoteToObjectAtom.reportRead();
|
||||
return super._emoteToObject;
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, Emote> get _emoteToObject => emoteToObject;
|
||||
ObservableMap<String, Emote> get _emoteToObject => emoteToObject;
|
||||
|
||||
@override
|
||||
set _emoteToObject(Map<String, Emote> value) {
|
||||
set _emoteToObject(ObservableMap<String, Emote> value) {
|
||||
_$_emoteToObjectAtom.reportWrite(value, super._emoteToObject, () {
|
||||
super._emoteToObject = value;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,13 +1,15 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:frosty/constants.dart';
|
||||
import 'package:frosty/models/emotes.dart';
|
||||
import 'package:frosty/models/events.dart';
|
||||
import 'package:frosty/models/irc.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';
|
||||
import 'package:frosty/screens/settings/stores/settings_store.dart';
|
||||
import 'package:frosty/utils.dart';
|
||||
import 'package:mobx/mobx.dart';
|
||||
import 'package:wakelock_plus/wakelock_plus.dart';
|
||||
import 'package:web_socket_channel/web_socket_channel.dart';
|
||||
|
|
@ -51,6 +53,10 @@ abstract class ChatStoreBase with Store {
|
|||
/// The subscription that handles the WebSocket connection.
|
||||
StreamSubscription? _channelListener;
|
||||
|
||||
WebSocketChannel? _sevenTVChannel;
|
||||
|
||||
StreamSubscription? _sevenTVChannelListener;
|
||||
|
||||
// The retry counter for exponential backoff.
|
||||
var _retries = 0;
|
||||
|
||||
|
|
@ -321,11 +327,15 @@ abstract class ChatStoreBase with Store {
|
|||
messageBuffer.add(
|
||||
IRCMessage.createNotice(
|
||||
message:
|
||||
"Connected to $displayName${regexEnglish.hasMatch(displayName) ? '' : ' ($channelName)'}'s chat!",
|
||||
"Connected to ${getReadableName(displayName, channelName)}'s chat!",
|
||||
),
|
||||
);
|
||||
|
||||
getAssets();
|
||||
getAssets().then((_) {
|
||||
if (assetsStore.sevenTvEmoteSetId != null) {
|
||||
listenToSevenTVEmoteSet(emoteSetId: assetsStore.sevenTvEmoteSetId!);
|
||||
}
|
||||
});
|
||||
|
||||
// Reset exponential backoff if successfully connected.
|
||||
_retries = 0;
|
||||
|
|
@ -363,6 +373,68 @@ abstract class ChatStoreBase with Store {
|
|||
});
|
||||
}
|
||||
|
||||
@action
|
||||
void listenToSevenTVEmoteSet({required String emoteSetId}) {
|
||||
final subscribePayload = SevenTVEvent(
|
||||
op: 35,
|
||||
d: SevenTVEventData(
|
||||
type: 'emote_set.update',
|
||||
condition: {'object_id': emoteSetId},
|
||||
),
|
||||
);
|
||||
|
||||
_sevenTVChannel?.sink.close(1001);
|
||||
_sevenTVChannel =
|
||||
WebSocketChannel.connect(Uri.parse('wss://events.7tv.io/v3'));
|
||||
|
||||
_sevenTVChannelListener = _sevenTVChannel?.stream.listen(
|
||||
(data) {
|
||||
debugPrint(data);
|
||||
final decoded = jsonDecode(data);
|
||||
|
||||
final event = SevenTVEvent.fromJson(decoded);
|
||||
|
||||
final body = event.d.body;
|
||||
if (event.d.type != 'emote_set.update' || body == null) return;
|
||||
|
||||
if (body.pushed != null) {
|
||||
final pushedEmote = body.pushed?.first.value;
|
||||
|
||||
if (pushedEmote == null) return;
|
||||
|
||||
final emote = Emote.from7TV(pushedEmote, EmoteType.sevenTVChannel);
|
||||
|
||||
assetsStore.emoteToObject[emote.name] = emote;
|
||||
|
||||
messageBuffer.add(
|
||||
IRCMessage.createNotice(
|
||||
message:
|
||||
'${getReadableName(body.actor.displayName, body.actor.username)} added 7TV emote "${emote.name}" to chat',
|
||||
),
|
||||
);
|
||||
} else if (body.pulled != null) {
|
||||
final pulledEmote = body.pulled?.first.oldValue;
|
||||
|
||||
if (pulledEmote == null) return;
|
||||
|
||||
assetsStore.emoteToObject.removeWhere(
|
||||
(name, _) => name == pulledEmote.name,
|
||||
);
|
||||
|
||||
messageBuffer.add(
|
||||
IRCMessage.createNotice(
|
||||
message:
|
||||
'${getReadableName(body.actor.displayName, body.actor.username)} removed 7TV emote "${pulledEmote.name}" from chat',
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
onError: (error) => debugPrint('7TV events error: ${error.toString()}'),
|
||||
);
|
||||
|
||||
_sevenTVChannel?.sink.add(jsonEncode(subscribePayload));
|
||||
}
|
||||
|
||||
@action
|
||||
void connectToChat() {
|
||||
_channel?.sink.close(1001);
|
||||
|
|
@ -575,6 +647,10 @@ abstract class ChatStoreBase with Store {
|
|||
_notificationTimer?.cancel();
|
||||
sleepTimer?.cancel();
|
||||
|
||||
_sevenTVChannel?.sink.close(1001);
|
||||
_sevenTVChannel = null;
|
||||
_sevenTVChannelListener?.cancel();
|
||||
|
||||
_channel?.sink.close(1001);
|
||||
_channel = null;
|
||||
_channelListener?.cancel();
|
||||
|
|
|
|||
|
|
@ -244,6 +244,17 @@ mixin _$ChatStore on ChatStoreBase, Store {
|
|||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void listenToSevenTVEmoteSet({required String emoteSetId}) {
|
||||
final _$actionInfo = _$ChatStoreBaseActionController.startAction(
|
||||
name: 'ChatStoreBase.listenToSevenTVEmoteSet');
|
||||
try {
|
||||
return super.listenToSevenTVEmoteSet(emoteSetId: emoteSetId);
|
||||
} finally {
|
||||
_$ChatStoreBaseActionController.endAction(_$actionInfo);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void connectToChat() {
|
||||
final _$actionInfo = _$ChatStoreBaseActionController.startAction(
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import 'package:frosty/constants.dart';
|
||||
import 'package:frosty/screens/channel/chat/stores/chat_store.dart';
|
||||
import 'package:frosty/screens/channel/chat/widgets/chat_message.dart';
|
||||
import 'package:frosty/utils.dart';
|
||||
import 'package:frosty/widgets/alert_message.dart';
|
||||
import 'package:frosty/widgets/block_report_modal.dart';
|
||||
import 'package:frosty/widgets/profile_picture.dart';
|
||||
|
|
@ -28,9 +28,7 @@ class ChatUserModal extends StatefulWidget {
|
|||
class _ChatUserModalState extends State<ChatUserModal> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final name = regexEnglish.hasMatch(widget.displayName)
|
||||
? widget.displayName
|
||||
: '${widget.displayName} (${widget.username})';
|
||||
final name = getReadableName(widget.displayName, widget.username);
|
||||
|
||||
return SizedBox(
|
||||
height: MediaQuery.of(context).size.height * 0.5,
|
||||
|
|
@ -99,7 +97,9 @@ class _ChatUserModalState extends State<ChatUserModal> {
|
|||
|
||||
return MediaQuery(
|
||||
data: MediaQuery.of(context).copyWith(
|
||||
textScaler: TextScaler.linear(widget.chatStore.settings.messageScale),
|
||||
textScaler: TextScaler.linear(
|
||||
widget.chatStore.settings.messageScale,
|
||||
),
|
||||
),
|
||||
child: DefaultTextStyle(
|
||||
style: DefaultTextStyle.of(context)
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import 'package:frosty/constants.dart';
|
||||
import 'package:frosty/models/irc.dart';
|
||||
import 'package:frosty/screens/channel/chat/stores/chat_store.dart';
|
||||
import 'package:frosty/screens/channel/chat/widgets/chat_message.dart';
|
||||
import 'package:frosty/utils.dart';
|
||||
import 'package:frosty/widgets/section_header.dart';
|
||||
|
||||
class ReplyThread extends StatelessWidget {
|
||||
|
|
@ -28,9 +28,7 @@ class ReplyThread extends StatelessWidget {
|
|||
final replyUserLogin = selectedMessage.tags['reply-parent-user-login'];
|
||||
final replyBody = selectedMessage.tags['reply-parent-msg-body'];
|
||||
|
||||
final replyName = regexEnglish.hasMatch(replyDisplayName!)
|
||||
? replyDisplayName
|
||||
: '$replyDisplayName ($replyUserLogin)';
|
||||
final replyName = getReadableName(replyDisplayName!, replyUserLogin!);
|
||||
|
||||
return Observer(
|
||||
builder: (context) {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:frosty/constants.dart';
|
||||
import 'package:frosty/models/stream.dart';
|
||||
import 'package:frosty/screens/home/top/categories/category_streams.dart';
|
||||
import 'package:frosty/utils.dart';
|
||||
import 'package:frosty/widgets/profile_picture.dart';
|
||||
|
||||
class VideoBar extends StatelessWidget {
|
||||
|
|
@ -30,9 +30,8 @@ class VideoBar extends StatelessWidget {
|
|||
final category =
|
||||
streamInfo.gameName.isNotEmpty ? streamInfo.gameName : 'No Category';
|
||||
|
||||
final streamerName = regexEnglish.hasMatch(streamInfo.userName)
|
||||
? streamInfo.userName
|
||||
: '${streamInfo.userName} (${streamInfo.userLogin})';
|
||||
final streamerName =
|
||||
getReadableName(streamInfo.userName, streamInfo.userLogin);
|
||||
|
||||
return Padding(
|
||||
padding: padding,
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import 'package:frosty/constants.dart';
|
||||
import 'package:frosty/models/channel.dart';
|
||||
import 'package:frosty/screens/channel/channel.dart';
|
||||
import 'package:frosty/screens/home/search/search_store.dart';
|
||||
import 'package:frosty/utils.dart';
|
||||
import 'package:frosty/widgets/alert_message.dart';
|
||||
import 'package:frosty/widgets/block_report_modal.dart';
|
||||
import 'package:frosty/widgets/loading_indicator.dart';
|
||||
|
|
@ -86,10 +86,10 @@ class _SearchResultsChannelsState extends State<SearchResultsChannels> {
|
|||
[
|
||||
...results.map(
|
||||
(channel) {
|
||||
final displayName = regexEnglish
|
||||
.hasMatch(channel.displayName)
|
||||
? channel.displayName
|
||||
: '${channel.displayName} (${channel.broadcasterLogin})';
|
||||
final displayName = getReadableName(
|
||||
channel.displayName,
|
||||
channel.broadcasterLogin,
|
||||
);
|
||||
|
||||
return InkWell(
|
||||
onTap: () => Navigator.push(
|
||||
|
|
|
|||
|
|
@ -2,11 +2,11 @@ import 'dart:math';
|
|||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:frosty/constants.dart';
|
||||
import 'package:frosty/models/stream.dart';
|
||||
import 'package:frosty/screens/channel/channel.dart';
|
||||
import 'package:frosty/screens/channel/video/video_bar.dart';
|
||||
import 'package:frosty/screens/settings/stores/auth_store.dart';
|
||||
import 'package:frosty/utils.dart';
|
||||
import 'package:frosty/widgets/block_report_modal.dart';
|
||||
import 'package:frosty/widgets/cached_image.dart';
|
||||
import 'package:frosty/widgets/loading_indicator.dart';
|
||||
|
|
@ -129,9 +129,8 @@ class LargeStreamCard extends StatelessWidget {
|
|||
),
|
||||
);
|
||||
|
||||
final streamerName = regexEnglish.hasMatch(streamInfo.userName)
|
||||
? streamInfo.userName
|
||||
: '${streamInfo.userName} (${streamInfo.userLogin})';
|
||||
final streamerName =
|
||||
getReadableName(streamInfo.userName, streamInfo.userLogin);
|
||||
|
||||
return InkWell(
|
||||
onTap: () => Navigator.push(
|
||||
|
|
|
|||
|
|
@ -2,11 +2,11 @@ import 'dart:math';
|
|||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:frosty/constants.dart';
|
||||
import 'package:frosty/models/stream.dart';
|
||||
import 'package:frosty/screens/channel/channel.dart';
|
||||
import 'package:frosty/screens/home/top/categories/category_streams.dart';
|
||||
import 'package:frosty/screens/settings/stores/auth_store.dart';
|
||||
import 'package:frosty/utils.dart';
|
||||
import 'package:frosty/widgets/block_report_modal.dart';
|
||||
import 'package:frosty/widgets/cached_image.dart';
|
||||
import 'package:frosty/widgets/loading_indicator.dart';
|
||||
|
|
@ -60,9 +60,8 @@ class StreamCard extends StatelessWidget {
|
|||
),
|
||||
);
|
||||
|
||||
final streamerName = regexEnglish.hasMatch(streamInfo.userName)
|
||||
? streamInfo.userName
|
||||
: '${streamInfo.userName} (${streamInfo.userLogin})';
|
||||
final streamerName =
|
||||
getReadableName(streamInfo.userName, streamInfo.userLogin);
|
||||
|
||||
const subFontSize = 14.0;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import 'package:frosty/constants.dart';
|
||||
import 'package:frosty/screens/settings/stores/auth_store.dart';
|
||||
import 'package:frosty/utils.dart';
|
||||
import 'package:frosty/widgets/alert_message.dart';
|
||||
|
||||
class BlockedUsers extends StatelessWidget {
|
||||
|
|
@ -34,10 +34,10 @@ class BlockedUsers extends StatelessWidget {
|
|||
return ListView(
|
||||
children: authStore.user.blockedUsers.map(
|
||||
(blockedUser) {
|
||||
final displayName = regexEnglish
|
||||
.hasMatch(blockedUser.displayName)
|
||||
? blockedUser.displayName
|
||||
: '${blockedUser.displayName} (${blockedUser.userLogin})';
|
||||
final displayName = getReadableName(
|
||||
blockedUser.displayName,
|
||||
blockedUser.userLogin,
|
||||
);
|
||||
|
||||
return ListTile(
|
||||
title: Text(displayName),
|
||||
|
|
|
|||
9
lib/utils.dart
Normal file
9
lib/utils.dart
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import 'package:frosty/constants.dart';
|
||||
|
||||
String getReadableName(String displayName, String username) {
|
||||
if (!regexEnglish.hasMatch(displayName)) {
|
||||
return '$displayName ($username)';
|
||||
}
|
||||
|
||||
return displayName;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue