diff --git a/lib/apis/twitch_api.dart b/lib/apis/twitch_api.dart index 74e504c4..4ddb3da0 100644 --- a/lib/apis/twitch_api.dart +++ b/lib/apis/twitch_api.dart @@ -291,6 +291,23 @@ class TwitchApi { } } + Future getStreamsByIds({ + required List userIds, + required Map headers, + }) async { + final uri = Uri.parse( + 'https://api.twitch.tv/helix/streams?${userIds.map((e) => 'user_id=$e').join('&')}&first=100', + ); + + final response = await _client.get(uri, headers: headers); + if (response.statusCode == 200) { + final decoded = jsonDecode(response.body); + return StreamsTwitch.fromJson(decoded); + } else { + return Future.error('Failed to get stream info'); + } + } + /// Returns a [UserTwitch] object containing the user info associated with the given [userLogin]. Future getUser({ String? userLogin, diff --git a/lib/screens/channel/chat/widgets/chat_user_modal.dart b/lib/screens/channel/chat/widgets/chat_user_modal.dart index 9824ced1..715c0a23 100644 --- a/lib/screens/channel/chat/widgets/chat_user_modal.dart +++ b/lib/screens/channel/chat/widgets/chat_user_modal.dart @@ -4,8 +4,8 @@ import 'package:frosty/screens/channel/chat/stores/chat_store.dart'; import 'package:frosty/screens/channel/chat/widgets/chat_message.dart'; import 'package:frosty/utils.dart'; import 'package:frosty/widgets/alert_message.dart'; -import 'package:frosty/widgets/block_report_modal.dart'; import 'package:frosty/widgets/profile_picture.dart'; +import 'package:frosty/widgets/user_actions_modal.dart'; class ChatUserModal extends StatefulWidget { final ChatStore chatStore; @@ -72,7 +72,7 @@ class _ChatUserModalState extends State { tooltip: 'More', onPressed: () => showModalBottomSheet( context: context, - builder: (context) => BlockReportModal( + builder: (context) => UserActionsModal( authStore: widget.chatStore.auth, name: name, userLogin: widget.username, diff --git a/lib/screens/home/search/search_results_channels.dart b/lib/screens/home/search/search_results_channels.dart index cd494fdf..1079690e 100644 --- a/lib/screens/home/search/search_results_channels.dart +++ b/lib/screens/home/search/search_results_channels.dart @@ -6,10 +6,10 @@ import 'package:frosty/screens/channel/channel.dart'; import 'package:frosty/screens/home/search/search_store.dart'; import 'package:frosty/utils.dart'; import 'package:frosty/widgets/alert_message.dart'; -import 'package:frosty/widgets/block_report_modal.dart'; import 'package:frosty/widgets/loading_indicator.dart'; import 'package:frosty/widgets/profile_picture.dart'; import 'package:frosty/widgets/uptime.dart'; +import 'package:frosty/widgets/user_actions_modal.dart'; import 'package:mobx/mobx.dart'; class SearchResultsChannels extends StatefulWidget { @@ -107,7 +107,7 @@ class _SearchResultsChannelsState extends State { showModalBottomSheet( context: context, - builder: (context) => BlockReportModal( + builder: (context) => UserActionsModal( authStore: widget.searchStore.authStore, name: displayName, userLogin: channel.broadcasterLogin, diff --git a/lib/screens/home/stream_list/large_stream_card.dart b/lib/screens/home/stream_list/large_stream_card.dart index fa37af3c..6440f882 100644 --- a/lib/screens/home/stream_list/large_stream_card.dart +++ b/lib/screens/home/stream_list/large_stream_card.dart @@ -8,10 +8,10 @@ import 'package:frosty/screens/channel/video/video_bar.dart'; import 'package:frosty/screens/settings/stores/auth_store.dart'; import 'package:frosty/theme.dart'; import 'package:frosty/utils.dart'; -import 'package:frosty/widgets/block_report_modal.dart'; import 'package:frosty/widgets/cached_image.dart'; import 'package:frosty/widgets/loading_indicator.dart'; import 'package:frosty/widgets/uptime.dart'; +import 'package:frosty/widgets/user_actions_modal.dart'; import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; @@ -19,12 +19,16 @@ class LargeStreamCard extends StatelessWidget { final StreamTwitch streamInfo; final bool showThumbnail; final bool showCategory; + final bool showPinOption; + final bool? isPinned; const LargeStreamCard({ super.key, required this.streamInfo, required this.showThumbnail, this.showCategory = true, + this.showPinOption = false, + this.isPinned, }); @override @@ -151,11 +155,13 @@ class LargeStreamCard extends StatelessWidget { showModalBottomSheet( context: context, - builder: (context) => BlockReportModal( + builder: (context) => UserActionsModal( authStore: context.read(), name: streamerName, userLogin: streamInfo.userLogin, userId: streamInfo.userId, + showPinOption: showPinOption, + isPinned: isPinned, ), ); }, diff --git a/lib/screens/home/stream_list/stream_card.dart b/lib/screens/home/stream_list/stream_card.dart index cc7ebb06..e281b9a6 100644 --- a/lib/screens/home/stream_list/stream_card.dart +++ b/lib/screens/home/stream_list/stream_card.dart @@ -8,12 +8,12 @@ import 'package:frosty/screens/home/top/categories/category_streams.dart'; import 'package:frosty/screens/settings/stores/auth_store.dart'; import 'package:frosty/theme.dart'; import 'package:frosty/utils.dart'; -import 'package:frosty/widgets/block_report_modal.dart'; import 'package:frosty/widgets/cached_image.dart'; import 'package:frosty/widgets/loading_indicator.dart'; import 'package:frosty/widgets/photo_view.dart'; import 'package:frosty/widgets/profile_picture.dart'; import 'package:frosty/widgets/uptime.dart'; +import 'package:frosty/widgets/user_actions_modal.dart'; import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; @@ -22,12 +22,16 @@ class StreamCard extends StatelessWidget { final StreamTwitch streamInfo; final bool showThumbnail; final bool showCategory; + final bool showPinOption; + final bool? isPinned; const StreamCard({ super.key, required this.streamInfo, required this.showThumbnail, this.showCategory = true, + this.showPinOption = false, + this.isPinned, }); @override @@ -210,11 +214,13 @@ class StreamCard extends StatelessWidget { showModalBottomSheet( context: context, - builder: (context) => BlockReportModal( + builder: (context) => UserActionsModal( authStore: context.read(), name: streamerName, userLogin: streamInfo.userLogin, userId: streamInfo.userId, + showPinOption: showPinOption, + isPinned: isPinned, ), ); }, diff --git a/lib/screens/home/stream_list/stream_list_store.dart b/lib/screens/home/stream_list/stream_list_store.dart index 2062babe..742a9c6e 100644 --- a/lib/screens/home/stream_list/stream_list_store.dart +++ b/lib/screens/home/stream_list/stream_list_store.dart @@ -5,6 +5,7 @@ import 'package:frosty/apis/twitch_api.dart'; import 'package:frosty/models/category.dart'; import 'package:frosty/models/stream.dart'; import 'package:frosty/screens/settings/stores/auth_store.dart'; +import 'package:frosty/screens/settings/stores/settings_store.dart'; import 'package:mobx/mobx.dart'; part 'stream_list_store.g.dart'; @@ -15,6 +16,8 @@ abstract class ListStoreBase with Store { /// The authentication store. final AuthStore authStore; + final SettingsStore settingsStore; + /// Twitch API service class for making requests. final TwitchApi twitchApi; @@ -36,19 +39,35 @@ abstract class ListStoreBase with Store { /// Returns whether or not there are more streams and loading status for pagination. @computed - bool get hasMore => _isLoading == false && _streamsCursor != null; + bool get hasMore => isLoading == false && _streamsCursor != null; + + @computed + bool get isLoading => + _isAllStreamsLoading || + _isPinnedStreamsLoading || + _isCategoryDetailsLoading; /// The loading status for pagination. - @readonly - bool _isLoading = false; /// The list of the fetched streams. @readonly var _allStreams = ObservableList(); + @readonly + bool _isAllStreamsLoading = false; + + @readonly + var _pinnedStreams = ObservableList(); + + @readonly + var _isPinnedStreamsLoading = false; + @readonly CategoryTwitch? _categoryDetails; + @readonly + var _isCategoryDetailsLoading = false; + /// Whether or not the scroll to top button is visible. @observable var showJumpButton = false; @@ -68,8 +87,11 @@ abstract class ListStoreBase with Store { @readonly String? _error; + ReactionDisposer? _pinnedStreamsReactioniDisposer; + ListStoreBase({ required this.authStore, + required this.settingsStore, required this.twitchApi, required this.listType, this.categoryId, @@ -86,17 +108,26 @@ abstract class ListStoreBase with Store { }); } - getStreams(); + if (listType == ListType.followed) { + _pinnedStreamsReactioniDisposer = reaction( + (_) => settingsStore.pinnedChannelIds, + (_) => getPinnedStreams(), + ); - if (listType == ListType.category && categoryId != null) { + getPinnedStreams(); + } + + if (listType == ListType.category) { _getCategoryDetails(); } + + getStreams(); } /// Fetches the streams based on the type and current cursor. @action Future getStreams() async { - _isLoading = true; + _isAllStreamsLoading = true; try { final StreamsTwitch newStreams; @@ -137,20 +168,50 @@ abstract class ListStoreBase with Store { _error = e.toString(); } - _isLoading = false; + _isAllStreamsLoading = false; + } + + @action + Future getPinnedStreams() async { + if (settingsStore.pinnedChannelIds.isEmpty) { + _pinnedStreams.clear(); + return; + } + + _isPinnedStreamsLoading = true; + + try { + _pinnedStreams = (await twitchApi.getStreamsByIds( + userIds: settingsStore.pinnedChannelIds, + headers: authStore.headersTwitch, + )) + .data + .asObservable(); + + _error = null; + } on SocketException { + _error = 'Failed to connect'; + } catch (e) { + _error = e.toString(); + } + + _isPinnedStreamsLoading = false; } /// Resets the cursor and then fetches the streams. @action - Future refreshStreams() { - _streamsCursor = null; + Future refreshStreams() async { + if (listType == ListType.followed) await getPinnedStreams(); - return getStreams(); + _streamsCursor = null; + await getStreams(); } @action Future _getCategoryDetails() async { - _isLoading = true; + if (categoryId == null) return; + + _isCategoryDetailsLoading = true; final categoryDetails = await twitchApi.getCategory( headers: authStore.headersTwitch, @@ -159,7 +220,7 @@ abstract class ListStoreBase with Store { _categoryDetails = categoryDetails.data.first; - _isLoading = false; + _isCategoryDetailsLoading = false; } /// Checks the last time the streams were refreshed and updates them if it has been more than 5 minutes. @@ -172,7 +233,11 @@ abstract class ListStoreBase with Store { lastTimeRefreshed = now; } - void dispose() => scrollController?.dispose(); + void dispose() { + _pinnedStreamsReactioniDisposer?.call(); + + scrollController?.dispose(); + } } /// The possible types of lists that can be displayed. diff --git a/lib/screens/home/stream_list/stream_list_store.g.dart b/lib/screens/home/stream_list/stream_list_store.g.dart index 3c1591dc..2d90749e 100644 --- a/lib/screens/home/stream_list/stream_list_store.g.dart +++ b/lib/screens/home/stream_list/stream_list_store.g.dart @@ -15,6 +15,13 @@ mixin _$ListStore on ListStoreBase, Store { bool get hasMore => (_$hasMoreComputed ??= Computed(() => super.hasMore, name: 'ListStoreBase.hasMore')) .value; + Computed? _$isLoadingComputed; + + @override + bool get isLoading => + (_$isLoadingComputed ??= Computed(() => super.isLoading, + name: 'ListStoreBase.isLoading')) + .value; Computed>? _$streamsComputed; @override @@ -23,24 +30,6 @@ mixin _$ListStore on ListStoreBase, Store { name: 'ListStoreBase.streams')) .value; - late final _$_isLoadingAtom = - Atom(name: 'ListStoreBase._isLoading', context: context); - - bool get isLoading { - _$_isLoadingAtom.reportRead(); - return super._isLoading; - } - - @override - bool get _isLoading => isLoading; - - @override - set _isLoading(bool value) { - _$_isLoadingAtom.reportWrite(value, super._isLoading, () { - super._isLoading = value; - }); - } - late final _$_allStreamsAtom = Atom(name: 'ListStoreBase._allStreams', context: context); @@ -59,6 +48,62 @@ mixin _$ListStore on ListStoreBase, Store { }); } + late final _$_isAllStreamsLoadingAtom = + Atom(name: 'ListStoreBase._isAllStreamsLoading', context: context); + + bool get isAllStreamsLoading { + _$_isAllStreamsLoadingAtom.reportRead(); + return super._isAllStreamsLoading; + } + + @override + bool get _isAllStreamsLoading => isAllStreamsLoading; + + @override + set _isAllStreamsLoading(bool value) { + _$_isAllStreamsLoadingAtom.reportWrite(value, super._isAllStreamsLoading, + () { + super._isAllStreamsLoading = value; + }); + } + + late final _$_pinnedStreamsAtom = + Atom(name: 'ListStoreBase._pinnedStreams', context: context); + + ObservableList get pinnedStreams { + _$_pinnedStreamsAtom.reportRead(); + return super._pinnedStreams; + } + + @override + ObservableList get _pinnedStreams => pinnedStreams; + + @override + set _pinnedStreams(ObservableList value) { + _$_pinnedStreamsAtom.reportWrite(value, super._pinnedStreams, () { + super._pinnedStreams = value; + }); + } + + late final _$_isPinnedStreamsLoadingAtom = + Atom(name: 'ListStoreBase._isPinnedStreamsLoading', context: context); + + bool get isPinnedStreamsLoading { + _$_isPinnedStreamsLoadingAtom.reportRead(); + return super._isPinnedStreamsLoading; + } + + @override + bool get _isPinnedStreamsLoading => isPinnedStreamsLoading; + + @override + set _isPinnedStreamsLoading(bool value) { + _$_isPinnedStreamsLoadingAtom + .reportWrite(value, super._isPinnedStreamsLoading, () { + super._isPinnedStreamsLoading = value; + }); + } + late final _$_categoryDetailsAtom = Atom(name: 'ListStoreBase._categoryDetails', context: context); @@ -77,6 +122,25 @@ mixin _$ListStore on ListStoreBase, Store { }); } + late final _$_isCategoryDetailsLoadingAtom = + Atom(name: 'ListStoreBase._isCategoryDetailsLoading', context: context); + + bool get isCategoryDetailsLoading { + _$_isCategoryDetailsLoadingAtom.reportRead(); + return super._isCategoryDetailsLoading; + } + + @override + bool get _isCategoryDetailsLoading => isCategoryDetailsLoading; + + @override + set _isCategoryDetailsLoading(bool value) { + _$_isCategoryDetailsLoadingAtom + .reportWrite(value, super._isCategoryDetailsLoading, () { + super._isCategoryDetailsLoading = value; + }); + } + late final _$showJumpButtonAtom = Atom(name: 'ListStoreBase.showJumpButton', context: context); @@ -119,6 +183,22 @@ mixin _$ListStore on ListStoreBase, Store { return _$getStreamsAsyncAction.run(() => super.getStreams()); } + late final _$getPinnedStreamsAsyncAction = + AsyncAction('ListStoreBase.getPinnedStreams', context: context); + + @override + Future getPinnedStreams() { + return _$getPinnedStreamsAsyncAction.run(() => super.getPinnedStreams()); + } + + late final _$refreshStreamsAsyncAction = + AsyncAction('ListStoreBase.refreshStreams', context: context); + + @override + Future refreshStreams() { + return _$refreshStreamsAsyncAction.run(() => super.refreshStreams()); + } + late final _$_getCategoryDetailsAsyncAction = AsyncAction('ListStoreBase._getCategoryDetails', context: context); @@ -128,25 +208,12 @@ mixin _$ListStore on ListStoreBase, Store { .run(() => super._getCategoryDetails()); } - late final _$ListStoreBaseActionController = - ActionController(name: 'ListStoreBase', context: context); - - @override - Future refreshStreams() { - final _$actionInfo = _$ListStoreBaseActionController.startAction( - name: 'ListStoreBase.refreshStreams'); - try { - return super.refreshStreams(); - } finally { - _$ListStoreBaseActionController.endAction(_$actionInfo); - } - } - @override String toString() { return ''' showJumpButton: ${showJumpButton}, hasMore: ${hasMore}, +isLoading: ${isLoading}, streams: ${streams} '''; } diff --git a/lib/screens/home/stream_list/streams_list.dart b/lib/screens/home/stream_list/streams_list.dart index 0bda7160..ebf734b4 100644 --- a/lib/screens/home/stream_list/streams_list.dart +++ b/lib/screens/home/stream_list/streams_list.dart @@ -12,6 +12,7 @@ import 'package:frosty/widgets/alert_message.dart'; import 'package:frosty/widgets/animated_scroll_border.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:provider/provider.dart'; /// A widget that displays a list of followed or top streams based on the provided [listType]. @@ -43,6 +44,7 @@ class _StreamsListState extends State with AutomaticKeepAliveClientMixin, WidgetsBindingObserver { late final _listStore = ListStore( authStore: context.read(), + settingsStore: context.read(), twitchApi: context.read(), listType: widget.listType, categoryId: widget.categoryId, @@ -126,6 +128,17 @@ class _StreamsListState extends State ); } + final settingsStore = context.watch(); + + final unpinnedStreams = _listStore.streams + .where( + (stream) => + !settingsStore.pinnedChannelIds.contains(stream.userId), + ) + .toList(); + + final isFollowingTab = widget.listType == ListType.followed; + return Stack( alignment: AlignmentDirectional.bottomCenter, children: [ @@ -144,42 +157,113 @@ class _StreamsListState extends State Expanded( child: Scrollbar( controller: _listStore.scrollController, - child: ListView.builder( + child: CustomScrollView( physics: const AlwaysScrollableScrollPhysics(), controller: _listStore.scrollController, - itemCount: _listStore.streams.length, - itemBuilder: (context, index) { - if (index > _listStore.streams.length - 10 && - _listStore.hasMore) { - debugPrint('$index ${_listStore.streams.length}'); - - _listStore.getStreams(); - } - return Observer( - builder: (context) => - context.read().largeStreamCard + slivers: [ + if (isFollowingTab && + _listStore.pinnedStreams.isNotEmpty) ...[ + const SliverToBoxAdapter( + child: SectionHeader( + 'Pinnned', + isFirst: true, + padding: EdgeInsets.fromLTRB( + 16, + 8, + 16, + 8, + ), + ), + ), + SliverList.builder( + itemCount: _listStore.pinnedStreams.length, + itemBuilder: (context, index) { + return Observer( + builder: (context) { + return settingsStore.largeStreamCard + ? LargeStreamCard( + key: ValueKey( + _listStore + .pinnedStreams[index].userId, + ), + streamInfo: + _listStore.pinnedStreams[index], + showThumbnail: context + .read() + .showThumbnails, + showCategory: + widget.categoryId == null, + showPinOption: true, + isPinned: true, + ) + : StreamCard( + key: ValueKey( + _listStore + .pinnedStreams[index].userId, + ), + streamInfo: + _listStore.pinnedStreams[index], + showThumbnail: context + .read() + .showThumbnails, + showCategory: + widget.categoryId == null, + showPinOption: true, + isPinned: true, + ); + }, + ); + }, + ), + const SliverToBoxAdapter( + child: SectionHeader( + 'All', + isFirst: true, + padding: EdgeInsets.fromLTRB( + 16, + 8, + 16, + 8, + ), + ), + ), + ], + SliverList.builder( + itemCount: unpinnedStreams.length, + itemBuilder: (context, index) { + if (index > unpinnedStreams.length - 10 && + _listStore.hasMore) { + _listStore.getStreams(); + } + return Observer( + builder: (context) => settingsStore + .largeStreamCard ? LargeStreamCard( key: ValueKey( - _listStore.streams[index].userId, + unpinnedStreams[index].userId, ), - streamInfo: _listStore.streams[index], - showThumbnail: context - .read() - .showThumbnails, + streamInfo: unpinnedStreams[index], + showThumbnail: + settingsStore.showThumbnails, showCategory: widget.categoryId == null, + showPinOption: isFollowingTab, + isPinned: false, ) : StreamCard( key: ValueKey( - _listStore.streams[index].userId, + unpinnedStreams[index].userId, ), - streamInfo: _listStore.streams[index], - showThumbnail: context - .read() - .showThumbnails, + streamInfo: unpinnedStreams[index], + showThumbnail: + settingsStore.showThumbnails, showCategory: widget.categoryId == null, + showPinOption: isFollowingTab, + isPinned: false, ), - ); - }, + ); + }, + ), + ], ), ), ), diff --git a/lib/screens/settings/stores/settings_store.dart b/lib/screens/settings/stores/settings_store.dart index 8f5f6a66..73e1c232 100644 --- a/lib/screens/settings/stores/settings_store.dart +++ b/lib/screens/settings/stores/settings_store.dart @@ -360,6 +360,7 @@ abstract class _SettingsStoreBase with Store { // * Global configs static const defaultFullScreen = false; static const defaultFullScreenChatOverlay = false; + static const defaultPinnedChannelIds = []; @JsonKey(defaultValue: defaultFullScreen) @observable @@ -369,10 +370,15 @@ abstract class _SettingsStoreBase with Store { @observable var fullScreenChatOverlay = defaultFullScreenChatOverlay; + @JsonKey(defaultValue: defaultPinnedChannelIds) + @observable + var pinnedChannelIds = defaultPinnedChannelIds; + @action void resetGlobalConfigs() { fullScreen = defaultFullScreen; fullScreenChatOverlay = defaultFullScreenChatOverlay; + pinnedChannelIds = defaultPinnedChannelIds; } @action diff --git a/lib/screens/settings/stores/settings_store.g.dart b/lib/screens/settings/stores/settings_store.g.dart index 4b8b23c0..10b6793f 100644 --- a/lib/screens/settings/stores/settings_store.g.dart +++ b/lib/screens/settings/stores/settings_store.g.dart @@ -67,7 +67,11 @@ SettingsStore _$SettingsStoreFromJson(Map json) => ..shareCrashLogsAndAnalytics = json['shareCrashLogsAndAnalytics'] as bool? ?? true ..fullScreen = json['fullScreen'] as bool? ?? false - ..fullScreenChatOverlay = json['fullScreenChatOverlay'] as bool? ?? false; + ..fullScreenChatOverlay = json['fullScreenChatOverlay'] as bool? ?? false + ..pinnedChannelIds = (json['pinnedChannelIds'] as List?) + ?.map((e) => e as String) + .toList() ?? + []; Map _$SettingsStoreToJson(SettingsStore instance) => { @@ -118,6 +122,7 @@ Map _$SettingsStoreToJson(SettingsStore instance) => 'shareCrashLogsAndAnalytics': instance.shareCrashLogsAndAnalytics, 'fullScreen': instance.fullScreen, 'fullScreenChatOverlay': instance.fullScreenChatOverlay, + 'pinnedChannelIds': instance.pinnedChannelIds, }; const _$ThemeTypeEnumMap = { @@ -895,6 +900,22 @@ mixin _$SettingsStore on _SettingsStoreBase, Store { }); } + late final _$pinnedChannelIdsAtom = + Atom(name: '_SettingsStoreBase.pinnedChannelIds', context: context); + + @override + List get pinnedChannelIds { + _$pinnedChannelIdsAtom.reportRead(); + return super.pinnedChannelIds; + } + + @override + set pinnedChannelIds(List value) { + _$pinnedChannelIdsAtom.reportWrite(value, super.pinnedChannelIds, () { + super.pinnedChannelIds = value; + }); + } + late final _$_SettingsStoreBaseActionController = ActionController(name: '_SettingsStoreBase', context: context); @@ -1012,7 +1033,8 @@ showRecentMessages: ${showRecentMessages}, darkenRecentMessages: ${darkenRecentMessages}, shareCrashLogsAndAnalytics: ${shareCrashLogsAndAnalytics}, fullScreen: ${fullScreen}, -fullScreenChatOverlay: ${fullScreenChatOverlay} +fullScreenChatOverlay: ${fullScreenChatOverlay}, +pinnedChannelIds: ${pinnedChannelIds} '''; } } diff --git a/lib/widgets/block_report_modal.dart b/lib/widgets/user_actions_modal.dart similarity index 64% rename from lib/widgets/block_report_modal.dart rename to lib/widgets/user_actions_modal.dart index b97a0aac..90a7ab5b 100644 --- a/lib/widgets/block_report_modal.dart +++ b/lib/widgets/user_actions_modal.dart @@ -1,20 +1,26 @@ import 'package:flutter/material.dart'; import 'package:frosty/screens/settings/stores/auth_store.dart'; +import 'package:frosty/screens/settings/stores/settings_store.dart'; import 'package:frosty/widgets/app_bar.dart'; +import 'package:provider/provider.dart'; import 'package:webview_flutter/webview_flutter.dart'; -class BlockReportModal extends StatelessWidget { +class UserActionsModal extends StatelessWidget { final AuthStore authStore; final String name; final String userLogin; final String userId; + final bool showPinOption; + final bool? isPinned; - const BlockReportModal({ + const UserActionsModal({ super.key, required this.authStore, required this.name, required this.userLogin, required this.userId, + this.showPinOption = false, + this.isPinned, }); @override @@ -23,6 +29,26 @@ class BlockReportModal extends StatelessWidget { primary: false, shrinkWrap: true, children: [ + if (showPinOption) + ListTile( + leading: const Icon(Icons.push_pin_outlined), + title: Text('${isPinned == true ? 'Unpin' : 'Pin'} $name'), + onTap: () { + if (isPinned == true) { + context.read().pinnedChannelIds = [ + ...context.read().pinnedChannelIds + ..remove(userId), + ]; + } else { + context.read().pinnedChannelIds = [ + ...context.read().pinnedChannelIds, + userId, + ]; + } + + Navigator.pop(context); + }, + ), if (authStore.isLoggedIn) ListTile( leading: const Icon(Icons.block_rounded),