Merge pull request #144 from tommyxchow/143-autocompletes-for-mentions
143 autocompletes for mentions
This commit is contained in:
commit
04e75ee594
9 changed files with 133 additions and 50 deletions
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
''';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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} ');
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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},
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue