Add hide blocked users in chat (#55)

Blocked users will be hidden from chat only if the logged in user is not
a mod or not the channel owner.
This commit is contained in:
tommyxchow 2021-12-11 00:45:44 -05:00
parent a671a54d0d
commit 81c18feb45
8 changed files with 121 additions and 29 deletions

View file

@ -25,6 +25,10 @@ abstract class _AuthBase with Store {
@readonly
UserTwitch? _user;
/// The user's list of blocked users.
@readonly
var _blockedUsers = ObservableList<UserBlockedTwitch>();
/// Whether the user is logged in or not.
@readonly
var _isLoggedIn = false;
@ -40,7 +44,7 @@ abstract class _AuthBase with Store {
_token = await _storage.read(key: 'USER_TOKEN');
// If the token does not exist, get the default token.
// Otherwise, get the user info and log in.
// Otherwise, log in.
if (_token == null) {
// Retrieve the currently stored default token if it exists.
_token = await _storage.read(key: 'DEFAULT_TOKEN');
@ -50,7 +54,12 @@ abstract class _AuthBase with Store {
await _storage.write(key: 'DEFAULT_TOKEN', value: _token);
}
} else {
// Get and update the current user's info.
_user = await Twitch.getUserInfo(headers: headersTwitch);
// Get and update the current user's list of blocked users.
if (_user?.id != null) _blockedUsers = (await Twitch.getUserBlockedList(id: _user!.id, headers: headersTwitch)).asObservable();
_isLoggedIn = true;
}
@ -77,7 +86,7 @@ abstract class _AuthBase with Store {
'client_id': clientId,
'redirect_uri': 'auth://',
'response_type': 'token',
'scope': 'chat:read chat:edit user:read:follows',
'scope': 'chat:read chat:edit user:read:follows user:read:blocked_users user:manage:blocked_users',
'force_verify': 'true',
},
);
@ -96,6 +105,8 @@ abstract class _AuthBase with Store {
// Retrieve the user info and set it
_user = await Twitch.getUserInfo(headers: headersTwitch);
if (_user?.id != null) _blockedUsers = (await Twitch.getUserBlockedList(id: _user!.id, headers: headersTwitch)).asObservable();
// Set the login status to logged in.
_isLoggedIn = true;
} catch (error) {

View file

@ -51,6 +51,23 @@ mixin _$AuthStore on _AuthBase, Store {
});
}
final _$_blockedUsersAtom = Atom(name: '_AuthBase._blockedUsers');
ObservableList<UserBlockedTwitch> get blockedUsers {
_$_blockedUsersAtom.reportRead();
return super._blockedUsers;
}
@override
ObservableList<UserBlockedTwitch> get _blockedUsers => blockedUsers;
@override
set _blockedUsers(ObservableList<UserBlockedTwitch> value) {
_$_blockedUsersAtom.reportWrite(value, super._blockedUsers, () {
super._blockedUsers = value;
});
}
final _$_isLoggedInAtom = Atom(name: '_AuthBase._isLoggedIn');
bool get isLoggedIn {

View file

@ -3,12 +3,19 @@ import 'package:frosty/api/irc_api.dart';
/// The object representation of a Twitch IRC message.
class IRCMessage {
final String raw;
final Map<String, String> tags;
final String? user;
Command command;
String? message;
IRCMessage({required this.tags, required this.command, required this.user, required this.message});
IRCMessage({
required this.raw,
required this.tags,
required this.command,
required this.user,
required this.message,
});
/// Parses an IRC string and returns its corresponding [IRCMessage] object.
factory IRCMessage.fromString(String whole) {
@ -91,6 +98,7 @@ class IRCMessage {
}
return IRCMessage(
raw: whole,
tags: mappedTags,
command: messageCommand,
user: user,
@ -101,28 +109,50 @@ class IRCMessage {
/// The object representation of the IRC ROOMSTATE message.
class ROOMSTATE {
final String emoteOnly;
final bool emoteOnly;
final String followersOnly;
final String r9k;
final bool r9k;
final String slowMode;
final String subMode;
final bool subMode;
const ROOMSTATE({
this.emoteOnly = "0",
this.followersOnly = "0",
this.r9k = "0",
this.slowMode = "0",
this.subMode = "0",
this.emoteOnly = false,
this.followersOnly = '-1',
this.r9k = false,
this.slowMode = '0',
this.subMode = false,
});
/// Create a new copy with the parameters from the provided [IRCMessage]
ROOMSTATE copyWith(IRCMessage ircMessage) {
return ROOMSTATE(
emoteOnly: ircMessage.tags['emote-only'] ?? emoteOnly,
followersOnly: ircMessage.tags['followers-only'] ?? followersOnly,
r9k: ircMessage.tags['r9k'] ?? r9k,
slowMode: ircMessage.tags['slow'] ?? slowMode,
subMode: ircMessage.tags['subs-only'] ?? subMode,
);
}
ROOMSTATE fromIRC(IRCMessage ircMessage) => ROOMSTATE(
emoteOnly: ircMessage.tags['emote-only'] == '0' ? false : true,
followersOnly: ircMessage.tags['followers-only'] ?? followersOnly,
r9k: ircMessage.tags['r9k'] == '0' ? false : true,
slowMode: ircMessage.tags['slow'] ?? slowMode,
subMode: ircMessage.tags['subs-only'] == '0' ? false : true,
);
}
class USERSTATE {
final String? raw;
final String color;
final String displayName;
final bool mod;
final bool subscriber;
const USERSTATE({
this.raw,
this.color = '',
this.displayName = '',
this.mod = false,
this.subscriber = false,
});
USERSTATE fromIRC(IRCMessage ircMessage) => USERSTATE(
raw: ircMessage.raw,
color: ircMessage.tags['color'] ?? color,
displayName: ircMessage.tags['display-name'] ?? displayName,
mod: ircMessage.tags['mod'] == '0' ? false : true,
subscriber: ircMessage.tags['subscriber'] == '0' ? false : true,
);
}

View file

@ -31,3 +31,18 @@ class UserTwitch {
factory UserTwitch.fromJson(Map<String, dynamic> json) => _$UserTwitchFromJson(json);
}
@JsonSerializable(createToJson: false, fieldRename: FieldRename.snake)
class UserBlockedTwitch {
final String userId;
final String userLogin;
final String displayName;
const UserBlockedTwitch(
this.userId,
this.userLogin,
this.displayName,
);
factory UserBlockedTwitch.fromJson(Map<String, dynamic> json) => _$UserBlockedTwitchFromJson(json);
}

View file

@ -18,3 +18,10 @@ UserTwitch _$UserTwitchFromJson(Map<String, dynamic> json) => UserTwitch(
json['view_count'] as int,
json['created_at'] as String,
);
UserBlockedTwitch _$UserBlockedTwitchFromJson(Map<String, dynamic> json) =>
UserBlockedTwitch(
json['user_id'] as String,
json['user_login'] as String,
json['display_name'] as String,
);

View file

@ -21,7 +21,7 @@ class ChatStats extends StatelessWidget {
),
ListTile(
leading: const Text('Emote-only'),
trailing: Text(chatStore.roomState.emoteOnly == '0' ? 'Disabled' : 'Enabled'),
trailing: Text(chatStore.roomState.emoteOnly ? 'Enabled' : 'Disabled'),
),
ListTile(
leading: const Text('Followers-only'),
@ -33,7 +33,7 @@ class ChatStats extends StatelessWidget {
),
ListTile(
leading: const Text('R9K'),
trailing: Text(chatStore.roomState.r9k == '0' ? 'Disabled' : 'Enabled'),
trailing: Text(chatStore.roomState.r9k ? 'Enabled' : 'Disabled'),
),
ListTile(
leading: const Text('Slow'),
@ -41,7 +41,7 @@ class ChatStats extends StatelessWidget {
),
ListTile(
leading: const Text('Sub-only'),
trailing: Text(chatStore.roomState.subMode == '0' ? 'Disabled' : 'Enabled'),
trailing: Text(chatStore.roomState.subMode ? 'Enabled' : 'Disabled'),
),
],
);

View file

@ -42,7 +42,7 @@ abstract class _ChatStoreBase with Store {
final SettingsStore settings;
/// The logged-in user's appearance in chat.
String? _userState;
var _userState = const USERSTATE();
/// Requested message to be sent by the user. Will only be sent on receival of a USERNOTICE command.
IRCMessage? toSend;
@ -112,6 +112,11 @@ abstract class _ChatStoreBase with Store {
if (message.startsWith('@')) {
final parsedIRCMessage = IRCMessage.fromString(message);
// Filter messages from any blocked users if not moderating and not the channel owner
if (!_userState.mod &&
channelName != auth.user?.login &&
auth.blockedUsers.where((blockedUser) => blockedUser.userLogin == parsedIRCMessage.user).isNotEmpty) continue;
switch (parsedIRCMessage.command) {
case Command.privateMessage:
messages.add(parsedIRCMessage);
@ -127,10 +132,10 @@ abstract class _ChatStoreBase with Store {
messages.add(parsedIRCMessage);
break;
case Command.roomState:
_roomState = _roomState.copyWith(parsedIRCMessage);
_roomState = _roomState.fromIRC(parsedIRCMessage);
continue;
case Command.userState:
_userState = message;
_userState = _userState.fromIRC(parsedIRCMessage);
if (toSend != null) {
messages.add(toSend!);
toSend = null;
@ -197,8 +202,9 @@ abstract class _ChatStoreBase with Store {
_channel.sink.add('PRIVMSG #$channelName :$message');
// Obtain the logged-in user's appearance in chat with USERSTATE and create the full message to render.
if (_userState != null) {
final userChatMessage = IRCMessage.fromString(_userState!);
final userStateString = _userState.raw;
if (userStateString != null) {
final userChatMessage = IRCMessage.fromString(userStateString);
userChatMessage.message = message;
toSend = userChatMessage;
}

View file

@ -85,7 +85,13 @@ class VideoOverlay extends StatelessWidget {
const SizedBox(height: 5.0),
// SingleChildScrollView(scrollDirection: Axis.horizontal, child: Text(title)),
// const SizedBox(height: 5.0),
if (videoStore.streamInfo != null) Text('${NumberFormat().format(videoStore.streamInfo?.viewerCount)} viewers'),
if (videoStore.streamInfo != null)
Text(
'${NumberFormat().format(videoStore.streamInfo?.viewerCount)} viewers',
style: const TextStyle(
color: Color(0xFFFFFFFF),
),
),
],
),
),