Add chat users/modes, refactor chat store

- Added ability to view all chat users in channel
- Updated chat modes to icons for better appearence
- Refactored chat into individual chat stores for better organization
- Fixed ROOMSTATE changes not updating properly
This commit is contained in:
Tommy Chow 2021-12-27 21:13:46 -05:00
parent 0185b80454
commit 8c77eac1e4
14 changed files with 554 additions and 297 deletions

View file

@ -53,7 +53,6 @@ class IRCMessage {
/// Returns a list of messages where the gievn CLEARMSG message is applied.
static List<IRCMessage> clearMessage({required List<IRCMessage> messages, required IRCMessage ircMessage}) {
// final targetUser = ircMessage.tags['login'];
final targetId = ircMessage.tags['target-msg-id'];
// Search for the message associated with the ID and mark the message for deletion.
@ -402,27 +401,27 @@ class IRCMessage {
/// The object representation of the IRC ROOMSTATE message.
class ROOMSTATE {
final bool emoteOnly;
final String emoteOnly;
final String followersOnly;
final bool r9k;
final String r9k;
final String slowMode;
final bool subMode;
final String subMode;
const ROOMSTATE({
this.emoteOnly = false,
this.emoteOnly = '0',
this.followersOnly = '-1',
this.r9k = false,
this.r9k = '0',
this.slowMode = '0',
this.subMode = false,
this.subMode = '0',
});
/// Create a new copy with the parameters from the provided [IRCMessage]
ROOMSTATE fromIRCMessage(IRCMessage ircMessage) => ROOMSTATE(
emoteOnly: ircMessage.tags['emote-only'] == '0' ? false : true,
emoteOnly: ircMessage.tags['emote-only'] ?? emoteOnly,
followersOnly: ircMessage.tags['followers-only'] ?? followersOnly,
r9k: ircMessage.tags['r9k'] == '0' ? false : true,
r9k: ircMessage.tags['r9k'] ?? r9k,
slowMode: ircMessage.tags['slow'] ?? slowMode,
subMode: ircMessage.tags['subs-only'] == '0' ? false : true,
subMode: ircMessage.tags['subs-only'] ?? subMode,
);
}

View file

@ -45,7 +45,7 @@ class _ChatState extends State<Chat> with WidgetsBindingObserver {
onTap: () {
// If tapping chat, hide the keyboard and emote menu.
FocusManager.instance.primaryFocus?.unfocus();
if (chatStore.showEmoteMenu) chatStore.showEmoteMenu = false;
if (chatStore.assetsStore.showEmoteMenu) chatStore.assetsStore.showEmoteMenu = false;
},
child: Observer(
builder: (context) => ListView.separated(
@ -58,7 +58,7 @@ class _ChatState extends State<Chat> with WidgetsBindingObserver {
itemBuilder: (context, index) => Observer(
builder: (context) => ChatMessage(
ircMessage: chatStore.messages[index],
chatStore: chatStore,
assetsStore: chatStore.assetsStore,
hideMessageIfBanned: context.read<SettingsStore>().hideBannedMessages,
zeroWidth: context.read<SettingsStore>().zeroWidthEnabled,
),
@ -83,7 +83,13 @@ class _ChatState extends State<Chat> with WidgetsBindingObserver {
),
),
if (context.read<AuthStore>().isLoggedIn) ChatBottomBar(chatStore: chatStore),
if (chatStore.showEmoteMenu) Expanded(child: EmoteMenu(chatStore: chatStore)),
if (chatStore.assetsStore.showEmoteMenu)
Expanded(
child: EmoteMenu(
assetsStore: chatStore.assetsStore,
textController: chatStore.textController,
),
),
],
),
);

View file

@ -0,0 +1,101 @@
import 'package:frosty/api/bttv_api.dart';
import 'package:frosty/api/ffz_api.dart';
import 'package:frosty/api/seventv_api.dart';
import 'package:frosty/api/twitch_api.dart';
import 'package:frosty/models/badges.dart';
import 'package:frosty/models/emotes.dart';
import 'package:mobx/mobx.dart';
part 'chat_assets_store.g.dart';
class ChatAssetsStore = _ChatAssetsStoreBase with _$ChatAssetsStore;
abstract class _ChatAssetsStoreBase with Store {
/// The map of emote words to their image or GIF URL. May be used by anyone in the chat.
final emoteToObject = ObservableMap<String, Emote>();
@computed
List<Emote> get bttvEmotes => emoteToObject.values.where((emote) => isBTTV(emote)).toList();
@computed
List<Emote> get ffzEmotes => emoteToObject.values.where((emote) => isFFZ(emote)).toList();
@computed
List<Emote> get sevenTvEmotes => emoteToObject.values.where((emote) => is7TV(emote)).toList();
/// The emotes that are "owned" and may be used by the current user.
@readonly
var _userEmoteToObject = ObservableMap<String, Emote>();
/// The map of badges ids to their object representation.
final badgesToObject = <String, BadgeInfoTwitch>{};
/// The current index of the emote menu stack.
@observable
var emoteMenuIndex = 0;
/// Whether or not the emote menu is visible.
@observable
var showEmoteMenu = false;
bool isBTTV(Emote emote) {
return emote.type == EmoteType.bttvChannel || emote.type == EmoteType.bttvGlobal || emote.type == EmoteType.bttvShared;
}
bool isFFZ(Emote emote) {
return emote.type == EmoteType.ffzChannel || emote.type == EmoteType.ffzGlobal;
}
bool is7TV(Emote emote) {
return emote.type == EmoteType.sevenTvChannel || emote.type == EmoteType.sevenTvGlobal;
}
/// Fetches global and channel assets (badges and emotes) and stores them in [_emoteToUrl]
@action
Future<void> getAssets({required String channelName, required Map<String, String> headers}) async {
// Fetch the desired channel/user's information.
final channelInfo = await Twitch.getUser(userLogin: channelName, headers: headers);
if (channelInfo != null) {
// Fetch the global and channel's assets (emotes & badges).
// Async awaits are placed in a list so they are performed in parallel.
final assets = [
...await FFZ.getEmotesGlobal(),
...await FFZ.getEmotesChannel(id: channelInfo.id),
...await BTTV.getEmotesGlobal(),
...await BTTV.getEmotesChannel(id: channelInfo.id),
...await Twitch.getEmotesGlobal(headers: headers),
...await Twitch.getEmotesChannel(id: channelInfo.id, headers: headers),
...await SevenTV.getEmotesGlobal(),
...await SevenTV.getEmotesChannel(user: channelInfo.login)
];
for (final emote in assets) {
emoteToObject[emote.name] = emote;
}
final badges = [
await Twitch.getBadgesGlobal(),
await Twitch.getBadgesChannel(id: channelInfo.id),
];
for (final map in badges) {
if (map != null) {
badgesToObject.addAll(map);
}
}
}
}
@action
Future<void> getUserEmotes({List<String>? emoteSets, required Map<String, String> headers}) async {
if (emoteSets != null) {
final userEmotes = <Emote>[];
for (final setId in emoteSets) {
userEmotes.addAll(await Twitch.getEmotesSets(setId: setId, headers: headers));
}
_userEmoteToObject = {for (final emote in userEmotes) emote.name: emote}.asObservable();
}
}
}

View file

@ -0,0 +1,112 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'chat_assets_store.dart';
// **************************************************************************
// StoreGenerator
// **************************************************************************
// ignore_for_file: non_constant_identifier_names, unnecessary_brace_in_string_interps, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic
mixin _$ChatAssetsStore on _ChatAssetsStoreBase, Store {
Computed<List<Emote>>? _$bttvEmotesComputed;
@override
List<Emote> get bttvEmotes =>
(_$bttvEmotesComputed ??= Computed<List<Emote>>(() => super.bttvEmotes,
name: '_ChatAssetsStoreBase.bttvEmotes'))
.value;
Computed<List<Emote>>? _$ffzEmotesComputed;
@override
List<Emote> get ffzEmotes =>
(_$ffzEmotesComputed ??= Computed<List<Emote>>(() => super.ffzEmotes,
name: '_ChatAssetsStoreBase.ffzEmotes'))
.value;
Computed<List<Emote>>? _$sevenTvEmotesComputed;
@override
List<Emote> get sevenTvEmotes => (_$sevenTvEmotesComputed ??=
Computed<List<Emote>>(() => super.sevenTvEmotes,
name: '_ChatAssetsStoreBase.sevenTvEmotes'))
.value;
final _$_userEmoteToObjectAtom =
Atom(name: '_ChatAssetsStoreBase._userEmoteToObject');
ObservableMap<String, Emote> get userEmoteToObject {
_$_userEmoteToObjectAtom.reportRead();
return super._userEmoteToObject;
}
@override
ObservableMap<String, Emote> get _userEmoteToObject => userEmoteToObject;
@override
set _userEmoteToObject(ObservableMap<String, Emote> value) {
_$_userEmoteToObjectAtom.reportWrite(value, super._userEmoteToObject, () {
super._userEmoteToObject = value;
});
}
final _$emoteMenuIndexAtom =
Atom(name: '_ChatAssetsStoreBase.emoteMenuIndex');
@override
int get emoteMenuIndex {
_$emoteMenuIndexAtom.reportRead();
return super.emoteMenuIndex;
}
@override
set emoteMenuIndex(int value) {
_$emoteMenuIndexAtom.reportWrite(value, super.emoteMenuIndex, () {
super.emoteMenuIndex = value;
});
}
final _$showEmoteMenuAtom = Atom(name: '_ChatAssetsStoreBase.showEmoteMenu');
@override
bool get showEmoteMenu {
_$showEmoteMenuAtom.reportRead();
return super.showEmoteMenu;
}
@override
set showEmoteMenu(bool value) {
_$showEmoteMenuAtom.reportWrite(value, super.showEmoteMenu, () {
super.showEmoteMenu = value;
});
}
final _$getAssetsAsyncAction = AsyncAction('_ChatAssetsStoreBase.getAssets');
@override
Future<void> getAssets(
{required String channelName, required Map<String, String> headers}) {
return _$getAssetsAsyncAction
.run(() => super.getAssets(channelName: channelName, headers: headers));
}
final _$getUserEmotesAsyncAction =
AsyncAction('_ChatAssetsStoreBase.getUserEmotes');
@override
Future<void> getUserEmotes(
{List<String>? emoteSets, required Map<String, String> headers}) {
return _$getUserEmotesAsyncAction
.run(() => super.getUserEmotes(emoteSets: emoteSets, headers: headers));
}
@override
String toString() {
return '''
emoteMenuIndex: ${emoteMenuIndex},
showEmoteMenu: ${showEmoteMenu},
bttvEmotes: ${bttvEmotes},
ffzEmotes: ${ffzEmotes},
sevenTvEmotes: ${sevenTvEmotes}
''';
}
}

View file

@ -15,7 +15,10 @@ class ChatBottomBar extends StatelessWidget {
icon: Icon(Icons.adaptive.more),
onPressed: () => showModalBottomSheet(
context: context,
builder: (_) => ChatDetails(chatStore: chatStore),
builder: (_) => ChatDetails(
chatDetails: chatStore.chatDetailsStore,
userLogin: chatStore.channelName,
),
),
),
Expanded(
@ -24,13 +27,13 @@ class ChatBottomBar extends StatelessWidget {
child: TextField(
minLines: 1,
maxLines: 5,
onTap: () => chatStore.showEmoteMenu = false,
onTap: () => chatStore.assetsStore.showEmoteMenu = false,
decoration: InputDecoration(
suffixIcon: IconButton(
icon: const Icon(Icons.emoji_emotions_outlined),
onPressed: () {
FocusManager.instance.primaryFocus?.unfocus();
chatStore.showEmoteMenu = !chatStore.showEmoteMenu;
chatStore.assetsStore.showEmoteMenu = !chatStore.assetsStore.showEmoteMenu;
},
),
isDense: true,

View file

@ -1,17 +1,17 @@
import 'package:flutter/material.dart';
import 'package:frosty/models/irc_message.dart';
import 'package:frosty/screens/channel/chat/chat_store.dart';
import 'package:frosty/screens/channel/chat/chat_assets_store.dart';
class ChatMessage extends StatelessWidget {
final IRCMessage ircMessage;
final ChatStore chatStore;
final ChatAssetsStore assetsStore;
final bool hideMessageIfBanned;
final bool zeroWidth;
const ChatMessage({
Key? key,
required this.ircMessage,
required this.chatStore,
required this.assetsStore,
this.hideMessageIfBanned = true,
this.zeroWidth = false,
}) : super(key: key);
@ -25,8 +25,8 @@ class ChatMessage extends StatelessWidget {
return Text.rich(
TextSpan(
children: ircMessage.generateSpan(
emoteToObject: chatStore.emoteToObject,
badgeToObject: chatStore.badgesToObject,
emoteToObject: assetsStore.emoteToObject,
badgeToObject: assetsStore.badgesToObject,
zeroWidthEnabled: zeroWidth,
),
),
@ -43,8 +43,8 @@ class ChatMessage extends StatelessWidget {
Text.rich(
TextSpan(
children: ircMessage.generateSpan(
emoteToObject: chatStore.emoteToObject,
badgeToObject: chatStore.badgesToObject,
emoteToObject: assetsStore.emoteToObject,
badgeToObject: assetsStore.badgesToObject,
hideMessage: hideMessageIfBanned,
zeroWidthEnabled: zeroWidth,
),
@ -85,8 +85,8 @@ class ChatMessage extends StatelessWidget {
Text.rich(
TextSpan(
children: ircMessage.generateSpan(
emoteToObject: chatStore.emoteToObject,
badgeToObject: chatStore.badgesToObject,
emoteToObject: assetsStore.emoteToObject,
badgeToObject: assetsStore.badgesToObject,
zeroWidthEnabled: zeroWidth,
),
),

View file

@ -2,15 +2,11 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:frosty/api/bttv_api.dart';
import 'package:frosty/api/ffz_api.dart';
import 'package:frosty/api/seventv_api.dart';
import 'package:frosty/api/twitch_api.dart';
import 'package:frosty/core/auth/auth_store.dart';
import 'package:frosty/core/settings/settings_store.dart';
import 'package:frosty/models/badges.dart';
import 'package:frosty/models/emotes.dart';
import 'package:frosty/models/irc_message.dart';
import 'package:frosty/screens/channel/chat/chat_assets_store.dart';
import 'package:frosty/screens/channel/chat/details/chat_details_store.dart';
import 'package:mobx/mobx.dart';
import 'package:web_socket_channel/web_socket_channel.dart';
@ -20,54 +16,33 @@ part 'chat_store.g.dart';
class ChatStore = _ChatStoreBase with _$ChatStore;
abstract class _ChatStoreBase with Store {
/// The provided auth store to determine login status, get the token, and use the headers for requests.
final AuthStore auth;
/// The provided setting store to account for any user-defined behaviors.
final SettingsStore settings;
/// The name of the channel to connect to.
final String channelName;
/// The Twitch IRC WebSocket channel.
final _channel = WebSocketChannel.connect(Uri.parse('wss://irc-ws.chat.twitch.tv:443'));
/// The map of emote words to their image or GIF URL. May be used by anyone in the chat.
final emoteToObject = ObservableMap<String, Emote>();
@computed
List<Emote> get bttvEmotes => emoteToObject.values.where((emote) => isBTTV(emote)).toList();
@computed
List<Emote> get ffzEmotes => emoteToObject.values.where((emote) => isFFZ(emote)).toList();
@computed
List<Emote> get sevenTvEmotes => emoteToObject.values.where((emote) => is7TV(emote)).toList();
/// The emotes that are "owned" and may be used by the current user.
@readonly
var _userEmoteToObject = ObservableMap<String, Emote>();
/// The map of badges ids to their object representation.
final badgesToObject = <String, BadgeInfoTwitch>{};
/// The scroll controller that controls auto-scroll and resume-scroll behavior.
final scrollController = ScrollController();
/// The text controller that handles the TextField inputs and sending of messages.
final textController = TextEditingController();
/// The name of the channel to connect to.
final String channelName;
/// The chat details store responsible for the chat modes and users in chat.
final chatDetailsStore = ChatDetailsStore();
/// The provided auth store to determine login status, get the token, and use the headers for requests.
final AuthStore auth;
/// The provided setting store to account for any user-defined behaviors.
final SettingsStore settings;
/// The assets store responsible for badges, emotes, and the emote menu.
final assetsStore = ChatAssetsStore();
/// Requested message to be sent by the user. Will only be sent on receival of a USERNOTICE command.
IRCMessage? toSend;
/// The current index of the emote menu stack.
@observable
var emoteMenuIndex = 0;
/// Whether or not the emote menu is visible.
@observable
var showEmoteMenu = false;
/// The list of chat messages to render and display.
@readonly
var _messages = ObservableList<IRCMessage>();
@ -80,10 +55,6 @@ abstract class _ChatStoreBase with Store {
@readonly
var _userState = const USERSTATE();
/// The rules and modes being used in the chat.
@readonly
var _roomState = const ROOMSTATE();
_ChatStoreBase({
required this.auth,
required this.settings,
@ -92,7 +63,7 @@ abstract class _ChatStoreBase with Store {
// Create a reaction where anytime the emote menu is shown or hidden,
// scroll to the bottom of the list. This will prevent the emote menu
// from covering the latest messages when summoned.
final disposeEmoteMenuReaction = reaction((_) => showEmoteMenu, (_) {
final disposeEmoteMenuReaction = reaction((_) => assetsStore.showEmoteMenu, (_) {
SchedulerBinding.instance?.addPostFrameCallback((_) {
if (scrollController.hasClients) scrollController.jumpTo(scrollController.position.maxScrollExtent);
});
@ -115,8 +86,16 @@ abstract class _ChatStoreBase with Store {
},
);
// Fetch the users in chat.
chatDetailsStore.updateChatters(channelName);
// Fetch the assets used in chat including badges and emotes.
getAssets();
_messages.add(IRCMessage.createNotice(message: 'Fetching channel assets...'));
assetsStore
.getAssets(channelName: channelName, headers: auth.headersTwitch)
.then((_) => _messages.add(IRCMessage.createNotice(message: 'Channel assets fetched!')))
.onError((error, stackTrace) => _messages.add(IRCMessage.createNotice(message: 'Failed to fetch assets: ${error.toString()}')));
// The list of messages sent to the IRC WebSocket channel to connect and join.
final commands = [
@ -185,7 +164,7 @@ abstract class _ChatStoreBase with Store {
_messages.add(parsedIRCMessage);
break;
case Command.roomState:
_roomState = _roomState.fromIRCMessage(parsedIRCMessage);
chatDetailsStore.roomState = chatDetailsStore.roomState.fromIRCMessage(parsedIRCMessage);
continue;
case Command.userState:
_userState = _userState.fromIRCMessage(parsedIRCMessage);
@ -196,7 +175,11 @@ abstract class _ChatStoreBase with Store {
break;
case Command.globalUserState:
final setIds = parsedIRCMessage.tags['emote-sets']?.split(',');
getUserEmotes(emoteSets: setIds);
_messages.add(IRCMessage.createNotice(message: 'Fetching user emotes...'));
assetsStore
.getUserEmotes(emoteSets: setIds, headers: auth.headersTwitch)
.then((_) => _messages.add(IRCMessage.createNotice(message: 'User emotes fetched!')))
.onError((error, stackTrace) => _messages.add(IRCMessage.createNotice(message: 'Failed to fetch user emotes: ${error.toString()}')));
continue;
case Command.none:
debugPrint('Unknown command: ${parsedIRCMessage.command}');
@ -262,7 +245,7 @@ abstract class _ChatStoreBase with Store {
// }
final userChatMessage = IRCMessage.fromString(userStateString);
userChatMessage.localEmotes.addAll(_userEmoteToObject);
userChatMessage.localEmotes.addAll(assetsStore.userEmoteToObject);
userChatMessage.message = message.trim();
toSend = userChatMessage;
}
@ -271,65 +254,6 @@ abstract class _ChatStoreBase with Store {
textController.clear();
}
/// Fetches global and channel assets (badges and emotes) and stores them in [_emoteToUrl]
@action
Future<void> getAssets() async {
_messages.add(IRCMessage.createNotice(message: 'Fetching channel assets...'));
// Fetch the desired channel/user's information.
final channelInfo = await Twitch.getUser(userLogin: channelName, headers: auth.headersTwitch);
if (channelInfo != null) {
// Fetch the global and channel's assets (emotes & badges).
// Async awaits are placed in a list so they are performed in parallel.
final assets = [
...await FFZ.getEmotesGlobal(),
...await FFZ.getEmotesChannel(id: channelInfo.id),
...await BTTV.getEmotesGlobal(),
...await BTTV.getEmotesChannel(id: channelInfo.id),
...await Twitch.getEmotesGlobal(headers: auth.headersTwitch),
...await Twitch.getEmotesChannel(id: channelInfo.id, headers: auth.headersTwitch),
...await SevenTV.getEmotesGlobal(),
...await SevenTV.getEmotesChannel(user: channelInfo.login)
];
// assets.sort((a, b) => a.id.compareTo(b.id));
for (final emote in assets) {
emoteToObject[emote.name] = emote;
}
final badges = [
await Twitch.getBadgesGlobal(),
await Twitch.getBadgesChannel(id: channelInfo.id),
];
for (final map in badges) {
if (map != null) {
badgesToObject.addAll(map);
}
}
_messages.add(IRCMessage.createNotice(message: 'Channel assets fetched!'));
}
}
@action
Future<void> getUserEmotes({List<String>? emoteSets}) async {
_messages.add(IRCMessage.createNotice(message: 'Fetching user emotes...'));
if (emoteSets != null) {
final userEmotes = <Emote>[];
for (final setId in emoteSets) {
userEmotes.addAll(await Twitch.getEmotesSets(setId: setId, headers: auth.headersTwitch));
}
_userEmoteToObject = {for (final emote in userEmotes) emote.name: emote}.asObservable();
}
_messages.add(IRCMessage.createNotice(message: 'User emotes fetched!'));
}
/// Pauses or resumes the chat subscription depending on the provided state.
void handleAppStateChange(AppLifecycleState state) {
switch (state) {
@ -344,18 +268,6 @@ abstract class _ChatStoreBase with Store {
}
}
bool isBTTV(Emote emote) {
return emote.type == EmoteType.bttvChannel || emote.type == EmoteType.bttvGlobal || emote.type == EmoteType.bttvShared;
}
bool isFFZ(Emote emote) {
return emote.type == EmoteType.ffzChannel || emote.type == EmoteType.ffzGlobal;
}
bool is7TV(Emote emote) {
return emote.type == EmoteType.sevenTvChannel || emote.type == EmoteType.sevenTvGlobal;
}
/// Closes and disposes all the channels and controllers used by the store.
void dispose() {
_channel.sink.close();

View file

@ -9,76 +9,6 @@ part of 'chat_store.dart';
// ignore_for_file: non_constant_identifier_names, unnecessary_brace_in_string_interps, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic
mixin _$ChatStore on _ChatStoreBase, Store {
Computed<List<Emote>>? _$bttvEmotesComputed;
@override
List<Emote> get bttvEmotes =>
(_$bttvEmotesComputed ??= Computed<List<Emote>>(() => super.bttvEmotes,
name: '_ChatStoreBase.bttvEmotes'))
.value;
Computed<List<Emote>>? _$ffzEmotesComputed;
@override
List<Emote> get ffzEmotes =>
(_$ffzEmotesComputed ??= Computed<List<Emote>>(() => super.ffzEmotes,
name: '_ChatStoreBase.ffzEmotes'))
.value;
Computed<List<Emote>>? _$sevenTvEmotesComputed;
@override
List<Emote> get sevenTvEmotes => (_$sevenTvEmotesComputed ??=
Computed<List<Emote>>(() => super.sevenTvEmotes,
name: '_ChatStoreBase.sevenTvEmotes'))
.value;
final _$_userEmoteToObjectAtom =
Atom(name: '_ChatStoreBase._userEmoteToObject');
ObservableMap<String, Emote> get userEmoteToObject {
_$_userEmoteToObjectAtom.reportRead();
return super._userEmoteToObject;
}
@override
ObservableMap<String, Emote> get _userEmoteToObject => userEmoteToObject;
@override
set _userEmoteToObject(ObservableMap<String, Emote> value) {
_$_userEmoteToObjectAtom.reportWrite(value, super._userEmoteToObject, () {
super._userEmoteToObject = value;
});
}
final _$emoteMenuIndexAtom = Atom(name: '_ChatStoreBase.emoteMenuIndex');
@override
int get emoteMenuIndex {
_$emoteMenuIndexAtom.reportRead();
return super.emoteMenuIndex;
}
@override
set emoteMenuIndex(int value) {
_$emoteMenuIndexAtom.reportWrite(value, super.emoteMenuIndex, () {
super.emoteMenuIndex = value;
});
}
final _$showEmoteMenuAtom = Atom(name: '_ChatStoreBase.showEmoteMenu');
@override
bool get showEmoteMenu {
_$showEmoteMenuAtom.reportRead();
return super.showEmoteMenu;
}
@override
set showEmoteMenu(bool value) {
_$showEmoteMenuAtom.reportWrite(value, super.showEmoteMenu, () {
super.showEmoteMenu = value;
});
}
final _$_messagesAtom = Atom(name: '_ChatStoreBase._messages');
ObservableList<IRCMessage> get messages {
@ -130,39 +60,6 @@ mixin _$ChatStore on _ChatStoreBase, Store {
});
}
final _$_roomStateAtom = Atom(name: '_ChatStoreBase._roomState');
ROOMSTATE get roomState {
_$_roomStateAtom.reportRead();
return super._roomState;
}
@override
ROOMSTATE get _roomState => roomState;
@override
set _roomState(ROOMSTATE value) {
_$_roomStateAtom.reportWrite(value, super._roomState, () {
super._roomState = value;
});
}
final _$getAssetsAsyncAction = AsyncAction('_ChatStoreBase.getAssets');
@override
Future<void> getAssets() {
return _$getAssetsAsyncAction.run(() => super.getAssets());
}
final _$getUserEmotesAsyncAction =
AsyncAction('_ChatStoreBase.getUserEmotes');
@override
Future<void> getUserEmotes({List<String>? emoteSets}) {
return _$getUserEmotesAsyncAction
.run(() => super.getUserEmotes(emoteSets: emoteSets));
}
final _$_ChatStoreBaseActionController =
ActionController(name: '_ChatStoreBase');
@ -202,11 +99,7 @@ mixin _$ChatStore on _ChatStoreBase, Store {
@override
String toString() {
return '''
emoteMenuIndex: ${emoteMenuIndex},
showEmoteMenu: ${showEmoteMenu},
bttvEmotes: ${bttvEmotes},
ffzEmotes: ${ffzEmotes},
sevenTvEmotes: ${sevenTvEmotes}
''';
}
}

View file

@ -1,11 +1,18 @@
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:frosty/screens/channel/chat/chat_store.dart';
import 'package:frosty/screens/channel/chat/details/chat_details_store.dart';
import 'package:frosty/screens/channel/chat/details/chat_modes.dart';
import 'package:frosty/screens/channel/chat/details/chat_users_list.dart';
class ChatDetails extends StatelessWidget {
final ChatStore chatStore;
final ChatDetailsStore chatDetails;
final String userLogin;
const ChatDetails({Key? key, required this.chatStore}) : super(key: key);
const ChatDetails({
Key? key,
required this.chatDetails,
required this.userLogin,
}) : super(key: key);
@override
Widget build(BuildContext context) {
@ -14,35 +21,20 @@ class ChatDetails extends StatelessWidget {
child: Center(
child: Observer(
builder: (_) {
return ListView(
return Column(
children: [
const ListTile(
title: Text('Modes'),
),
ListTile(
leading: const Text('Emote-only'),
trailing: Text(chatStore.roomState.emoteOnly ? 'Enabled' : 'Disabled'),
),
ListTile(
leading: const Text('Followers-only'),
trailing: Text(chatStore.roomState.followersOnly == '-1'
? 'Disabled'
: chatStore.roomState.followersOnly == '0'
? 'Enabled'
: '${chatStore.roomState.followersOnly} minute(s)'),
),
ListTile(
leading: const Text('R9K'),
trailing: Text(chatStore.roomState.r9k ? 'Enabled' : 'Disabled'),
),
ListTile(
leading: const Text('Slow'),
trailing: Text(chatStore.roomState.slowMode == '0' ? 'Disabled' : '${chatStore.roomState.slowMode} seconds'),
),
ListTile(
leading: const Text('Sub-only'),
trailing: Text(chatStore.roomState.subMode ? 'Enabled' : 'Disabled'),
Padding(
padding: const EdgeInsets.all(10.0),
child: ChatModes(roomState: chatDetails.roomState),
),
Expanded(
child: RefreshIndicator(
onRefresh: () => chatDetails.updateChatters(userLogin),
child: ChattersList(
chatUsers: chatDetails.chatters,
),
),
)
],
);
},

View file

@ -0,0 +1,23 @@
import 'package:frosty/api/twitch_api.dart';
import 'package:frosty/models/chatters.dart';
import 'package:frosty/models/irc_message.dart';
import 'package:mobx/mobx.dart';
part 'chat_details_store.g.dart';
class ChatDetailsStore = _ChatDetailsStoreBase with _$ChatDetailsStore;
abstract class _ChatDetailsStoreBase with Store {
/// The rules and modes being used in the chat.
@observable
var roomState = const ROOMSTATE();
/// The list and types of chatters in the chat room.
@readonly
ChatUsers? _chatters;
@action
Future<void> updateChatters(String userLogin) async {
_chatters = await Twitch.getChatters(userLogin: userLogin);
}
}

View file

@ -0,0 +1,59 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'chat_details_store.dart';
// **************************************************************************
// StoreGenerator
// **************************************************************************
// ignore_for_file: non_constant_identifier_names, unnecessary_brace_in_string_interps, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic
mixin _$ChatDetailsStore on _ChatDetailsStoreBase, Store {
final _$roomStateAtom = Atom(name: '_ChatDetailsStoreBase.roomState');
@override
ROOMSTATE get roomState {
_$roomStateAtom.reportRead();
return super.roomState;
}
@override
set roomState(ROOMSTATE value) {
_$roomStateAtom.reportWrite(value, super.roomState, () {
super.roomState = value;
});
}
final _$_chattersAtom = Atom(name: '_ChatDetailsStoreBase._chatters');
ChatUsers? get chatters {
_$_chattersAtom.reportRead();
return super._chatters;
}
@override
ChatUsers? get _chatters => chatters;
@override
set _chatters(ChatUsers? value) {
_$_chattersAtom.reportWrite(value, super._chatters, () {
super._chatters = value;
});
}
final _$updateChattersAsyncAction =
AsyncAction('_ChatDetailsStoreBase.updateChatters');
@override
Future<void> updateChatters(String userLogin) {
return _$updateChattersAsyncAction
.run(() => super.updateChatters(userLogin));
}
@override
String toString() {
return '''
roomState: ${roomState}
''';
}
}

View file

@ -0,0 +1,63 @@
import 'package:flutter/material.dart';
import 'package:frosty/models/irc_message.dart';
class ChatModes extends StatelessWidget {
final ROOMSTATE roomState;
const ChatModes({Key? key, required this.roomState}) : super(key: key);
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Tooltip(
preferBelow: false,
message: 'Emote-Only: ${roomState.emoteOnly != '0' ? 'Enabled' : 'Disabled'}',
child: Icon(
Icons.emoji_emotions_outlined,
color: roomState.emoteOnly != '0' ? Colors.green : Colors.grey,
),
),
Tooltip(
preferBelow: false,
message: roomState.followersOnly == '-1'
? 'Followers-Only: Disabled'
: roomState.followersOnly == '0'
? 'Followers-Only: Only followed users can chat.'
: roomState.followersOnly == '1'
? 'Followers-Only: Users followed for at least ${roomState.followersOnly} minute can chat.'
: 'Followers-Only: Users followed for at least ${roomState.followersOnly} minutes can chat',
child: Icon(
Icons.favorite,
color: roomState.followersOnly != '-1' ? Colors.green : Colors.grey,
),
),
Tooltip(
preferBelow: false,
message: 'Unique Chat Messages: ${roomState.r9k != '0' ? 'Enabled' : 'Disabled'}',
child: Text(
'R9K',
style: TextStyle(color: roomState.r9k != '0' ? Colors.green : Colors.grey),
),
),
Tooltip(
preferBelow: false,
message: 'Slow: ${roomState.slowMode == '0' ? 'Disabled' : '${roomState.slowMode} seconds'}',
child: Icon(
Icons.history_toggle_off,
color: roomState.slowMode != '0' ? Colors.green : Colors.grey,
),
),
Tooltip(
preferBelow: false,
message: 'Subs-Only: ${roomState.subMode != '0' ? 'Enabled' : 'Disabled'}',
child: Icon(
Icons.attach_money,
color: roomState.subMode != '0' ? Colors.green : Colors.grey,
),
),
],
);
}
}

View file

@ -0,0 +1,93 @@
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:frosty/models/chatters.dart';
import 'package:intl/intl.dart';
class ChattersList extends StatelessWidget {
final ChatUsers? chatUsers;
const ChattersList({Key? key, required this.chatUsers}) : super(key: key);
@override
Widget build(BuildContext context) {
final chatters = chatUsers?.chatters;
if (chatters != null) {
const headers = [
'Moderators',
'VIPs',
'Staff',
'Admins',
'Global Moderators',
'Users',
];
final userTypes = [
chatters.moderators,
chatters.vips,
chatters.staff,
chatters.admins,
chatters.globalMods,
chatters.viewers,
];
const textStyle = TextStyle(fontWeight: FontWeight.bold);
return CustomScrollView(slivers: [
SliverPadding(
padding: const EdgeInsets.only(left: 10.0, right: 10.0, top: 10.0),
sliver: SliverToBoxAdapter(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Users', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20)),
Text(
'${NumberFormat().format(chatUsers?.chatterCount)} users in chat',
style: const TextStyle(fontStyle: FontStyle.italic),
),
],
),
),
),
SliverPadding(
padding: const EdgeInsets.only(left: 10.0, right: 10.0, top: 20.0),
sliver: SliverToBoxAdapter(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Broadcaster',
style: textStyle,
),
const SizedBox(height: 5.0),
if (chatters.broadcaster.isNotEmpty) Text(chatters.broadcaster.first),
],
),
),
),
...userTypes.expandIndexed((index, type) => [
if (type.isNotEmpty) ...[
SliverPadding(
padding: const EdgeInsets.only(left: 10.0, right: 10.0, top: 20.0),
sliver: SliverToBoxAdapter(
child: Text(
headers[index],
style: textStyle,
),
),
),
SliverPadding(
padding: const EdgeInsets.only(left: 10.0, right: 10.0, top: 5.0),
sliver: SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) => Text(type[index]),
childCount: type.length,
),
),
),
]
])
]);
}
return const Center(child: Text('Failed to get chatters :('));
}
}

View file

@ -1,13 +1,14 @@
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:frosty/screens/channel/chat/chat_store.dart';
import 'package:frosty/screens/channel/chat/chat_assets_store.dart';
import 'package:frosty/screens/channel/chat/emote_menu/emote_menu_panel.dart';
class EmoteMenu extends StatelessWidget {
final ChatStore chatStore;
final ChatAssetsStore assetsStore;
final TextEditingController textController;
const EmoteMenu({Key? key, required this.chatStore}) : super(key: key);
const EmoteMenu({Key? key, required this.assetsStore, required this.textController}) : super(key: key);
@override
Widget build(BuildContext context) {
@ -24,31 +25,31 @@ class EmoteMenu extends StatelessWidget {
children: [
Expanded(
child: PageView(
onPageChanged: (index) => chatStore.emoteMenuIndex = index,
onPageChanged: (index) => assetsStore.emoteMenuIndex = index,
controller: pageContoller,
children: [
Observer(
builder: (_) => EmoteMenuPanel(
textController: chatStore.textController,
emotes: chatStore.userEmoteToObject.values.toList(),
textController: textController,
emotes: assetsStore.userEmoteToObject.values.toList(),
),
),
Observer(
builder: (_) => EmoteMenuPanel(
textController: chatStore.textController,
emotes: chatStore.bttvEmotes,
textController: textController,
emotes: assetsStore.bttvEmotes,
),
),
Observer(
builder: (_) => EmoteMenuPanel(
textController: chatStore.textController,
emotes: chatStore.ffzEmotes,
textController: textController,
emotes: assetsStore.ffzEmotes,
),
),
Observer(
builder: (_) => EmoteMenuPanel(
textController: chatStore.textController,
emotes: chatStore.sevenTvEmotes,
textController: textController,
emotes: assetsStore.sevenTvEmotes,
),
),
],
@ -66,9 +67,9 @@ class EmoteMenu extends StatelessWidget {
duration: const Duration(milliseconds: 300),
curve: Curves.ease,
);
chatStore.emoteMenuIndex = index;
assetsStore.emoteMenuIndex = index;
},
style: index == chatStore.emoteMenuIndex ? null : TextButton.styleFrom(primary: Colors.grey),
style: index == assetsStore.emoteMenuIndex ? null : TextButton.styleFrom(primary: Colors.grey),
child: Text(section),
),
),