2021-12-13 02:19:06 -05:00
|
|
|
import 'dart:async';
|
|
|
|
|
|
2021-10-23 02:28:20 -04:00
|
|
|
import 'package:flutter/material.dart';
|
2022-01-19 15:12:54 -05:00
|
|
|
import 'package:frosty/constants/constants.dart';
|
2021-11-21 23:29:47 -05:00
|
|
|
import 'package:frosty/core/auth/auth_store.dart';
|
2022-01-15 15:46:46 -05:00
|
|
|
import 'package:frosty/models/badges.dart';
|
|
|
|
|
import 'package:frosty/models/emotes.dart';
|
2022-01-16 12:39:20 -05:00
|
|
|
import 'package:frosty/models/irc.dart';
|
2022-01-07 21:56:45 -05:00
|
|
|
import 'package:frosty/screens/channel/stores/chat_assets_store.dart';
|
|
|
|
|
import 'package:frosty/screens/channel/stores/chat_details_store.dart';
|
2022-01-04 02:58:02 -05:00
|
|
|
import 'package:frosty/screens/settings/stores/settings_store.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.
|
2022-05-12 04:58:03 -04:00
|
|
|
class ChatStore = ChatStoreBase with _$ChatStore;
|
2021-10-01 00:08:19 -04:00
|
|
|
|
2022-05-12 04:58:03 -04:00
|
|
|
abstract class ChatStoreBase with Store {
|
2022-01-23 15:02:21 -05:00
|
|
|
static const _messageUpperLimit = 7500;
|
2022-01-06 11:38:45 -05:00
|
|
|
static const _messageLimit = 5000;
|
2022-01-06 03:42:55 -05:00
|
|
|
|
2021-12-27 21:13:46 -05:00
|
|
|
/// The provided auth store to determine login status, get the token, and use the headers for requests.
|
|
|
|
|
final AuthStore auth;
|
2021-12-22 18:25:56 -05:00
|
|
|
|
2021-12-27 21:13:46 -05:00
|
|
|
/// The provided setting store to account for any user-defined behaviors.
|
|
|
|
|
final SettingsStore settings;
|
2021-12-22 18:25:56 -05:00
|
|
|
|
2022-01-10 13:10:31 -05:00
|
|
|
/// The focus node for the textfield to allow for showing and hiding the keyboard/focus.
|
|
|
|
|
final textFieldFocusNode = FocusNode();
|
|
|
|
|
|
2021-12-27 21:13:46 -05:00
|
|
|
/// The name of the channel to connect to.
|
|
|
|
|
final String channelName;
|
2021-12-11 17:09:15 -05:00
|
|
|
|
2022-01-15 15:46:46 -05:00
|
|
|
/// The channel's ID for API requests.
|
|
|
|
|
final String channelId;
|
|
|
|
|
|
|
|
|
|
/// The channel's display name to show on widgets.
|
|
|
|
|
final String displayName;
|
|
|
|
|
|
2021-12-27 21:13:46 -05:00
|
|
|
/// The Twitch IRC WebSocket channel.
|
2022-01-04 22:45:59 -05:00
|
|
|
WebSocketChannel? _channel;
|
|
|
|
|
|
|
|
|
|
// The retry counter for exponential backoff.
|
|
|
|
|
var _retries = 0;
|
|
|
|
|
|
|
|
|
|
// The current time to wait between retries for exponential backoff.
|
|
|
|
|
var _backoffTime = 0;
|
2021-11-14 04:25:33 -05:00
|
|
|
|
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-12-27 21:13:46 -05:00
|
|
|
/// The chat details store responsible for the chat modes and users in chat.
|
2022-01-14 14:56:54 -05:00
|
|
|
final ChatDetailsStore chatDetailsStore;
|
2021-10-17 17:56:16 -04:00
|
|
|
|
2021-12-27 21:13:46 -05:00
|
|
|
/// The assets store responsible for badges, emotes, and the emote menu.
|
2022-01-14 14:56:54 -05:00
|
|
|
final ChatAssetsStore assetsStore;
|
2021-11-23 04:15:13 -05:00
|
|
|
|
2022-01-09 12:53:40 -05:00
|
|
|
/// Requested message to be sent by the user. Will only be sent on receipt of a USERNOTICE command.
|
2021-11-23 00:47:21 -05:00
|
|
|
IRCMessage? toSend;
|
|
|
|
|
|
2021-12-15 23:54:06 -05:00
|
|
|
/// The list of chat messages to render and display.
|
|
|
|
|
@readonly
|
|
|
|
|
var _messages = ObservableList<IRCMessage>();
|
|
|
|
|
|
2021-10-26 01:06:57 -04:00
|
|
|
/// If the chat should automatically scroll/jump to the latest message.
|
|
|
|
|
@readonly
|
|
|
|
|
var _autoScroll = true;
|
|
|
|
|
|
2022-04-01 16:30:35 -04:00
|
|
|
@readonly
|
|
|
|
|
var _showAutocomplete = false;
|
|
|
|
|
|
2021-12-12 18:51:18 -05:00
|
|
|
/// The logged-in user's appearance in chat.
|
|
|
|
|
@readonly
|
|
|
|
|
var _userState = const USERSTATE();
|
|
|
|
|
|
2022-01-24 16:03:17 -05:00
|
|
|
final reactions = <ReactionDisposer>[];
|
2022-01-04 22:45:59 -05:00
|
|
|
|
2022-05-12 04:58:03 -04:00
|
|
|
ChatStoreBase({
|
2021-11-23 04:15:13 -05:00
|
|
|
required this.auth,
|
2022-01-14 14:56:54 -05:00
|
|
|
required this.chatDetailsStore,
|
|
|
|
|
required this.assetsStore,
|
2021-11-23 04:15:13 -05:00
|
|
|
required this.settings,
|
|
|
|
|
required this.channelName,
|
2022-01-15 15:46:46 -05:00
|
|
|
required this.channelId,
|
|
|
|
|
required this.displayName,
|
2021-11-23 04:15:13 -05:00
|
|
|
}) {
|
2022-01-26 15:42:37 -05:00
|
|
|
// Create a reaction that will reconnect to chat when logging in or out.
|
|
|
|
|
// Closing the channel will trigger a reconnect with the new credentials.
|
|
|
|
|
reactions.add(reaction(
|
|
|
|
|
(_) => auth.isLoggedIn,
|
|
|
|
|
(_) => _channel?.sink.close(1001),
|
|
|
|
|
));
|
|
|
|
|
|
2022-01-19 23:01:31 -05:00
|
|
|
assetsStore.init();
|
|
|
|
|
|
2021-12-15 23:54:06 -05:00
|
|
|
_messages.add(IRCMessage.createNotice(message: 'Connecting to chat...'));
|
2021-12-13 02:19:06 -05:00
|
|
|
|
2022-01-26 15:42:37 -05:00
|
|
|
connectToChat();
|
2021-12-13 02:19:06 -05: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
|
|
|
}
|
|
|
|
|
});
|
2022-01-23 14:01:16 -05:00
|
|
|
|
2022-04-08 22:50:40 -04:00
|
|
|
// Add a listener to the textfield focus that will hide the emote menu if it is currently shown.
|
2022-01-23 14:01:16 -05:00
|
|
|
textFieldFocusNode.addListener(() {
|
2022-04-08 22:50:40 -04:00
|
|
|
if (textFieldFocusNode.hasFocus && assetsStore.showEmoteMenu) assetsStore.showEmoteMenu = false;
|
2022-01-23 14:01:16 -05:00
|
|
|
});
|
2022-04-01 16:30:35 -04:00
|
|
|
|
|
|
|
|
// Add a listener to the textfield that will show/hide the autocomplete bar if focused.
|
|
|
|
|
// Will also rebuild the autocomplete bar when typing, refreshing the results as the user types.
|
|
|
|
|
textController
|
|
|
|
|
.addListener(() => _showAutocomplete = textFieldFocusNode.hasFocus && textController.text.split(' ').last.isNotEmpty ? true : false);
|
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')) {
|
2022-01-06 09:32:39 -05:00
|
|
|
// debugPrint(message);
|
2021-09-11 02:17:09 -04:00
|
|
|
if (message.startsWith('@')) {
|
2022-01-05 07:23:19 -05:00
|
|
|
final parsedIRCMessage = IRCMessage.fromString(message, userLogin: auth.user.details?.login);
|
2021-10-16 19:11:35 -04:00
|
|
|
|
2021-12-11 15:51:30 -05:00
|
|
|
// Filter messages from any blocked users if not a moderator or not the channel owner.
|
2021-12-11 00:45:44 -05:00
|
|
|
if (!_userState.mod &&
|
2021-12-22 18:25:56 -05:00
|
|
|
channelName != auth.user.details?.login &&
|
|
|
|
|
auth.user.blockedUsers.where((blockedUser) => blockedUser.userLogin == parsedIRCMessage.user).isNotEmpty) continue;
|
2021-12-11 00:45:44 -05:00
|
|
|
|
2021-10-17 02:15:51 -04:00
|
|
|
switch (parsedIRCMessage.command) {
|
2021-10-24 03:53:08 -04:00
|
|
|
case Command.privateMessage:
|
2021-12-15 23:54:06 -05:00
|
|
|
_messages.add(parsedIRCMessage);
|
2021-10-20 05:12:15 -04:00
|
|
|
break;
|
2021-10-24 03:53:08 -04:00
|
|
|
case Command.clearChat:
|
2021-12-16 00:06:28 -05:00
|
|
|
_messages = IRCMessage.clearChat(messages: _messages, ircMessage: parsedIRCMessage).asObservable();
|
2021-10-20 05:12:15 -04:00
|
|
|
break;
|
2021-10-24 03:53:08 -04:00
|
|
|
case Command.clearMessage:
|
2021-12-16 00:06:28 -05:00
|
|
|
_messages = IRCMessage.clearMessage(messages: _messages, ircMessage: parsedIRCMessage).asObservable();
|
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-12-15 23:54:06 -05: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-27 21:13:46 -05:00
|
|
|
chatDetailsStore.roomState = chatDetailsStore.roomState.fromIRCMessage(parsedIRCMessage);
|
2021-11-23 00:47:21 -05:00
|
|
|
continue;
|
2021-10-24 03:53:08 -04:00
|
|
|
case Command.userState:
|
2021-12-17 20:43:03 -05:00
|
|
|
_userState = _userState.fromIRCMessage(parsedIRCMessage);
|
2021-11-23 00:47:21 -05:00
|
|
|
if (toSend != null) {
|
2021-12-15 23:54:06 -05:00
|
|
|
_messages.add(toSend!);
|
2021-11-23 00:47:21 -05:00
|
|
|
toSend = null;
|
|
|
|
|
}
|
2021-10-22 02:46:06 -04:00
|
|
|
break;
|
2021-10-24 03:53:08 -04:00
|
|
|
case Command.globalUserState:
|
2021-12-22 18:25:56 -05:00
|
|
|
final setIds = parsedIRCMessage.tags['emote-sets']?.split(',');
|
2022-01-06 21:27:40 -05:00
|
|
|
if (setIds != null) {
|
2022-01-15 15:46:46 -05:00
|
|
|
assetsStore.userEmotesFuture(
|
|
|
|
|
emoteSets: setIds,
|
|
|
|
|
headers: auth.headersTwitch,
|
|
|
|
|
onError: (error) {
|
|
|
|
|
debugPrint(error.toString());
|
|
|
|
|
return <Emote>[];
|
|
|
|
|
},
|
|
|
|
|
);
|
2022-01-06 21:27:40 -05:00
|
|
|
}
|
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
|
|
|
|
2022-01-05 07:23:19 -05:00
|
|
|
if (_autoScroll) {
|
2022-01-06 03:42:55 -05:00
|
|
|
if (_messages.length >= _messageLimit) _messages.removeAt(0);
|
|
|
|
|
|
2022-05-22 21:57:14 -04:00
|
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
2022-01-05 07:23:19 -05:00
|
|
|
if (scrollController.hasClients) scrollController.jumpTo(scrollController.position.maxScrollExtent);
|
|
|
|
|
});
|
|
|
|
|
}
|
2021-12-28 20:03:37 -05:00
|
|
|
|
2022-01-23 15:02:21 -05:00
|
|
|
// Remove messages when an upper limit is reached.
|
|
|
|
|
// This will prevent an infinite amount of messages when autoscroll is off.
|
|
|
|
|
if (_messages.length >= _messageUpperLimit) _messages.removeRange(0, 500);
|
|
|
|
|
|
2021-12-30 20:14:05 -05:00
|
|
|
// Hard upper-limit of 5000 messages to prevent infinite messages being added when scrolling.
|
2021-10-22 02:46:06 -04:00
|
|
|
} else if (message == 'PING :tmi.twitch.tv') {
|
2022-01-04 22:45:59 -05:00
|
|
|
_channel?.sink.add('PONG :tmi.twitch.tv');
|
2021-10-17 17:56:16 -04:00
|
|
|
return;
|
2022-01-04 22:45:59 -05:00
|
|
|
} else if (message.contains('Welcome, GLHF!')) {
|
2022-01-19 15:12:54 -05:00
|
|
|
_messages
|
|
|
|
|
.add(IRCMessage.createNotice(message: "Connected to $displayName${regexEnglish.hasMatch(displayName) ? '' : ' ($channelName)'}'s chat!"));
|
2022-01-04 22:45:59 -05:00
|
|
|
|
|
|
|
|
// Fetch the assets used in chat including badges and emotes.
|
2022-01-15 15:46:46 -05:00
|
|
|
assetsStore.assetsFuture(
|
|
|
|
|
channelId: channelId,
|
|
|
|
|
headers: auth.headersTwitch,
|
|
|
|
|
onEmoteError: (error) {
|
|
|
|
|
debugPrint(error.toString());
|
|
|
|
|
return <Emote>[];
|
|
|
|
|
},
|
|
|
|
|
onBadgeError: (error) {
|
|
|
|
|
debugPrint(error.toString());
|
|
|
|
|
return <Badge>[];
|
|
|
|
|
},
|
|
|
|
|
);
|
2022-01-04 22:45:59 -05:00
|
|
|
// Reset exponential backoff if successfully connected.
|
|
|
|
|
_retries = 0;
|
|
|
|
|
_backoffTime = 0;
|
2021-10-17 17:56:16 -04:00
|
|
|
}
|
|
|
|
|
}
|
2021-10-24 03:53:08 -04:00
|
|
|
}
|
|
|
|
|
|
2021-10-26 01:06:57 -04:00
|
|
|
/// Re-enables [_autoScroll] and jumps to the latest message.
|
|
|
|
|
@action
|
|
|
|
|
void resumeScroll() {
|
2022-01-06 03:42:55 -05:00
|
|
|
if (_messages.length >= _messageLimit) _messages.removeRange(0, _messages.length - _messageLimit);
|
|
|
|
|
|
2021-10-26 01:06:57 -04:00
|
|
|
_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.
|
2022-05-22 21:57:14 -04:00
|
|
|
WidgetsBinding.instance.addPostFrameCallback((_) => scrollController.jumpTo(scrollController.position.maxScrollExtent));
|
2021-10-26 01:06:57 -04:00
|
|
|
}
|
|
|
|
|
|
2022-01-23 15:02:21 -05:00
|
|
|
@action
|
2022-01-26 15:42:37 -05:00
|
|
|
void connectToChat() {
|
2022-01-04 22:45:59 -05:00
|
|
|
_channel?.sink.close(1001);
|
|
|
|
|
_channel = WebSocketChannel.connect(Uri.parse('wss://irc-ws.chat.twitch.tv:443'));
|
|
|
|
|
|
|
|
|
|
// Listen for new messages and forward them to the handler.
|
|
|
|
|
_channel?.stream.listen(
|
|
|
|
|
(data) => _handleIRCData(data.toString()),
|
2022-01-26 15:42:37 -05:00
|
|
|
onError: (error) => debugPrint('Chat error: ${error.toString()}'),
|
2022-01-04 22:45:59 -05:00
|
|
|
onDone: () async {
|
|
|
|
|
if (_channel == null) return;
|
|
|
|
|
|
2022-01-06 21:27:40 -05:00
|
|
|
if (_backoffTime > 0) {
|
|
|
|
|
// Add notice that chat was disconnected and then wait the backoff time before reconnecting.
|
2022-01-14 18:33:29 -05:00
|
|
|
final notice = 'Disconnected from chat, waiting $_backoffTime ${_backoffTime == 1 ? 'second' : 'seconds'} before reconnecting...';
|
2022-01-06 21:27:40 -05:00
|
|
|
_messages.add(IRCMessage.createNotice(message: notice));
|
|
|
|
|
}
|
|
|
|
|
|
2022-01-04 22:45:59 -05:00
|
|
|
await Future.delayed(Duration(seconds: _backoffTime));
|
|
|
|
|
|
|
|
|
|
// Increase the backoff time for the next retry.
|
|
|
|
|
_backoffTime == 0 ? _backoffTime++ : _backoffTime *= 2;
|
|
|
|
|
|
|
|
|
|
// Increment the retry count and attempt the reconnect.
|
|
|
|
|
_retries++;
|
2022-01-06 21:27:40 -05:00
|
|
|
_messages.add(IRCMessage.createNotice(message: 'Reconnecting to chat (attempt $_retries)...'));
|
2022-01-26 15:42:37 -05:00
|
|
|
connectToChat();
|
2022-01-04 22:45:59 -05:00
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// The list of messages sent to the IRC WebSocket channel to connect and join.
|
|
|
|
|
final commands = [
|
|
|
|
|
// Request the tags and commands capabilities.
|
|
|
|
|
// This will display tags containing metadata along with each IRC message.
|
|
|
|
|
'CAP REQ :twitch.tv/tags twitch.tv/commands',
|
|
|
|
|
|
|
|
|
|
// The OAuth token in order to connect, default or user token.
|
|
|
|
|
'PASS oauth:${auth.token}',
|
|
|
|
|
|
|
|
|
|
// The nickname for the connecting user. 'justinfan888' is the Twitch default if not logged in.
|
|
|
|
|
'NICK ${auth.isLoggedIn ? auth.user.details!.login : 'justinfan888'}',
|
|
|
|
|
|
|
|
|
|
// Join the desired channel's room.
|
|
|
|
|
'JOIN #$channelName',
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
// Send each command in order.
|
|
|
|
|
for (final command in commands) {
|
|
|
|
|
_channel?.sink.add(command);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-12-15 23:54:06 -05:00
|
|
|
/// Sends the given string message by the logged-in user and adds it to [_messages].
|
2022-01-23 14:01:16 -05:00
|
|
|
@action
|
2021-11-25 22:01:02 -05:00
|
|
|
void sendMessage(String message) {
|
|
|
|
|
// Do not send if the message is blank/empty.
|
2022-01-04 22:45:59 -05:00
|
|
|
if (message.isEmpty) return;
|
|
|
|
|
|
|
|
|
|
if (_channel == null || _channel?.closeCode != null) {
|
|
|
|
|
_messages.add(IRCMessage.createNotice(message: 'Failed to send message: disconnected from chat.'));
|
2022-04-08 22:50:40 -04:00
|
|
|
} else {
|
|
|
|
|
// 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.
|
|
|
|
|
var userStateString = _userState.raw;
|
|
|
|
|
if (userStateString != null) {
|
|
|
|
|
if (message.length > 3 && message.substring(0, 3) == '/me') {
|
|
|
|
|
userStateString += ' :\x01ACTION ${message.replaceRange(0, 3, '').trim()}\x01';
|
|
|
|
|
} else {
|
2022-05-12 04:58:03 -04:00
|
|
|
userStateString += ' :${message.trim()}';
|
2022-04-08 22:50:40 -04:00
|
|
|
}
|
2021-11-25 22:01:02 -05:00
|
|
|
|
2022-04-08 22:50:40 -04:00
|
|
|
final userChatMessage = IRCMessage.fromString(userStateString);
|
|
|
|
|
userChatMessage.localEmotes?.addAll(assetsStore.userEmoteToObject);
|
2021-11-25 22:01:02 -05:00
|
|
|
|
2022-04-08 22:50:40 -04:00
|
|
|
toSend = userChatMessage;
|
2022-01-01 02:00:33 -05:00
|
|
|
}
|
2021-12-17 20:43:03 -05:00
|
|
|
|
2022-04-08 22:50:40 -04:00
|
|
|
// Clear the previous input in the TextField.
|
|
|
|
|
textController.clear();
|
2021-11-25 22:01:02 -05:00
|
|
|
}
|
|
|
|
|
|
2022-04-08 22:50:40 -04:00
|
|
|
// Scroll to the latest message after sending.
|
2022-05-22 21:57:14 -04:00
|
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
2022-04-08 22:50:40 -04:00
|
|
|
if (scrollController.hasClients) scrollController.jumpTo(scrollController.position.maxScrollExtent);
|
|
|
|
|
});
|
2021-11-25 22:01:02 -05:00
|
|
|
}
|
|
|
|
|
|
2022-04-01 16:30:35 -04:00
|
|
|
/// Adds the given [emote] to the chat textfield.
|
|
|
|
|
@action
|
|
|
|
|
void addEmote(Emote emote, {bool autocompleteMode = false}) {
|
|
|
|
|
if (textController.text.isEmpty || textController.text.endsWith(' ')) {
|
2022-05-12 04:58:03 -04:00
|
|
|
textController.text += '${emote.name} ';
|
2022-04-01 16:30:35 -04:00
|
|
|
} else if (autocompleteMode && _showAutocomplete && textController.text.endsWith('')) {
|
|
|
|
|
final split = textController.text.split(' ')
|
|
|
|
|
..removeLast()
|
2022-05-12 04:58:03 -04:00
|
|
|
..add('${emote.name} ');
|
2022-04-01 16:30:35 -04:00
|
|
|
|
|
|
|
|
textController.text = split.join(' ');
|
|
|
|
|
} else {
|
2022-05-12 04:58:03 -04:00
|
|
|
textController.text += ' ${emote.name} ';
|
2022-04-01 16:30:35 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
assetsStore.recentEmotes
|
|
|
|
|
..removeWhere((recentEmote) => recentEmote.name == emote.name && recentEmote.type == emote.type)
|
|
|
|
|
..insert(0, emote);
|
|
|
|
|
|
|
|
|
|
textController.selection = TextSelection.fromPosition(TextPosition(offset: textController.text.length));
|
|
|
|
|
}
|
|
|
|
|
|
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() {
|
2022-01-04 22:45:59 -05:00
|
|
|
_channel?.sink.close(1001);
|
|
|
|
|
_channel = null;
|
|
|
|
|
|
2022-01-24 16:03:17 -05:00
|
|
|
for (final reactionDisposer in reactions) {
|
|
|
|
|
reactionDisposer();
|
|
|
|
|
}
|
|
|
|
|
|
2022-01-26 15:42:37 -05:00
|
|
|
textFieldFocusNode.dispose();
|
2021-11-14 04:25:33 -05:00
|
|
|
textController.dispose();
|
|
|
|
|
scrollController.dispose();
|
2022-01-19 23:01:31 -05:00
|
|
|
assetsStore.dispose();
|
2021-10-17 17:56:16 -04:00
|
|
|
}
|
2021-09-03 02:14:17 -04:00
|
|
|
}
|