Merge pull request #144 from tommyxchow/143-autocompletes-for-mentions

143 autocompletes for mentions
This commit is contained in:
Tommy Chow 2022-06-10 16:24:05 -04:00 • committed by GitHub
commit 04e75ee594
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 133 additions and 50 deletions

View file

@ -43,6 +43,8 @@ class _ChattersListState extends State<ChattersList> {
widget.chatDetails.showJumpButton = true;
}
});
widget.chatDetails.updateChatters(widget.userLogin);
}
@override
@ -58,7 +60,6 @@ class _ChattersListState extends State<ChattersList> {
];
final chatDetailsStore = widget.chatDetails;
chatDetailsStore.updateChatters(widget.userLogin);
return Column(
children: [
@ -116,6 +117,7 @@ class _ChattersListState extends State<ChattersList> {
}
return CustomScrollView(
physics: const AlwaysScrollableScrollPhysics(),
controller: _scrollController,
slivers: [
SliverPadding(

View file

@ -14,16 +14,20 @@ class ChatBottomBar extends StatelessWidget {
Widget build(BuildContext context) {
return Observer(
builder: (context) {
final emotes = [
final matchingEmotes = [
...chatStore.assetsStore.userEmoteToObject.values,
...chatStore.assetsStore.bttvEmotes,
...chatStore.assetsStore.ffzEmotes,
...chatStore.assetsStore.sevenTVEmotes
].where((emote) => emote.name.toLowerCase().contains(chatStore.textController.text.split(' ').last.toLowerCase())).toList();
final matchingChatters = chatStore.chatDetailsStore.allChatters
.where((chatter) => chatter.contains(chatStore.textController.text.split(' ').last.replaceFirst('@', '').toLowerCase()))
.toList();
return Column(
children: [
if (chatStore.settings.emoteAutocomplete && chatStore.showAutocomplete && emotes.isNotEmpty) ...[
if (chatStore.settings.autocomplete && chatStore.showEmoteAutocomplete && matchingEmotes.isNotEmpty) ...[
const Divider(
height: 1.0,
thickness: 1.0,
@ -32,27 +36,52 @@ class ChatBottomBar extends StatelessWidget {
height: 50,
child: ListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 5.0),
itemCount: emotes.length,
itemCount: matchingEmotes.length,
scrollDirection: Axis.horizontal,
itemBuilder: (context, index) => InkWell(
onTap: () => chatStore.addEmote(emotes[index], autocompleteMode: true),
onTap: () => chatStore.addEmote(matchingEmotes[index], autocompleteMode: true),
child: Tooltip(
message: emotes[index].name,
message: matchingEmotes[index].name,
preferBelow: false,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 5.0),
child: Center(
child: CachedNetworkImage(
imageUrl: emotes[index].url,
imageUrl: matchingEmotes[index].url,
fadeInDuration: const Duration(),
height: emotes[index].height?.toDouble() ?? defaultEmoteSize,
width: emotes[index].width?.toDouble(),
height: matchingEmotes[index].height?.toDouble() ?? defaultEmoteSize,
width: matchingEmotes[index].width?.toDouble(),
),
),
),
),
),
),
),
],
if (chatStore.settings.autocomplete && chatStore.showMentionAutocomplete && matchingChatters.isNotEmpty) ...[
const Divider(
height: 1.0,
thickness: 1.0,
),
SizedBox(
height: 50,
child: ListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 5.0),
itemCount: matchingChatters.length,
scrollDirection: Axis.horizontal,
itemBuilder: (context, index) => TextButton(
onPressed: () {
final split = chatStore.textController.text.split(' ')
..removeLast()
..add('@${matchingChatters[index]} ');
chatStore.textController.text = split.join(' ');
chatStore.textController.selection = TextSelection.fromPosition(TextPosition(offset: chatStore.textController.text.length));
},
child: Text(matchingChatters[index]),
),
),
)
],
Row(

View file

@ -23,6 +23,10 @@ abstract class ChatDetailsStoreBase with Store {
@observable
var filterText = '';
/// The list and types of chatters in the chat room.
@readonly
ChatUsers? _chatUsers;
@computed
Iterable<List<String>> get filteredUsers => [
_chatUsers!.chatters.broadcaster,
@ -34,9 +38,16 @@ abstract class ChatDetailsStoreBase with Store {
_chatUsers!.chatters.viewers,
].map((e) => e.where((user) => user.contains(filterText)).toList());
/// The list and types of chatters in the chat room.
@readonly
ChatUsers? _chatUsers;
@computed
List<String> get allChatters => [
...?_chatUsers?.chatters.broadcaster,
...?_chatUsers?.chatters.staff,
...?_chatUsers?.chatters.admins,
...?_chatUsers?.chatters.globalMods,
...?_chatUsers?.chatters.moderators,
...?_chatUsers?.chatters.vips,
...?_chatUsers?.chatters.viewers,
];
@readonly
String? _error;

View file

@ -16,6 +16,13 @@ mixin _$ChatDetailsStore on ChatDetailsStoreBase, Store {
Computed<Iterable<List<String>>>(() => super.filteredUsers,
name: 'ChatDetailsStoreBase.filteredUsers'))
.value;
Computed<List<String>>? _$allChattersComputed;
@override
List<String> get allChatters =>
(_$allChattersComputed ??= Computed<List<String>>(() => super.allChatters,
name: 'ChatDetailsStoreBase.allChatters'))
.value;
late final _$roomStateAtom =
Atom(name: 'ChatDetailsStoreBase.roomState', context: context);
@ -116,7 +123,8 @@ mixin _$ChatDetailsStore on ChatDetailsStoreBase, Store {
roomState: ${roomState},
showJumpButton: ${showJumpButton},
filterText: ${filterText},
filteredUsers: ${filteredUsers}
filteredUsers: ${filteredUsers},
allChatters: ${allChatters}
''';
}
}

View file

@ -72,7 +72,10 @@ abstract class ChatStoreBase with Store {
var _autoScroll = true;
@readonly
var _showAutocomplete = false;
var _showEmoteAutocomplete = false;
@readonly
var _showMentionAutocomplete = false;
/// The logged-in user's appearance in chat.
@readonly
@ -100,6 +103,7 @@ abstract class ChatStoreBase with Store {
));
assetsStore.init();
chatDetailsStore.updateChatters(channelName);
_messages.add(IRCMessage.createNotice(message: 'Connecting to chat...'));
@ -121,17 +125,25 @@ abstract class ChatStoreBase with Store {
}
});
// Add a listener to the textfield focus that will hide the emote menu if it is currently shown.
textFieldFocusNode.addListener(() {
if (textFieldFocusNode.hasFocus && assetsStore.showEmoteMenu) assetsStore.showEmoteMenu = false;
if (textFieldFocusNode.hasFocus) {
// Hide the emote menu if it is currently shown.
if (assetsStore.showEmoteMenu) assetsStore.showEmoteMenu = false;
// Refresh the chatters list to keep autocomplete mentions updated.
if (settings.autocomplete) chatDetailsStore.updateChatters(channelName);
}
// Un-expand the chat when unfocusing.
if (!textFieldFocusNode.hasFocus) expandChat = false;
});
// 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);
textController.addListener(
() => _showEmoteAutocomplete = !_showMentionAutocomplete && textFieldFocusNode.hasFocus && textController.text.split(' ').last.isNotEmpty);
textController.addListener(() => _showMentionAutocomplete = textFieldFocusNode.hasFocus && textController.text.split(' ').last.startsWith('@'));
}
/// Handle and process the provided string-representation of the IRC data.
@ -342,7 +354,7 @@ abstract class ChatStoreBase with Store {
void addEmote(Emote emote, {bool autocompleteMode = false}) {
if (textController.text.isEmpty || textController.text.endsWith(' ')) {
textController.text += '${emote.name} ';
} else if (autocompleteMode && _showAutocomplete && textController.text.endsWith('')) {
} else if (autocompleteMode && _showEmoteAutocomplete && textController.text.endsWith('')) {
final split = textController.text.split(' ')
..removeLast()
..add('${emote.name} ');

View file

@ -45,21 +45,41 @@ mixin _$ChatStore on ChatStoreBase, Store {
});
}
late final _$_showAutocompleteAtom =
Atom(name: 'ChatStoreBase._showAutocomplete', context: context);
late final _$_showEmoteAutocompleteAtom =
Atom(name: 'ChatStoreBase._showEmoteAutocomplete', context: context);
bool get showAutocomplete {
_$_showAutocompleteAtom.reportRead();
return super._showAutocomplete;
bool get showEmoteAutocomplete {
_$_showEmoteAutocompleteAtom.reportRead();
return super._showEmoteAutocomplete;
}
@override
bool get _showAutocomplete => showAutocomplete;
bool get _showEmoteAutocomplete => showEmoteAutocomplete;
@override
set _showAutocomplete(bool value) {
_$_showAutocompleteAtom.reportWrite(value, super._showAutocomplete, () {
super._showAutocomplete = value;
set _showEmoteAutocomplete(bool value) {
_$_showEmoteAutocompleteAtom
.reportWrite(value, super._showEmoteAutocomplete, () {
super._showEmoteAutocomplete = value;
});
}
late final _$_showMentionAutocompleteAtom =
Atom(name: 'ChatStoreBase._showMentionAutocomplete', context: context);
bool get showMentionAutocomplete {
_$_showMentionAutocompleteAtom.reportRead();
return super._showMentionAutocomplete;
}
@override
bool get _showMentionAutocomplete => showMentionAutocomplete;
@override
set _showMentionAutocomplete(bool value) {
_$_showMentionAutocompleteAtom
.reportWrite(value, super._showMentionAutocomplete, () {
super._showMentionAutocomplete = value;
});
}

View file

@ -38,6 +38,14 @@ class _ChatSettingsState extends State<ChatSettings> {
),
),
children: [
SwitchListTile.adaptive(
isThreeLine: true,
title: const Text('Autocomplete'),
subtitle: const Text('Shows a bar that suggests matching emotes and mentions when typing.'),
value: settingsStore.autocomplete,
onChanged: settingsStore.showBottomBar ? (newValue) => settingsStore.autocomplete = newValue : null,
),
const SizedBox(height: 15.0),
ListTile(
title: Row(
children: [
@ -108,13 +116,6 @@ class _ChatSettingsState extends State<ChatSettings> {
fontSize: 12.0,
padding: sectionPadding,
),
SwitchListTile.adaptive(
isThreeLine: true,
title: const Text('Emote Autocomplete'),
subtitle: const Text('Shows a bar that suggests matching emotes when typing.'),
value: settingsStore.emoteAutocomplete,
onChanged: settingsStore.showBottomBar ? (newValue) => settingsStore.emoteAutocomplete = newValue : null,
),
SwitchListTile.adaptive(
isThreeLine: true,
title: const Text('Zero-Width Emotes'),

View file

@ -91,7 +91,7 @@ abstract class _SettingsStoreBase with Store {
static const defaultEmoteScale = 1.0;
static const defaultEmoteAutocomplete = true;
static const defaultAutocomplete = true;
static const defaultChatWidth = 0.3;
@ -145,9 +145,9 @@ abstract class _SettingsStoreBase with Store {
@observable
var emoteScale = defaultEmoteScale;
@JsonKey(defaultValue: defaultEmoteAutocomplete)
@JsonKey(defaultValue: defaultAutocomplete)
@observable
var emoteAutocomplete = defaultEmoteAutocomplete;
var autocomplete = defaultAutocomplete;
@JsonKey(defaultValue: defaultChatWidth)
@observable
@ -218,7 +218,7 @@ abstract class _SettingsStoreBase with Store {
messageSpacing = defaultMessageSpacing;
badgeScale = defaultBadgeScale;
emoteScale = defaultEmoteScale;
emoteAutocomplete = defaultEmoteAutocomplete;
autocomplete = defaultAutocomplete;
chatWidth = defaultChatWidth;
landscapeChatLeftSide = false;
chatDelay = defaultChatDelay;

View file

@ -34,7 +34,7 @@ SettingsStore _$SettingsStoreFromJson(Map<String, dynamic> json) =>
..messageScale = (json['messageScale'] as num?)?.toDouble() ?? 1.0
..badgeScale = (json['badgeScale'] as num?)?.toDouble() ?? 1.0
..emoteScale = (json['emoteScale'] as num?)?.toDouble() ?? 1.0
..emoteAutocomplete = json['emoteAutocomplete'] as bool? ?? true
..autocomplete = json['autocomplete'] as bool? ?? true
..chatWidth = (json['chatWidth'] as num?)?.toDouble() ?? 0.3
..landscapeChatLeftSide = json['landscapeChatLeftSide'] as bool? ?? false
..fullScreenChatOverlayOpacity =
@ -67,7 +67,7 @@ Map<String, dynamic> _$SettingsStoreToJson(SettingsStore instance) =>
'messageScale': instance.messageScale,
'badgeScale': instance.badgeScale,
'emoteScale': instance.emoteScale,
'emoteAutocomplete': instance.emoteAutocomplete,
'autocomplete': instance.autocomplete,
'chatWidth': instance.chatWidth,
'landscapeChatLeftSide': instance.landscapeChatLeftSide,
'fullScreenChatOverlayOpacity': instance.fullScreenChatOverlayOpacity,
@ -419,19 +419,19 @@ mixin _$SettingsStore on _SettingsStoreBase, Store {
});
}
late final _$emoteAutocompleteAtom =
Atom(name: '_SettingsStoreBase.emoteAutocomplete', context: context);
late final _$autocompleteAtom =
Atom(name: '_SettingsStoreBase.autocomplete', context: context);
@override
bool get emoteAutocomplete {
_$emoteAutocompleteAtom.reportRead();
return super.emoteAutocomplete;
bool get autocomplete {
_$autocompleteAtom.reportRead();
return super.autocomplete;
}
@override
set emoteAutocomplete(bool value) {
_$emoteAutocompleteAtom.reportWrite(value, super.emoteAutocomplete, () {
super.emoteAutocomplete = value;
set autocomplete(bool value) {
_$autocompleteAtom.reportWrite(value, super.autocomplete, () {
super.autocomplete = value;
});
}
@ -604,7 +604,7 @@ messageSpacing: ${messageSpacing},
messageScale: ${messageScale},
badgeScale: ${badgeScale},
emoteScale: ${emoteScale},
emoteAutocomplete: ${emoteAutocomplete},
autocomplete: ${autocomplete},
chatWidth: ${chatWidth},
landscapeChatLeftSide: ${landscapeChatLeftSide},
fullScreenChatOverlayOpacity: ${fullScreenChatOverlayOpacity},