Refactor chat store
- Added user login to IRC NICK command - Added first iteration of clearChat (#8) - Moved parse method to irc_api.dart - Fixed extra element when splitting snapshot data message
This commit is contained in:
parent
7673a2c8a0
commit
9bcc9a0479
3 changed files with 94 additions and 98 deletions
|
|
@ -3,8 +3,10 @@ import 'package:flutter/scheduler.dart';
|
|||
import 'package:flutter/widgets.dart';
|
||||
import 'package:frosty/api/bttv_api.dart';
|
||||
import 'package:frosty/api/ffz_api.dart';
|
||||
import 'package:frosty/api/irc_api.dart';
|
||||
import 'package:frosty/api/seventv_api.dart';
|
||||
import 'package:frosty/api/twitch_api.dart';
|
||||
import 'package:frosty/models/irc_message.dart';
|
||||
import 'package:frosty/stores/auth_store.dart';
|
||||
import 'package:frosty/widgets/chat_message.dart';
|
||||
import 'package:mobx/mobx.dart';
|
||||
|
|
@ -15,25 +17,27 @@ part 'chat_store.g.dart';
|
|||
class ChatStore = _ChatStoreBase with _$ChatStore;
|
||||
|
||||
abstract class _ChatStoreBase with Store {
|
||||
@observable
|
||||
bool autoScroll = true;
|
||||
|
||||
List<String> messages = [];
|
||||
final messages = <IrcMessage>[];
|
||||
|
||||
final channel = WebSocketChannel.connect(Uri.parse('wss://irc-ws.chat.twitch.tv:443'));
|
||||
|
||||
final _assetToUrl = <String, String>{};
|
||||
|
||||
final _emoteIdToWord = <String, String>{};
|
||||
|
||||
final String channelName;
|
||||
|
||||
final AuthStore auth;
|
||||
|
||||
final scrollController = ScrollController();
|
||||
|
||||
final String channelName;
|
||||
final AuthStore auth;
|
||||
@readonly
|
||||
var _autoScroll = true;
|
||||
|
||||
_ChatStoreBase({required this.auth, required this.channelName}) {
|
||||
final commands = [
|
||||
'PASS oauth:${auth.token}',
|
||||
'NICK justinfan888',
|
||||
'NICK ${auth.isLoggedIn ? auth.user!.login : 'justinfan888'}',
|
||||
'CAP REQ :twitch.tv/tags',
|
||||
'CAP REQ :twitch.tv/commands',
|
||||
// 'CAP REQ :twitch.tv/membership',
|
||||
|
|
@ -47,9 +51,9 @@ abstract class _ChatStoreBase with Store {
|
|||
|
||||
scrollController.addListener(() {
|
||||
if (!scrollController.position.atEdge && scrollController.position.pixels < scrollController.position.maxScrollExtent) {
|
||||
autoScroll = false;
|
||||
_autoScroll = false;
|
||||
} else if (scrollController.position.atEdge && scrollController.position.pixels != scrollController.position.minScrollExtent) {
|
||||
autoScroll = true;
|
||||
_autoScroll = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -80,76 +84,72 @@ abstract class _ChatStoreBase with Store {
|
|||
}
|
||||
}
|
||||
|
||||
@action
|
||||
void handleWebsocketData(Object? data) {
|
||||
for (final message in data.toString().split('\r\n')) {
|
||||
final ircMessages = data.toString();
|
||||
for (final message in ircMessages.substring(0, ircMessages.length - 2).split('\r\n')) {
|
||||
if (message.startsWith('@')) {
|
||||
messages.add(message);
|
||||
continue;
|
||||
}
|
||||
if (message.startsWith('P')) {
|
||||
channel.sink.add('PONG :tmi.twitch.tv');
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
final parsed = Irc.parse(message);
|
||||
|
||||
Widget parseIrcMessage(String whole) {
|
||||
var mappedTags = <String, String>{};
|
||||
|
||||
final tagAndIrcMessageDivider = whole.indexOf(' ');
|
||||
final tags = whole.substring(1, tagAndIrcMessageDivider).replaceAll('\\s', ' ');
|
||||
final ircMessage = whole.substring(tagAndIrcMessageDivider + 2);
|
||||
|
||||
for (final tag in tags.split(';')) {
|
||||
if (!tag.endsWith('=')) {
|
||||
final tagSplit = tag.split('=');
|
||||
mappedTags[tagSplit[0]] = tagSplit[1];
|
||||
}
|
||||
}
|
||||
|
||||
final splitMessage = ircMessage.split(' ');
|
||||
|
||||
// final user = splitMessage[0].substring(0, splitMessage[0].indexOf('!'));
|
||||
// print(user);
|
||||
|
||||
final command = splitMessage[1];
|
||||
|
||||
switch (command) {
|
||||
case 'CLEARCHAT':
|
||||
break;
|
||||
case 'CLEARMSG':
|
||||
break;
|
||||
case 'GLOBALUSERSTATE':
|
||||
break;
|
||||
case 'PRIVMSG':
|
||||
final message = splitMessage.sublist(3).join(' ').substring(1);
|
||||
|
||||
if (autoScroll) {
|
||||
SchedulerBinding.instance?.addPostFrameCallback((_) {
|
||||
scrollController.jumpTo(scrollController.position.maxScrollExtent);
|
||||
});
|
||||
switch (parsed.command) {
|
||||
case 'CLEARCHAT':
|
||||
clearChat(ircMessage: parsed);
|
||||
break;
|
||||
case 'CLEARMSG':
|
||||
break;
|
||||
case 'GLOBALUSERSTATE':
|
||||
break;
|
||||
case 'PRIVMSG':
|
||||
if (_autoScroll) {
|
||||
if (messages.length > 100) {
|
||||
messages.removeRange(0, messages.length - 100);
|
||||
}
|
||||
SchedulerBinding.instance?.addPostFrameCallback((_) {
|
||||
scrollController.jumpTo(scrollController.position.maxScrollExtent);
|
||||
});
|
||||
}
|
||||
messages.add(parsed);
|
||||
break;
|
||||
case 'ROOMSTATE':
|
||||
break;
|
||||
case 'USERNOTICE':
|
||||
break;
|
||||
case 'USERSTATE':
|
||||
break;
|
||||
}
|
||||
return ChatMessage(
|
||||
key: Key(mappedTags['id']!),
|
||||
children: privateMessage(tags: mappedTags, chatMessage: message),
|
||||
);
|
||||
|
||||
case 'ROOMSTATE':
|
||||
break;
|
||||
case 'USERNOTICE':
|
||||
break;
|
||||
case 'USERSTATE':
|
||||
break;
|
||||
} else if (message.startsWith('P')) {
|
||||
channel.sink.add('PONG :tmi.twitch.tv');
|
||||
}
|
||||
}
|
||||
return const SizedBox();
|
||||
}
|
||||
|
||||
List<InlineSpan> privateMessage({required Map<String, String> tags, required String chatMessage}) {
|
||||
// debugPrint(chatMessage);
|
||||
var result = <InlineSpan>[];
|
||||
void clearChat({required IrcMessage ircMessage}) {
|
||||
// If there is no message, it means that entire chat was cleared.
|
||||
if (ircMessage.message == null) {
|
||||
// messages.clear();
|
||||
messages[0].message = 'User was permabanned!';
|
||||
return;
|
||||
}
|
||||
|
||||
final emoteTags = tags['emotes'];
|
||||
final bannedUser = ircMessage.message;
|
||||
final banDuration = ircMessage.tags['ban-duration'];
|
||||
|
||||
debugPrint('$bannedUser was banned');
|
||||
|
||||
messages.asMap().forEach((i, message) {
|
||||
if (message.user! == bannedUser) {
|
||||
if (banDuration == null) {
|
||||
messages[i].message = 'User was permabanned!';
|
||||
} else {
|
||||
messages[i].message = 'User was timed out for $banDuration seconds!';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ChatMessage renderChatMessage({required IrcMessage ircMessage}) {
|
||||
final result = <InlineSpan>[];
|
||||
|
||||
final emoteTags = ircMessage.tags['emotes'];
|
||||
if (emoteTags != null) {
|
||||
final emotes = emoteTags.split('/');
|
||||
|
||||
|
|
@ -172,15 +172,15 @@ abstract class _ChatStoreBase with Store {
|
|||
final startIndex = int.parse(indexSplit[0]);
|
||||
final endIndex = int.parse(indexSplit[1]);
|
||||
|
||||
final emoteWord = chatMessage.substring(startIndex, endIndex + 1);
|
||||
final emoteWord = ircMessage.message!.substring(startIndex, endIndex + 1);
|
||||
|
||||
_emoteIdToWord[emoteId] = emoteWord;
|
||||
_assetToUrl[emoteWord] = 'https://static-cdn.jtvnw.net/emoticons/v2/$emoteId/default/dark/3.0';
|
||||
}
|
||||
}
|
||||
|
||||
final words = chatMessage.split(' ');
|
||||
final badges = tags['badges'];
|
||||
final words = ircMessage.message!.split(' ');
|
||||
final badges = ircMessage.tags['badges'];
|
||||
if (badges != null) {
|
||||
for (final badge in badges.split(',')) {
|
||||
final badgeUrl = _assetToUrl[badge];
|
||||
|
|
@ -203,9 +203,9 @@ abstract class _ChatStoreBase with Store {
|
|||
|
||||
result.add(
|
||||
TextSpan(
|
||||
text: tags['display-name']!,
|
||||
text: ircMessage.tags['display-name']!,
|
||||
style: TextStyle(
|
||||
color: HexColor.fromHex(tags['color'] ?? '#868686'),
|
||||
color: HexColor.fromHex(ircMessage.tags['color'] ?? '#868686'),
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
|
|
@ -235,12 +235,16 @@ abstract class _ChatStoreBase with Store {
|
|||
result.add(TextSpan(text: word));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
|
||||
return ChatMessage(
|
||||
key: Key(ircMessage.tags['id']!),
|
||||
children: result,
|
||||
);
|
||||
}
|
||||
|
||||
@action
|
||||
void resumeScroll() {
|
||||
autoScroll = true;
|
||||
_autoScroll = true;
|
||||
scrollController.jumpTo(scrollController.position.maxScrollExtent);
|
||||
SchedulerBinding.instance?.addPostFrameCallback((_) {
|
||||
scrollController.jumpTo(scrollController.position.maxScrollExtent);
|
||||
|
|
|
|||
|
|
@ -9,18 +9,20 @@ 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 {
|
||||
final _$autoScrollAtom = Atom(name: '_ChatStoreBase.autoScroll');
|
||||
final _$_autoScrollAtom = Atom(name: '_ChatStoreBase._autoScroll');
|
||||
|
||||
@override
|
||||
bool get autoScroll {
|
||||
_$autoScrollAtom.reportRead();
|
||||
return super.autoScroll;
|
||||
_$_autoScrollAtom.reportRead();
|
||||
return super._autoScroll;
|
||||
}
|
||||
|
||||
@override
|
||||
set autoScroll(bool value) {
|
||||
_$autoScrollAtom.reportWrite(value, super.autoScroll, () {
|
||||
super.autoScroll = value;
|
||||
bool get _autoScroll => autoScroll;
|
||||
|
||||
@override
|
||||
set _autoScroll(bool value) {
|
||||
_$_autoScrollAtom.reportWrite(value, super._autoScroll, () {
|
||||
super._autoScroll = value;
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -34,17 +36,6 @@ mixin _$ChatStore on _ChatStoreBase, Store {
|
|||
final _$_ChatStoreBaseActionController =
|
||||
ActionController(name: '_ChatStoreBase');
|
||||
|
||||
@override
|
||||
void handleWebsocketData(Object? data) {
|
||||
final _$actionInfo = _$_ChatStoreBaseActionController.startAction(
|
||||
name: '_ChatStoreBase.handleWebsocketData');
|
||||
try {
|
||||
return super.handleWebsocketData(data);
|
||||
} finally {
|
||||
_$_ChatStoreBaseActionController.endAction(_$actionInfo);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void resumeScroll() {
|
||||
final _$actionInfo = _$_ChatStoreBaseActionController.startAction(
|
||||
|
|
@ -59,7 +50,7 @@ mixin _$ChatStore on _ChatStoreBase, Store {
|
|||
@override
|
||||
String toString() {
|
||||
return '''
|
||||
autoScroll: ${autoScroll}
|
||||
|
||||
''';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ class _ChatState extends State<Chat> {
|
|||
controller: widget.chatStore.scrollController,
|
||||
padding: const EdgeInsets.all(5.0),
|
||||
itemBuilder: (context, index) {
|
||||
return widget.chatStore.messages[index];
|
||||
return widget.chatStore.renderChatMessage(ircMessage: widget.chatStore.messages[index]);
|
||||
},
|
||||
);
|
||||
},
|
||||
|
|
@ -63,6 +63,7 @@ class _ChatState extends State<Chat> {
|
|||
@override
|
||||
void dispose() {
|
||||
widget.chatStore.channel.sink.close();
|
||||
widget.chatStore.scrollController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue