diff --git a/lib/apis/seventv_api.dart b/lib/apis/seventv_api.dart index 7a1cc241..68e85846 100644 --- a/lib/apis/seventv_api.dart +++ b/lib/apis/seventv_api.dart @@ -26,18 +26,24 @@ class SevenTVApi { } } - /// Returns a map of a channel's 7TV emotes to their URL. - Future> 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)> 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'); } diff --git a/lib/models/emotes.dart b/lib/models/emotes.dart index c0eb7dcb..adcb32dc 100644 --- a/lib/models/emotes.dart +++ b/lib/models/emotes.dart @@ -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 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, diff --git a/lib/models/emotes.g.dart b/lib/models/emotes.g.dart index 35262539..7185c980 100644 --- a/lib/models/emotes.g.dart +++ b/lib/models/emotes.g.dart @@ -54,7 +54,16 @@ EmoteFFZ _$EmoteFFZFromJson(Map json) => EmoteFFZ( Emote7TV _$Emote7TVFromJson(Map json) => Emote7TV( json['id'] as String, json['name'] as String, - Emote7TVData.fromJson(json['data'] as Map), + json['data'] == null + ? null + : Emote7TVData.fromJson(json['data'] as Map), + ); + +Emote7TVUser _$Emote7TVUserFromJson(Map 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 json) => Emote7TVData( diff --git a/lib/models/events.dart b/lib/models/events.dart new file mode 100644 index 00000000..8414c277 --- /dev/null +++ b/lib/models/events.dart @@ -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 json) => + _$SevenTVEventFromJson(json); + + Map toJson() => _$SevenTVEventToJson(this); +} + +@JsonSerializable() +class SevenTVEventData { + final String? type; + final Map? condition; + @JsonKey(includeToJson: false) + final SevenTVEventEmoteSetBody? body; + + const SevenTVEventData({ + required this.type, + this.condition, + this.body, + }); + + factory SevenTVEventData.fromJson(Map json) => + _$SevenTVEventDataFromJson(json); + + Map 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 json) => + _$SevenTVEventUpdatedEmoteFromJson(json); +} + +@JsonSerializable(createToJson: false) +class SevenTVEventEmoteSetBody { + final Emote7TVUser actor; + final List? pushed; + final List? pulled; + + const SevenTVEventEmoteSetBody({ + required this.actor, + this.pushed, + this.pulled, + }); + + factory SevenTVEventEmoteSetBody.fromJson(Map json) => + _$SevenTVEventEmoteSetBodyFromJson(json); +} diff --git a/lib/models/events.g.dart b/lib/models/events.g.dart new file mode 100644 index 00000000..fd3b8be8 --- /dev/null +++ b/lib/models/events.g.dart @@ -0,0 +1,72 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'events.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +SevenTVEvent _$SevenTVEventFromJson(Map json) => SevenTVEvent( + op: json['op'] as int, + t: json['t'] as int?, + d: SevenTVEventData.fromJson(json['d'] as Map), + ); + +Map _$SevenTVEventToJson(SevenTVEvent instance) { + final val = { + '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 json) => + SevenTVEventData( + type: json['type'] as String?, + condition: (json['condition'] as Map?)?.map( + (k, e) => MapEntry(k, e as String), + ), + body: json['body'] == null + ? null + : SevenTVEventEmoteSetBody.fromJson( + json['body'] as Map), + ); + +Map _$SevenTVEventDataToJson(SevenTVEventData instance) => + { + 'type': instance.type, + 'condition': instance.condition, + }; + +SevenTVEventUpdatedEmote _$SevenTVEventUpdatedEmoteFromJson( + Map json) => + SevenTVEventUpdatedEmote( + value: json['value'] == null + ? null + : Emote7TV.fromJson(json['value'] as Map), + oldValue: json['old_value'] == null + ? null + : Emote7TV.fromJson(json['old_value'] as Map), + ); + +SevenTVEventEmoteSetBody _$SevenTVEventEmoteSetBodyFromJson( + Map json) => + SevenTVEventEmoteSetBody( + actor: Emote7TVUser.fromJson(json['actor'] as Map), + pushed: (json['pushed'] as List?) + ?.map((e) => + SevenTVEventUpdatedEmote.fromJson(e as Map)) + .toList(), + pulled: (json['pulled'] as List?) + ?.map((e) => + SevenTVEventUpdatedEmote.fromJson(e as Map)) + .toList(), + ); diff --git a/lib/models/irc.dart b/lib/models/irc.dart index ad33b757..9758bfff 100644 --- a/lib/models/irc.dart +++ b/lib/models/irc.dart @@ -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, diff --git a/lib/screens/channel/channel.dart b/lib/screens/channel/channel.dart index bc311860..3411f384 100644 --- a/lib/screens/channel/channel.dart +++ b/lib/screens/channel/channel.dart @@ -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 { final appBar = FrostyAppBar( title: Text( - regexEnglish.hasMatch(_chatStore.displayName) - ? _chatStore.displayName - : '${_chatStore.displayName} (${_chatStore.channelName})', + getReadableName(_chatStore.displayName, _chatStore.channelName), ), ); diff --git a/lib/screens/channel/chat/stores/chat_assets_store.dart b/lib/screens/channel/chat/stores/chat_assets_store.dart index d1d8d7e2..95e2cebc 100644 --- a/lib/screens/channel/chat/stores/chat_assets_store.dart +++ b/lib/screens/channel/chat/stores/chat_assets_store.dart @@ -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 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 = {}; + var _emoteToObject = ObservableMap(); /// 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 diff --git a/lib/screens/channel/chat/stores/chat_assets_store.g.dart b/lib/screens/channel/chat/stores/chat_assets_store.g.dart index 322318f5..f3ed0101 100644 --- a/lib/screens/channel/chat/stores/chat_assets_store.g.dart +++ b/lib/screens/channel/chat/stores/chat_assets_store.g.dart @@ -52,16 +52,16 @@ mixin _$ChatAssetsStore on ChatAssetsStoreBase, Store { late final _$_emoteToObjectAtom = Atom(name: 'ChatAssetsStoreBase._emoteToObject', context: context); - Map get emoteToObject { + ObservableMap get emoteToObject { _$_emoteToObjectAtom.reportRead(); return super._emoteToObject; } @override - Map get _emoteToObject => emoteToObject; + ObservableMap get _emoteToObject => emoteToObject; @override - set _emoteToObject(Map value) { + set _emoteToObject(ObservableMap value) { _$_emoteToObjectAtom.reportWrite(value, super._emoteToObject, () { super._emoteToObject = value; }); diff --git a/lib/screens/channel/chat/stores/chat_store.dart b/lib/screens/channel/chat/stores/chat_store.dart index 5833b953..594d6236 100644 --- a/lib/screens/channel/chat/stores/chat_store.dart +++ b/lib/screens/channel/chat/stores/chat_store.dart @@ -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(); diff --git a/lib/screens/channel/chat/stores/chat_store.g.dart b/lib/screens/channel/chat/stores/chat_store.g.dart index 12d8ea9a..066832fd 100644 --- a/lib/screens/channel/chat/stores/chat_store.g.dart +++ b/lib/screens/channel/chat/stores/chat_store.g.dart @@ -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( diff --git a/lib/screens/channel/chat/widgets/chat_user_modal.dart b/lib/screens/channel/chat/widgets/chat_user_modal.dart index a45c5698..75396001 100644 --- a/lib/screens/channel/chat/widgets/chat_user_modal.dart +++ b/lib/screens/channel/chat/widgets/chat_user_modal.dart @@ -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 { @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 { 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) diff --git a/lib/screens/channel/chat/widgets/reply_thread.dart b/lib/screens/channel/chat/widgets/reply_thread.dart index 2b320085..7da9a883 100644 --- a/lib/screens/channel/chat/widgets/reply_thread.dart +++ b/lib/screens/channel/chat/widgets/reply_thread.dart @@ -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) { diff --git a/lib/screens/channel/video/video_bar.dart b/lib/screens/channel/video/video_bar.dart index 16e75484..eee2fad4 100644 --- a/lib/screens/channel/video/video_bar.dart +++ b/lib/screens/channel/video/video_bar.dart @@ -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, diff --git a/lib/screens/home/search/search_results_channels.dart b/lib/screens/home/search/search_results_channels.dart index 7796884e..2804faa8 100644 --- a/lib/screens/home/search/search_results_channels.dart +++ b/lib/screens/home/search/search_results_channels.dart @@ -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 { [ ...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( diff --git a/lib/screens/home/stream_list/large_stream_card.dart b/lib/screens/home/stream_list/large_stream_card.dart index a70e8962..202a360a 100644 --- a/lib/screens/home/stream_list/large_stream_card.dart +++ b/lib/screens/home/stream_list/large_stream_card.dart @@ -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( diff --git a/lib/screens/home/stream_list/stream_card.dart b/lib/screens/home/stream_list/stream_card.dart index 31f311b5..0a2092ab 100644 --- a/lib/screens/home/stream_list/stream_card.dart +++ b/lib/screens/home/stream_list/stream_card.dart @@ -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; diff --git a/lib/screens/settings/account/blocked_users.dart b/lib/screens/settings/account/blocked_users.dart index b16165a0..d7d3b217 100644 --- a/lib/screens/settings/account/blocked_users.dart +++ b/lib/screens/settings/account/blocked_users.dart @@ -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), diff --git a/lib/utils.dart b/lib/utils.dart new file mode 100644 index 00000000..c9f8567b --- /dev/null +++ b/lib/utils.dart @@ -0,0 +1,9 @@ +import 'package:frosty/constants.dart'; + +String getReadableName(String displayName, String username) { + if (!regexEnglish.hasMatch(displayName)) { + return '$displayName ($username)'; + } + + return displayName; +}