2021-10-23 02:28:20 -04:00
|
|
|
import 'package:flutter/material.dart';
|
2021-09-11 01:10:56 -04:00
|
|
|
import 'package:flutter/scheduler.dart';
|
2021-10-03 22:14:55 -04:00
|
|
|
import 'package:frosty/api/bttv_api.dart';
|
|
|
|
|
import 'package:frosty/api/ffz_api.dart';
|
2021-10-16 19:11:35 -04:00
|
|
|
import 'package:frosty/api/irc_api.dart';
|
2021-10-03 22:14:55 -04:00
|
|
|
import 'package:frosty/api/seventv_api.dart';
|
|
|
|
|
import 'package:frosty/api/twitch_api.dart';
|
2021-11-21 23:29:47 -05:00
|
|
|
import 'package:frosty/core/auth/auth_store.dart';
|
2021-11-23 04:15:13 -05:00
|
|
|
import 'package:frosty/core/settings/settings_store.dart';
|
2021-10-22 02:46:06 -04:00
|
|
|
import 'package:frosty/models/irc.dart';
|
2021-10-01 00:08:19 -04:00
|
|
|
import 'package:mobx/mobx.dart';
|
2021-09-03 02:14:17 -04:00
|
|
|
import 'package:web_socket_channel/web_socket_channel.dart';
|
|
|
|
|
|
2021-10-01 00:08:19 -04:00
|
|
|
part 'chat_store.g.dart';
|
|
|
|
|
|
2021-10-26 01:06:57 -04:00
|
|
|
/// The store and view-model for chat-related activities.
|
2021-10-01 00:08:19 -04:00
|
|
|
class ChatStore = _ChatStoreBase with _$ChatStore;
|
|
|
|
|
|
|
|
|
|
abstract class _ChatStoreBase with Store {
|
2021-10-26 01:06:57 -04:00
|
|
|
/// The Twitch IRC WebSocket channel.
|
|
|
|
|
final _channel = WebSocketChannel.connect(Uri.parse('wss://irc-ws.chat.twitch.tv:443'));
|
2021-10-22 02:46:06 -04:00
|
|
|
|
2021-11-02 03:51:34 -04:00
|
|
|
/// The map of badges and emote words to their image or GIF URL.
|
2021-09-03 02:14:17 -04:00
|
|
|
final _assetToUrl = <String, String>{};
|
2021-09-06 19:03:39 -04:00
|
|
|
|
2021-11-14 04:25:33 -05:00
|
|
|
/// The list of chat messages to render and display.
|
|
|
|
|
final messages = ObservableList<IRCMessage>();
|
|
|
|
|
|
2021-10-26 01:06:57 -04:00
|
|
|
/// The scroll controller that controls auto-scroll and resume-scroll behavior.
|
2021-11-14 04:25:33 -05:00
|
|
|
final scrollController = ScrollController();
|
2021-10-16 19:11:35 -04:00
|
|
|
|
2021-10-26 01:06:57 -04:00
|
|
|
/// The text controller that handles the TextField inputs and sending of messages.
|
2021-11-14 04:25:33 -05:00
|
|
|
final textController = TextEditingController();
|
2021-10-03 22:14:55 -04:00
|
|
|
|
2021-10-26 01:06:57 -04:00
|
|
|
/// The name of the channel to connect to.
|
2021-10-17 17:56:16 -04:00
|
|
|
final String channelName;
|
|
|
|
|
|
2021-10-26 01:06:57 -04:00
|
|
|
/// The provided auth store to determine login status, get the token, and use the headers for requests.
|
2021-10-17 17:56:16 -04:00
|
|
|
final AuthStore auth;
|
2021-10-16 19:11:35 -04:00
|
|
|
|
2021-11-23 04:15:13 -05:00
|
|
|
/// The provided setting store to account for any user-defined behaviors.
|
|
|
|
|
final SettingsStore settings;
|
|
|
|
|
|
2021-10-26 01:06:57 -04:00
|
|
|
/// The logged-in user's appearance in chat.
|
2021-12-11 00:45:44 -05:00
|
|
|
var _userState = const USERSTATE();
|
2021-10-26 01:06:57 -04:00
|
|
|
|
2021-11-23 00:47:21 -05:00
|
|
|
/// Requested message to be sent by the user. Will only be sent on receival of a USERNOTICE command.
|
|
|
|
|
IRCMessage? toSend;
|
|
|
|
|
|
2021-10-26 01:06:57 -04:00
|
|
|
/// If the chat should automatically scroll/jump to the latest message.
|
|
|
|
|
@readonly
|
|
|
|
|
var _autoScroll = true;
|
|
|
|
|
|
|
|
|
|
/// The rules and modes being used in the chat.
|
|
|
|
|
@readonly
|
2021-11-02 03:51:34 -04:00
|
|
|
var _roomState = const ROOMSTATE();
|
2021-10-26 01:06:57 -04:00
|
|
|
|
2021-11-23 04:15:13 -05:00
|
|
|
_ChatStoreBase({
|
|
|
|
|
required this.auth,
|
|
|
|
|
required this.settings,
|
|
|
|
|
required this.channelName,
|
|
|
|
|
}) {
|
2021-10-26 01:06:57 -04:00
|
|
|
// Listen for new messages and forward them to the handler.
|
|
|
|
|
_channel.stream.listen(
|
|
|
|
|
(data) => _handleIRCData(data.toString()),
|
2021-11-25 22:01:02 -05:00
|
|
|
onError: (error) => debugPrint('Failed to connect to chat: ${error.toString()}'),
|
2021-10-26 01:06:57 -04:00
|
|
|
onDone: () => debugPrint("Disconnected from $channelName's chat."),
|
2021-10-17 02:25:40 -04:00
|
|
|
);
|
2021-10-20 05:12:15 -04:00
|
|
|
|
2021-10-26 01:06:57 -04:00
|
|
|
// The list of messages sent to the IRC WebSocket channel to connect and join.
|
2021-09-03 02:14:17 -04:00
|
|
|
final commands = [
|
2021-10-26 01:06:57 -04:00
|
|
|
// Request the tags and commands capabilities.
|
|
|
|
|
// This will display tags containing metadata along with each IRC message.
|
2021-10-17 17:56:16 -04:00
|
|
|
'CAP REQ :twitch.tv/tags twitch.tv/commands',
|
2021-10-26 01:06:57 -04:00
|
|
|
|
|
|
|
|
// The OAuth token in order to connect, default or user token.
|
2021-10-03 22:14:55 -04:00
|
|
|
'PASS oauth:${auth.token}',
|
2021-10-26 01:06:57 -04:00
|
|
|
|
|
|
|
|
// The nickname for the connecting user. 'justinfan888' is the Twitch default if not logged in.
|
2021-10-16 19:11:35 -04:00
|
|
|
'NICK ${auth.isLoggedIn ? auth.user!.login : 'justinfan888'}',
|
2021-10-26 01:06:57 -04:00
|
|
|
|
|
|
|
|
// Join the desired channel's room.
|
2021-10-10 00:10:58 -04:00
|
|
|
'JOIN #$channelName',
|
2021-09-03 02:14:17 -04:00
|
|
|
];
|
|
|
|
|
|
2021-11-02 03:51:34 -04:00
|
|
|
// Send each command in order.
|
2021-09-03 02:14:17 -04:00
|
|
|
for (final command in commands) {
|
2021-10-26 01:06:57 -04:00
|
|
|
_channel.sink.add(command);
|
2021-09-03 02:14:17 -04:00
|
|
|
}
|
2021-10-20 05:12:15 -04:00
|
|
|
|
2021-10-26 01:06:57 -04:00
|
|
|
// Tell the scrollController to determine when auto-scroll should be enabled or disabled.
|
2021-11-14 04:25:33 -05:00
|
|
|
scrollController.addListener(() {
|
2021-10-26 01:06:57 -04:00
|
|
|
// If the user scrolls up, auto-scroll will stop, allowing them to freely scroll back to previous messages.
|
|
|
|
|
// Else if the user scrolls back to the bottom edge (latest message), auto-scroll will resume.
|
2021-12-06 23:54:00 -05:00
|
|
|
if (scrollController.position.pixels < scrollController.position.maxScrollExtent) {
|
|
|
|
|
if (_autoScroll == true) _autoScroll = false;
|
|
|
|
|
} else if (scrollController.position.atEdge || scrollController.position.pixels > scrollController.position.maxScrollExtent) {
|
|
|
|
|
if (_autoScroll == false) _autoScroll = true;
|
2021-09-06 19:03:39 -04:00
|
|
|
}
|
|
|
|
|
});
|
2021-09-03 02:14:17 -04:00
|
|
|
}
|
|
|
|
|
|
2021-10-26 01:06:57 -04:00
|
|
|
/// Handle and process the provided string-representation of the IRC data.
|
|
|
|
|
///
|
|
|
|
|
/// If a message, parses the IRC data into an [IRCMessage] and handles it based on the [Command].
|
|
|
|
|
/// Else if a PING request, sends back the PONG to keep the connection alive.
|
2021-10-22 02:46:06 -04:00
|
|
|
@action
|
2021-10-26 01:06:57 -04:00
|
|
|
void _handleIRCData(String data) {
|
|
|
|
|
// The IRC data can contain more than one message separated by CRLF.
|
2021-11-02 03:51:34 -04:00
|
|
|
// To account for this, split by CRLF, then loop and process each message.
|
|
|
|
|
for (final message in data.trimRight().split('\r\n')) {
|
2021-09-11 02:17:09 -04:00
|
|
|
if (message.startsWith('@')) {
|
2021-10-22 02:46:06 -04:00
|
|
|
final parsedIRCMessage = IRCMessage.fromString(message);
|
2021-10-16 19:11:35 -04:00
|
|
|
|
2021-12-11 00:45:44 -05:00
|
|
|
// 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;
|
|
|
|
|
|
2021-10-17 02:15:51 -04:00
|
|
|
switch (parsedIRCMessage.command) {
|
2021-10-24 03:53:08 -04:00
|
|
|
case Command.privateMessage:
|
|
|
|
|
messages.add(parsedIRCMessage);
|
2021-10-20 05:12:15 -04:00
|
|
|
break;
|
2021-10-24 03:53:08 -04:00
|
|
|
case Command.clearChat:
|
|
|
|
|
IRC.clearChat(messages: messages, ircMessage: parsedIRCMessage);
|
2021-10-20 05:12:15 -04:00
|
|
|
break;
|
2021-10-24 03:53:08 -04:00
|
|
|
case Command.clearMessage:
|
|
|
|
|
IRC.clearMessage(messages: messages, ircMessage: parsedIRCMessage);
|
2021-10-22 02:46:06 -04:00
|
|
|
break;
|
2021-11-23 00:47:21 -05:00
|
|
|
case Command.notice:
|
2021-10-24 03:53:08 -04:00
|
|
|
case Command.userNotice:
|
2021-10-20 05:12:15 -04:00
|
|
|
messages.add(parsedIRCMessage);
|
2021-10-16 19:11:35 -04:00
|
|
|
break;
|
2021-10-24 03:53:08 -04:00
|
|
|
case Command.roomState:
|
2021-12-11 00:45:44 -05:00
|
|
|
_roomState = _roomState.fromIRC(parsedIRCMessage);
|
2021-11-23 00:47:21 -05:00
|
|
|
continue;
|
2021-10-24 03:53:08 -04:00
|
|
|
case Command.userState:
|
2021-12-11 00:45:44 -05:00
|
|
|
_userState = _userState.fromIRC(parsedIRCMessage);
|
2021-11-23 00:47:21 -05:00
|
|
|
if (toSend != null) {
|
|
|
|
|
messages.add(toSend!);
|
|
|
|
|
toSend = null;
|
|
|
|
|
}
|
2021-10-22 02:46:06 -04:00
|
|
|
break;
|
2021-10-24 03:53:08 -04:00
|
|
|
case Command.globalUserState:
|
|
|
|
|
// Updates the current global user state data (it includes user-id),
|
|
|
|
|
// Don't really see a use for it when USERSTATE exists, so leaving it unimplemented for now.
|
2021-11-23 00:47:21 -05:00
|
|
|
continue;
|
|
|
|
|
case Command.none:
|
2021-10-24 03:53:08 -04:00
|
|
|
debugPrint('Unknown command: ${parsedIRCMessage.command}');
|
2021-11-23 00:47:21 -05:00
|
|
|
continue;
|
2021-10-16 19:11:35 -04:00
|
|
|
}
|
2021-11-25 22:01:02 -05:00
|
|
|
|
2021-11-23 00:47:21 -05:00
|
|
|
_deleteAndScrollToEnd();
|
2021-10-22 02:46:06 -04:00
|
|
|
} else if (message == 'PING :tmi.twitch.tv') {
|
2021-10-26 01:06:57 -04:00
|
|
|
_channel.sink.add('PONG :tmi.twitch.tv');
|
2021-10-17 17:56:16 -04:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
2021-10-24 03:53:08 -04:00
|
|
|
}
|
|
|
|
|
|
2021-10-26 01:06:57 -04:00
|
|
|
/// If [_autoScroll] is enabled, removes messages if [messages] is too large and scrolls to the latest message.
|
2021-10-24 03:53:08 -04:00
|
|
|
@action
|
2021-10-26 01:06:57 -04:00
|
|
|
void _deleteAndScrollToEnd() {
|
2021-10-17 17:56:16 -04:00
|
|
|
if (_autoScroll) {
|
2021-11-23 04:15:13 -05:00
|
|
|
// If there are more messages than the limit, remove around 10% of them from the oldest.
|
|
|
|
|
if (messages.length > settings.messageLimit && settings.messageLimit != 1000) {
|
2021-11-26 05:47:28 -05:00
|
|
|
messages.removeRange(0, (settings.messageLimit / 5).ceil());
|
2021-10-17 17:56:16 -04:00
|
|
|
}
|
2021-10-20 05:12:15 -04:00
|
|
|
|
2021-10-26 01:06:57 -04:00
|
|
|
// After the end of the frame, scroll to the bottom of the chat.
|
|
|
|
|
// This is a postFrameCallback because the chat should scroll after the widget is built and rendered.
|
2021-10-18 02:43:47 -04:00
|
|
|
SchedulerBinding.instance?.addPostFrameCallback((_) {
|
2021-11-14 04:25:33 -05:00
|
|
|
if (scrollController.hasClients) {
|
|
|
|
|
scrollController.jumpTo(scrollController.position.maxScrollExtent);
|
2021-10-18 02:43:47 -04:00
|
|
|
}
|
|
|
|
|
});
|
2021-09-11 01:10:56 -04:00
|
|
|
}
|
2021-09-11 02:17:09 -04:00
|
|
|
}
|
2021-09-11 01:10:56 -04:00
|
|
|
|
2021-10-26 01:06:57 -04:00
|
|
|
/// Re-enables [_autoScroll] and jumps to the latest message.
|
|
|
|
|
@action
|
|
|
|
|
void resumeScroll() {
|
|
|
|
|
_autoScroll = true;
|
|
|
|
|
|
|
|
|
|
// Jump to the latest message (bottom of the list/chat).
|
2021-11-14 04:25:33 -05:00
|
|
|
scrollController.jumpTo(scrollController.position.maxScrollExtent);
|
2021-10-26 01:06:57 -04:00
|
|
|
|
|
|
|
|
// Schedule a postFrameCallback in the event a new message is added at the same time.
|
|
|
|
|
SchedulerBinding.instance?.addPostFrameCallback((_) {
|
2021-11-14 04:25:33 -05:00
|
|
|
scrollController.jumpTo(scrollController.position.maxScrollExtent);
|
2021-10-26 01:06:57 -04:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2021-11-25 22:01:02 -05:00
|
|
|
/// Sends the given string message by the logged-in user and adds it to [messages].
|
|
|
|
|
void sendMessage(String message) {
|
|
|
|
|
// Do not send if the message is blank/empty.
|
|
|
|
|
if (message.isEmpty) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Send the message to the IRC chat room.
|
|
|
|
|
_channel.sink.add('PRIVMSG #$channelName :$message');
|
|
|
|
|
|
|
|
|
|
// Obtain the logged-in user's appearance in chat with USERSTATE and create the full message to render.
|
2021-12-11 00:45:44 -05:00
|
|
|
final userStateString = _userState.raw;
|
|
|
|
|
if (userStateString != null) {
|
|
|
|
|
final userChatMessage = IRCMessage.fromString(userStateString);
|
2021-11-25 22:01:02 -05:00
|
|
|
userChatMessage.message = message;
|
|
|
|
|
toSend = userChatMessage;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Clear the previous input in the TextField.
|
|
|
|
|
textController.clear();
|
|
|
|
|
}
|
|
|
|
|
|
2021-10-26 01:06:57 -04:00
|
|
|
/// Fetches global and channel assets (badges and emotes) and stores them in [_assetToUrl]
|
|
|
|
|
Future<void> getAssets() async {
|
|
|
|
|
// 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),
|
2021-11-02 03:51:34 -04:00
|
|
|
await Twitch.getEmotesGlobal(headers: auth.headersTwitch),
|
|
|
|
|
await Twitch.getEmotesChannel(id: channelInfo.id, headers: auth.headersTwitch),
|
2021-10-26 01:06:57 -04:00
|
|
|
await Twitch.getBadgesGlobal(headers: auth.headersTwitch),
|
|
|
|
|
await Twitch.getBadgesChannel(id: channelInfo.id, headers: auth.headersTwitch),
|
|
|
|
|
await SevenTV.getEmotesGlobal(),
|
|
|
|
|
await SevenTV.getEmotesChannel(user: channelInfo.login)
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
// Add all the assets to the global word-to-asset map.
|
|
|
|
|
// 'Global' meaning these assets can be used by any message when rendering.
|
|
|
|
|
for (final map in assets) {
|
|
|
|
|
if (map != null) {
|
|
|
|
|
_assetToUrl.addAll(map);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-11-23 00:47:21 -05:00
|
|
|
// TODO: Split render functions and use switch statment.
|
2021-10-26 01:06:57 -04:00
|
|
|
/// Returns a chat message widget for the given [IRCMessage].
|
2021-11-23 00:47:21 -05:00
|
|
|
Widget renderChatMessage(IRCMessage ircMessage, BuildContext context) {
|
2021-10-24 03:53:08 -04:00
|
|
|
if (ircMessage.command == Command.clearChat || ircMessage.command == Command.clearMessage) {
|
2021-12-08 02:14:15 -05:00
|
|
|
final List<InlineSpan> span;
|
|
|
|
|
|
|
|
|
|
if (settings.hideBannedMessages) {
|
|
|
|
|
span = IRC.generateSpan(ircMessage: ircMessage, assetToUrl: _assetToUrl, hideMessage: true);
|
|
|
|
|
} else {
|
|
|
|
|
span = IRC.generateSpan(ircMessage: ircMessage, assetToUrl: _assetToUrl);
|
|
|
|
|
}
|
2021-11-23 00:47:21 -05:00
|
|
|
|
2021-11-02 03:51:34 -04:00
|
|
|
// Render timeouts and bans
|
2021-10-18 02:43:47 -04:00
|
|
|
final banDuration = ircMessage.tags['ban-duration'];
|
|
|
|
|
return Padding(
|
2021-11-14 20:52:29 -05:00
|
|
|
padding: const EdgeInsets.all(10.0),
|
2021-10-18 02:43:47 -04:00
|
|
|
child: Opacity(
|
|
|
|
|
opacity: 0.50,
|
|
|
|
|
child: Column(
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
children: [
|
2021-11-12 22:45:07 -05:00
|
|
|
Text.rich(
|
|
|
|
|
TextSpan(
|
|
|
|
|
children: span,
|
|
|
|
|
),
|
2021-10-18 02:43:47 -04:00
|
|
|
),
|
2021-10-26 01:06:57 -04:00
|
|
|
const SizedBox(height: 5),
|
2021-12-08 02:14:15 -05:00
|
|
|
if (banDuration == null)
|
|
|
|
|
if (ircMessage.command == Command.clearMessage)
|
|
|
|
|
const Text('Message Deleted', style: TextStyle(fontWeight: FontWeight.bold))
|
|
|
|
|
else
|
|
|
|
|
const Text('User Permanently Banned', style: TextStyle(fontWeight: FontWeight.bold))
|
|
|
|
|
else
|
|
|
|
|
Text(
|
|
|
|
|
int.parse(banDuration) > 1 ? 'Timed out for $banDuration seconds' : 'Timed out for $banDuration second',
|
|
|
|
|
style: const TextStyle(fontWeight: FontWeight.bold),
|
|
|
|
|
)
|
2021-10-18 02:43:47 -04:00
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
);
|
2021-11-23 00:47:21 -05:00
|
|
|
} else if (ircMessage.command == Command.notice) {
|
|
|
|
|
return Padding(
|
|
|
|
|
padding: const EdgeInsets.all(10.0),
|
|
|
|
|
child: Text.rich(
|
|
|
|
|
TextSpan(
|
|
|
|
|
text: ircMessage.message,
|
|
|
|
|
),
|
|
|
|
|
style: TextStyle(
|
|
|
|
|
color: Theme.of(context).textTheme.bodyText2?.color?.withOpacity(0.5),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
);
|
2021-10-24 03:53:08 -04:00
|
|
|
} else if (ircMessage.command == Command.userNotice) {
|
2021-11-23 00:47:21 -05:00
|
|
|
final span = IRC.generateSpan(ircMessage: ircMessage, assetToUrl: _assetToUrl);
|
|
|
|
|
|
2021-11-02 03:51:34 -04:00
|
|
|
// Render sub alerts
|
2021-10-23 02:28:20 -04:00
|
|
|
return Container(
|
2021-11-27 03:56:39 -05:00
|
|
|
color: const Color(0xFF673AB7).withOpacity(0.25),
|
2021-11-14 20:52:29 -05:00
|
|
|
padding: const EdgeInsets.all(10.0),
|
2021-10-23 02:28:20 -04:00
|
|
|
child: Column(
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
children: [
|
2021-11-02 03:51:34 -04:00
|
|
|
Text(ircMessage.tags['system-msg']!),
|
2021-10-26 01:06:57 -04:00
|
|
|
const SizedBox(height: 5),
|
2021-10-23 02:28:20 -04:00
|
|
|
if (ircMessage.message != null)
|
2021-11-12 22:45:07 -05:00
|
|
|
Text.rich(
|
|
|
|
|
TextSpan(
|
|
|
|
|
children: span,
|
|
|
|
|
),
|
2021-10-23 02:28:20 -04:00
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
);
|
2021-10-18 02:43:47 -04:00
|
|
|
} else {
|
2021-11-23 00:47:21 -05:00
|
|
|
final span = IRC.generateSpan(ircMessage: ircMessage, assetToUrl: _assetToUrl);
|
|
|
|
|
|
2021-11-02 03:51:34 -04:00
|
|
|
// Render normal chat message (PRIVMSG).
|
2021-10-23 02:28:20 -04:00
|
|
|
return Padding(
|
2021-11-14 20:52:29 -05:00
|
|
|
padding: const EdgeInsets.symmetric(vertical: 5.0, horizontal: 10.0),
|
2021-11-12 22:45:07 -05:00
|
|
|
child: Text.rich(
|
|
|
|
|
TextSpan(
|
|
|
|
|
children: span,
|
|
|
|
|
),
|
2021-10-23 02:28:20 -04:00
|
|
|
),
|
2021-10-18 02:43:47 -04:00
|
|
|
);
|
|
|
|
|
}
|
2021-09-03 02:14:17 -04:00
|
|
|
}
|
|
|
|
|
|
2021-10-26 01:06:57 -04:00
|
|
|
/// Closes and disposes all the channels and controllers used by the store.
|
2021-10-17 17:56:16 -04:00
|
|
|
void dispose() {
|
|
|
|
|
_channel.sink.close();
|
2021-11-14 04:25:33 -05:00
|
|
|
textController.dispose();
|
|
|
|
|
scrollController.dispose();
|
2021-10-17 17:56:16 -04:00
|
|
|
}
|
2021-09-03 02:14:17 -04:00
|
|
|
}
|