Obtain chatters based on chat messages (#262)
* Remove chatters API endpoint * Get chatters based on chat messages
This commit is contained in:
parent
3af5406f60
commit
37297870c5
7 changed files with 187 additions and 370 deletions
|
|
@ -5,7 +5,6 @@ import 'package:frosty/constants.dart';
|
|||
import 'package:frosty/models/badges.dart';
|
||||
import 'package:frosty/models/category.dart';
|
||||
import 'package:frosty/models/channel.dart';
|
||||
import 'package:frosty/models/chatters.dart';
|
||||
import 'package:frosty/models/emotes.dart';
|
||||
import 'package:frosty/models/stream.dart';
|
||||
import 'package:frosty/models/user.dart';
|
||||
|
|
@ -421,21 +420,6 @@ class TwitchApi {
|
|||
}
|
||||
}
|
||||
|
||||
/// Returns a [ChatUsers] object containing the names of chatters in the given [userLogin]'s chat.
|
||||
Future<ChatUsers> getChatters({required String userLogin}) async {
|
||||
final uri =
|
||||
Uri.parse('https://tmi.twitch.tv/group/user/$userLogin/chatters');
|
||||
|
||||
final response = await _client.get(uri);
|
||||
if (response.statusCode == 200) {
|
||||
final decoded = jsonDecode(response.body);
|
||||
|
||||
return ChatUsers.fromJson(decoded);
|
||||
} else {
|
||||
return Future.error('Failed to get chatters');
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a user's list of blocked users given their id.
|
||||
Future<List<UserBlockedTwitch>> getUserBlockedList({
|
||||
required String id,
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import 'package:frosty/widgets/button.dart';
|
|||
import 'package:frosty/widgets/dialog.dart';
|
||||
import 'package:frosty/widgets/list_tile.dart';
|
||||
|
||||
class ChatDetails extends StatefulWidget {
|
||||
class ChatDetails extends StatelessWidget {
|
||||
final ChatDetailsStore chatDetailsStore;
|
||||
final ChatStore chatStore;
|
||||
final String userLogin;
|
||||
|
|
@ -25,11 +25,6 @@ class ChatDetails extends StatefulWidget {
|
|||
required this.userLogin,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<ChatDetails> createState() => _ChatDetailsState();
|
||||
}
|
||||
|
||||
class _ChatDetailsState extends State<ChatDetails> {
|
||||
Future<void> _showSleepTimerDialog(BuildContext context) {
|
||||
return showDialog(
|
||||
context: context,
|
||||
|
|
@ -40,15 +35,15 @@ class _ChatDetailsState extends State<ChatDetails> {
|
|||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Opacity(
|
||||
opacity: widget.chatStore.sleepTimer != null &&
|
||||
widget.chatStore.sleepTimer!.isActive
|
||||
opacity: chatStore.sleepTimer != null &&
|
||||
chatStore.sleepTimer!.isActive
|
||||
? 1.0
|
||||
: 0.5,
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.timer_rounded),
|
||||
Text(
|
||||
' ${widget.chatStore.timeRemaining.toString().split('.')[0]}',
|
||||
' ${chatStore.timeRemaining.toString().split('.')[0]}',
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontFeatures: [FontFeature.tabularFigures()]),
|
||||
|
|
@ -56,7 +51,7 @@ class _ChatDetailsState extends State<ChatDetails> {
|
|||
const Spacer(),
|
||||
IconButton(
|
||||
tooltip: 'Cancel sleep timer',
|
||||
onPressed: widget.chatStore.cancelSleepTimer,
|
||||
onPressed: chatStore.cancelSleepTimer,
|
||||
icon: const Icon(Icons.cancel_rounded),
|
||||
),
|
||||
],
|
||||
|
|
@ -65,13 +60,12 @@ class _ChatDetailsState extends State<ChatDetails> {
|
|||
Row(
|
||||
children: [
|
||||
DropdownButton(
|
||||
value: widget.chatStore.sleepHours,
|
||||
value: chatStore.sleepHours,
|
||||
items: List.generate(24, (index) => index)
|
||||
.map((e) => DropdownMenuItem(
|
||||
value: e, child: Text(e.toString())))
|
||||
.toList(),
|
||||
onChanged: (int? hours) =>
|
||||
widget.chatStore.sleepHours = hours!,
|
||||
onChanged: (int? hours) => chatStore.sleepHours = hours!,
|
||||
menuMaxHeight: 200,
|
||||
),
|
||||
const SizedBox(width: 10.0),
|
||||
|
|
@ -81,13 +75,13 @@ class _ChatDetailsState extends State<ChatDetails> {
|
|||
Row(
|
||||
children: [
|
||||
DropdownButton(
|
||||
value: widget.chatStore.sleepMinutes,
|
||||
value: chatStore.sleepMinutes,
|
||||
items: List.generate(60, (index) => index)
|
||||
.map((e) => DropdownMenuItem(
|
||||
value: e, child: Text(e.toString())))
|
||||
.toList(),
|
||||
onChanged: (int? minutes) =>
|
||||
widget.chatStore.sleepMinutes = minutes!,
|
||||
chatStore.sleepMinutes = minutes!,
|
||||
menuMaxHeight: 200,
|
||||
),
|
||||
const SizedBox(width: 10.0),
|
||||
|
|
@ -100,13 +94,13 @@ class _ChatDetailsState extends State<ChatDetails> {
|
|||
actions: [
|
||||
Observer(
|
||||
builder: (context) => Button(
|
||||
onPressed: widget.chatStore.sleepHours == 0 &&
|
||||
widget.chatStore.sleepMinutes == 0
|
||||
? null
|
||||
: () => widget.chatStore.updateSleepTimer(
|
||||
onTimerFinished: () => navigatorKey.currentState
|
||||
?.popUntil((route) => route.isFirst),
|
||||
),
|
||||
onPressed:
|
||||
chatStore.sleepHours == 0 && chatStore.sleepMinutes == 0
|
||||
? null
|
||||
: () => chatStore.updateSleepTimer(
|
||||
onTimerFinished: () => navigatorKey.currentState
|
||||
?.popUntil((route) => route.isFirst),
|
||||
),
|
||||
child: const Text('Set timer'),
|
||||
),
|
||||
),
|
||||
|
|
@ -120,7 +114,7 @@ class _ChatDetailsState extends State<ChatDetails> {
|
|||
);
|
||||
}
|
||||
|
||||
Future<void> _showClearDialog() {
|
||||
Future<void> _showClearDialog(BuildContext context) {
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (context) => FrostyDialog(
|
||||
|
|
@ -128,10 +122,7 @@ class _ChatDetailsState extends State<ChatDetails> {
|
|||
message: 'Are you sure you want to clear your recent emotes?',
|
||||
actions: [
|
||||
Button(
|
||||
onPressed: () {
|
||||
setState(widget.chatStore.assetsStore.recentEmotes.clear);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
onPressed: Navigator.of(context).pop,
|
||||
child: const Text('Yes'),
|
||||
),
|
||||
Button(
|
||||
|
|
@ -144,17 +135,11 @@ class _ChatDetailsState extends State<ChatDetails> {
|
|||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
widget.chatDetailsStore.updateChatters();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final children = [
|
||||
ListTile(
|
||||
title: ChatModes(roomState: widget.chatDetailsStore.roomState),
|
||||
title: ChatModes(roomState: chatDetailsStore.roomState),
|
||||
),
|
||||
FrostyListTile(
|
||||
leading: const Icon(Icons.people_outline),
|
||||
|
|
@ -169,9 +154,9 @@ class _ChatDetailsState extends State<ChatDetails> {
|
|||
child: GestureDetector(
|
||||
onTap: FocusScope.of(context).unfocus,
|
||||
child: ChattersList(
|
||||
chatDetailsStore: widget.chatDetailsStore,
|
||||
chatStore: widget.chatStore,
|
||||
userLogin: widget.userLogin,
|
||||
chatDetailsStore: chatDetailsStore,
|
||||
chatStore: chatStore,
|
||||
userLogin: userLogin,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
@ -186,24 +171,24 @@ class _ChatDetailsState extends State<ChatDetails> {
|
|||
FrostyListTile(
|
||||
leading: const Icon(Icons.delete_outline_rounded),
|
||||
title: 'Clear recent emotes',
|
||||
onTap: _showClearDialog,
|
||||
onTap: () => _showClearDialog(context),
|
||||
),
|
||||
FrostyListTile(
|
||||
leading: const Icon(Icons.refresh_rounded),
|
||||
title: 'Reconnect to chat',
|
||||
onTap: () {
|
||||
widget.chatStore.updateNotification('Reconnecting to chat...');
|
||||
chatStore.updateNotification('Reconnecting to chat...');
|
||||
|
||||
widget.chatStore.connectToChat();
|
||||
chatStore.connectToChat();
|
||||
},
|
||||
),
|
||||
FrostyListTile(
|
||||
leading: const Icon(Icons.refresh_rounded),
|
||||
title: 'Refresh badges and emotes',
|
||||
onTap: () async {
|
||||
await widget.chatStore.getAssets();
|
||||
await chatStore.getAssets();
|
||||
|
||||
widget.chatStore.updateNotification('Badges and emotes refreshed');
|
||||
chatStore.updateNotification('Badges and emotes refreshed');
|
||||
},
|
||||
),
|
||||
FrostyListTile(
|
||||
|
|
@ -212,8 +197,7 @@ class _ChatDetailsState extends State<ChatDetails> {
|
|||
onTap: () => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) =>
|
||||
Settings(settingsStore: widget.chatStore.settings),
|
||||
builder: (context) => Settings(settingsStore: chatStore.settings),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,8 +1,5 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:frosty/apis/twitch_api.dart';
|
||||
import 'package:frosty/models/chatters.dart';
|
||||
import 'package:frosty/models/irc.dart';
|
||||
import 'package:mobx/mobx.dart';
|
||||
|
||||
|
|
@ -37,33 +34,11 @@ abstract class ChatDetailsStoreBase with Store {
|
|||
var _filterText = '';
|
||||
|
||||
/// The list and types of chatters in the chat room.
|
||||
@readonly
|
||||
ChatUsers? _chatUsers;
|
||||
final chatUsers = <String>{};
|
||||
|
||||
@computed
|
||||
Iterable<List<String>> get filteredUsers => [
|
||||
_chatUsers!.chatters.broadcaster,
|
||||
_chatUsers!.chatters.staff,
|
||||
_chatUsers!.chatters.admins,
|
||||
_chatUsers!.chatters.globalMods,
|
||||
_chatUsers!.chatters.moderators,
|
||||
_chatUsers!.chatters.vips,
|
||||
_chatUsers!.chatters.viewers,
|
||||
].map((e) => e.where((user) => user.contains(_filterText)).toList());
|
||||
|
||||
@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;
|
||||
Iterable<String> get filteredUsers =>
|
||||
chatUsers.where((user) => user.contains(_filterText));
|
||||
|
||||
ChatDetailsStoreBase({required this.twitchApi, required this.channelName}) {
|
||||
scrollController.addListener(() {
|
||||
|
|
@ -76,21 +51,6 @@ abstract class ChatDetailsStoreBase with Store {
|
|||
});
|
||||
|
||||
textController.addListener(() => _filterText = textController.text);
|
||||
|
||||
updateChatters();
|
||||
}
|
||||
|
||||
@action
|
||||
Future<void> updateChatters() async {
|
||||
try {
|
||||
_chatUsers = await twitchApi.getChatters(userLogin: channelName);
|
||||
_error = null;
|
||||
} on SocketException {
|
||||
_error = 'Failed to connect';
|
||||
} catch (e) {
|
||||
debugPrint(e.toString());
|
||||
_error = e.toString();
|
||||
}
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
|
|
|
|||
|
|
@ -9,20 +9,13 @@ part of 'chat_details_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, no_leading_underscores_for_local_identifiers
|
||||
|
||||
mixin _$ChatDetailsStore on ChatDetailsStoreBase, Store {
|
||||
Computed<Iterable<List<String>>>? _$filteredUsersComputed;
|
||||
Computed<Iterable<String>>? _$filteredUsersComputed;
|
||||
|
||||
@override
|
||||
Iterable<List<String>> get filteredUsers => (_$filteredUsersComputed ??=
|
||||
Computed<Iterable<List<String>>>(() => super.filteredUsers,
|
||||
Iterable<String> get filteredUsers => (_$filteredUsersComputed ??=
|
||||
Computed<Iterable<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);
|
||||
|
|
@ -74,57 +67,12 @@ mixin _$ChatDetailsStore on ChatDetailsStoreBase, Store {
|
|||
});
|
||||
}
|
||||
|
||||
late final _$_chatUsersAtom =
|
||||
Atom(name: 'ChatDetailsStoreBase._chatUsers', context: context);
|
||||
|
||||
ChatUsers? get chatUsers {
|
||||
_$_chatUsersAtom.reportRead();
|
||||
return super._chatUsers;
|
||||
}
|
||||
|
||||
@override
|
||||
ChatUsers? get _chatUsers => chatUsers;
|
||||
|
||||
@override
|
||||
set _chatUsers(ChatUsers? value) {
|
||||
_$_chatUsersAtom.reportWrite(value, super._chatUsers, () {
|
||||
super._chatUsers = value;
|
||||
});
|
||||
}
|
||||
|
||||
late final _$_errorAtom =
|
||||
Atom(name: 'ChatDetailsStoreBase._error', context: context);
|
||||
|
||||
String? get error {
|
||||
_$_errorAtom.reportRead();
|
||||
return super._error;
|
||||
}
|
||||
|
||||
@override
|
||||
String? get _error => error;
|
||||
|
||||
@override
|
||||
set _error(String? value) {
|
||||
_$_errorAtom.reportWrite(value, super._error, () {
|
||||
super._error = value;
|
||||
});
|
||||
}
|
||||
|
||||
late final _$updateChattersAsyncAction =
|
||||
AsyncAction('ChatDetailsStoreBase.updateChatters', context: context);
|
||||
|
||||
@override
|
||||
Future<void> updateChatters() {
|
||||
return _$updateChattersAsyncAction.run(() => super.updateChatters());
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return '''
|
||||
roomState: ${roomState},
|
||||
showJumpButton: ${showJumpButton},
|
||||
filteredUsers: ${filteredUsers},
|
||||
allChatters: ${allChatters}
|
||||
filteredUsers: ${filteredUsers}
|
||||
''';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
|
|
@ -8,10 +7,7 @@ import 'package:frosty/screens/channel/chat/stores/chat_store.dart';
|
|||
import 'package:frosty/screens/channel/chat/widgets/chat_user_modal.dart';
|
||||
import 'package:frosty/screens/settings/stores/auth_store.dart';
|
||||
import 'package:frosty/widgets/alert_message.dart';
|
||||
import 'package:frosty/widgets/button.dart';
|
||||
import 'package:frosty/widgets/loading_indicator.dart';
|
||||
import 'package:frosty/widgets/scroll_to_top_button.dart';
|
||||
import 'package:frosty/widgets/section_header.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
|
|
@ -34,210 +30,155 @@ class ChattersList extends StatefulWidget {
|
|||
class _ChattersListState extends State<ChattersList> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
const headers = [
|
||||
'Broadcaster',
|
||||
'Staff',
|
||||
'Admins',
|
||||
'Global moderators',
|
||||
'Moderators',
|
||||
'VIPs',
|
||||
'Viewers',
|
||||
];
|
||||
|
||||
return Observer(
|
||||
builder: (context) {
|
||||
if (widget.chatDetailsStore.error != null) {
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const AlertMessage(
|
||||
message: 'Failed to get chatters',
|
||||
),
|
||||
const SizedBox(height: 10.0),
|
||||
Button(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 15.0),
|
||||
onPressed: widget.chatDetailsStore.updateChatters,
|
||||
child: const Text('Try again'),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
if (widget.chatDetailsStore.chatUsers == null) {
|
||||
return const LoadingIndicator(subtitle: 'Getting chatters...');
|
||||
}
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(10.0),
|
||||
child: Observer(
|
||||
builder: (context) {
|
||||
return TextField(
|
||||
controller: widget.chatDetailsStore.textController,
|
||||
focusNode: widget.chatDetailsStore.textFieldFocusNode,
|
||||
autocorrect: false,
|
||||
decoration: InputDecoration(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
prefixIcon: const Icon(Icons.filter_list_rounded),
|
||||
hintText: 'Filter chatters',
|
||||
suffixIcon: widget.chatDetailsStore.textFieldFocusNode
|
||||
.hasFocus ||
|
||||
widget.chatDetailsStore.filterText.isNotEmpty
|
||||
? IconButton(
|
||||
tooltip:
|
||||
widget.chatDetailsStore.filterText.isEmpty
|
||||
? 'Cancel'
|
||||
: 'Clear',
|
||||
onPressed: () {
|
||||
if (widget
|
||||
.chatDetailsStore.filterText.isEmpty) {
|
||||
widget.chatDetailsStore.textFieldFocusNode
|
||||
.unfocus();
|
||||
}
|
||||
widget.chatDetailsStore.textController.clear();
|
||||
},
|
||||
icon: const Icon(Icons.close_rounded),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
HapticFeedback.lightImpact();
|
||||
|
||||
widget.chatDetailsStore.updateChatters();
|
||||
},
|
||||
child: Stack(
|
||||
alignment: AlignmentDirectional.bottomCenter,
|
||||
children: [
|
||||
Observer(
|
||||
builder: (context) {
|
||||
return CustomScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
controller: widget.chatDetailsStore.scrollController,
|
||||
slivers: [
|
||||
if (widget.chatDetailsStore.filterText.isEmpty)
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 10.0, right: 10.0, top: 20.0),
|
||||
sliver: SliverToBoxAdapter(
|
||||
child: Text(
|
||||
'${NumberFormat().format(widget.chatDetailsStore.chatUsers?.chatterCount)} in chat',
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 18.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (widget
|
||||
.chatDetailsStore.chatUsers?.chatterCount ==
|
||||
0)
|
||||
const SliverFillRemaining(
|
||||
hasScrollBody: false,
|
||||
child: Center(
|
||||
child: AlertMessage(
|
||||
message: 'No chatters found'),
|
||||
),
|
||||
)
|
||||
else if (widget.chatDetailsStore.filteredUsers
|
||||
.expand((element) => element)
|
||||
.isEmpty)
|
||||
const SliverFillRemaining(
|
||||
hasScrollBody: false,
|
||||
child: Center(
|
||||
child: AlertMessage(
|
||||
message: 'No matching chatters'),
|
||||
),
|
||||
)
|
||||
else
|
||||
...widget.chatDetailsStore.filteredUsers
|
||||
.expandIndexed(
|
||||
(index, users) => [
|
||||
if (users.isNotEmpty) ...[
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 10.0, right: 10.0, top: 20.0),
|
||||
sliver: SliverToBoxAdapter(
|
||||
child: SectionHeader(
|
||||
headers[index],
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 5.0),
|
||||
),
|
||||
),
|
||||
),
|
||||
SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) => InkWell(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 10.0,
|
||||
vertical: 5.0),
|
||||
child: Text(users[index]),
|
||||
),
|
||||
onLongPress: () async {
|
||||
HapticFeedback.lightImpact();
|
||||
|
||||
final userInfo = await context
|
||||
.read<TwitchApi>()
|
||||
.getUser(
|
||||
headers: context
|
||||
.read<AuthStore>()
|
||||
.headersTwitch,
|
||||
userLogin: users[index]);
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
showModalBottomSheet(
|
||||
backgroundColor:
|
||||
Colors.transparent,
|
||||
isScrollControlled: true,
|
||||
context: context,
|
||||
builder: (context) =>
|
||||
ChatUserModal(
|
||||
chatStore: widget.chatStore,
|
||||
username: userInfo.login,
|
||||
userId: userInfo.id,
|
||||
displayName:
|
||||
userInfo.displayName,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
childCount: users.length,
|
||||
),
|
||||
),
|
||||
]
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
Observer(
|
||||
builder: (context) => AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
switchInCurve: Curves.easeOut,
|
||||
switchOutCurve: Curves.easeIn,
|
||||
child: widget.chatDetailsStore.showJumpButton
|
||||
? ScrollToTopButton(
|
||||
scrollController:
|
||||
widget.chatDetailsStore.scrollController)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
],
|
||||
return Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(10.0),
|
||||
child: Observer(
|
||||
builder: (context) {
|
||||
return TextField(
|
||||
controller: widget.chatDetailsStore.textController,
|
||||
focusNode: widget.chatDetailsStore.textFieldFocusNode,
|
||||
autocorrect: false,
|
||||
decoration: InputDecoration(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
prefixIcon: const Icon(Icons.filter_list_rounded),
|
||||
hintText: 'Filter chatters',
|
||||
suffixIcon: widget
|
||||
.chatDetailsStore.textFieldFocusNode.hasFocus ||
|
||||
widget.chatDetailsStore.filterText.isNotEmpty
|
||||
? IconButton(
|
||||
tooltip: widget.chatDetailsStore.filterText.isEmpty
|
||||
? 'Cancel'
|
||||
: 'Clear',
|
||||
onPressed: () {
|
||||
if (widget.chatDetailsStore.filterText.isEmpty) {
|
||||
widget.chatDetailsStore.textFieldFocusNode
|
||||
.unfocus();
|
||||
}
|
||||
widget.chatDetailsStore.textController.clear();
|
||||
},
|
||||
icon: const Icon(Icons.close_rounded),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
HapticFeedback.lightImpact();
|
||||
|
||||
setState(() {});
|
||||
},
|
||||
child: Stack(
|
||||
alignment: AlignmentDirectional.bottomCenter,
|
||||
children: [
|
||||
Observer(
|
||||
builder: (context) {
|
||||
return CustomScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
controller: widget.chatDetailsStore.scrollController,
|
||||
slivers: [
|
||||
if (widget.chatDetailsStore.filterText.isEmpty)
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 10.0,
|
||||
right: 10.0,
|
||||
top: 20.0,
|
||||
bottom: 10.0,
|
||||
),
|
||||
sliver: SliverToBoxAdapter(
|
||||
child: Text(
|
||||
'${NumberFormat().format(widget.chatDetailsStore.chatUsers.length)} chatters found',
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 18.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (widget.chatDetailsStore.chatUsers.isEmpty)
|
||||
const SliverFillRemaining(
|
||||
hasScrollBody: false,
|
||||
child: Center(
|
||||
child: AlertMessage(message: 'No chatters found'),
|
||||
),
|
||||
)
|
||||
else if (widget.chatDetailsStore.filteredUsers.isEmpty)
|
||||
const SliverFillRemaining(
|
||||
hasScrollBody: false,
|
||||
child: Center(
|
||||
child:
|
||||
AlertMessage(message: 'No matching chatters'),
|
||||
),
|
||||
)
|
||||
else
|
||||
SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) => InkWell(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 10.0, vertical: 5.0),
|
||||
child: Text(widget
|
||||
.chatDetailsStore.filteredUsers
|
||||
.elementAt(index)),
|
||||
),
|
||||
onLongPress: () async {
|
||||
HapticFeedback.lightImpact();
|
||||
|
||||
final userInfo = await context
|
||||
.read<TwitchApi>()
|
||||
.getUser(
|
||||
headers: context
|
||||
.read<AuthStore>()
|
||||
.headersTwitch,
|
||||
userLogin: widget
|
||||
.chatDetailsStore.filteredUsers
|
||||
.elementAt(index));
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
showModalBottomSheet(
|
||||
backgroundColor: Colors.transparent,
|
||||
isScrollControlled: true,
|
||||
context: context,
|
||||
builder: (context) => ChatUserModal(
|
||||
chatStore: widget.chatStore,
|
||||
username: userInfo.login,
|
||||
userId: userInfo.id,
|
||||
displayName: userInfo.displayName,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
childCount:
|
||||
widget.chatDetailsStore.filteredUsers.length,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
Observer(
|
||||
builder: (context) => AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
switchInCurve: Curves.easeOut,
|
||||
switchOutCurve: Curves.easeIn,
|
||||
child: widget.chatDetailsStore.showJumpButton
|
||||
? ScrollToTopButton(
|
||||
scrollController:
|
||||
widget.chatDetailsStore.scrollController)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -169,7 +169,6 @@ abstract class ChatStoreBase with Store {
|
|||
const Duration(milliseconds: 200), (timer) => addMessages());
|
||||
|
||||
assetsStore.init();
|
||||
chatDetailsStore.updateChatters();
|
||||
|
||||
_messageBuffer
|
||||
.add(IRCMessage.createNotice(message: 'Connecting to chat...'));
|
||||
|
|
@ -197,9 +196,6 @@ abstract class ChatStoreBase with Store {
|
|||
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();
|
||||
}
|
||||
|
||||
// Un-expand the chat when unfocusing.
|
||||
|
|
@ -234,6 +230,10 @@ abstract class ChatStoreBase with Store {
|
|||
final parsedIRCMessage =
|
||||
IRCMessage.fromString(message, userLogin: auth.user.details?.login);
|
||||
|
||||
if (parsedIRCMessage.user != null) {
|
||||
chatDetailsStore.chatUsers.add(parsedIRCMessage.user!);
|
||||
}
|
||||
|
||||
// Filter messages from any blocked users if not a moderator or not the channel owner.
|
||||
if (!_userState.mod &&
|
||||
channelName != auth.user.details?.login &&
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ class ChatBottomBar extends StatelessWidget {
|
|||
chatStore.textController.text.split(' ').last.toLowerCase()))
|
||||
.toList();
|
||||
|
||||
final matchingChatters = chatStore.chatDetailsStore.allChatters
|
||||
final matchingChatters = chatStore.chatDetailsStore.chatUsers
|
||||
.where((chatter) => chatter.contains(chatStore.textController.text
|
||||
.split(' ')
|
||||
.last
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue