748 lines
		
	
	
		
			23 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
			
		
		
	
	
			748 lines
		
	
	
		
			23 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
import 'package:easy_localization/easy_localization.dart';
 | 
						|
import 'package:flutter/foundation.dart';
 | 
						|
import 'package:flutter/material.dart';
 | 
						|
import 'package:go_router/go_router.dart';
 | 
						|
import 'package:flutter_hooks/flutter_hooks.dart';
 | 
						|
import 'package:gap/gap.dart';
 | 
						|
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
						|
import 'package:island/models/activity.dart';
 | 
						|
import 'package:island/models/publisher.dart';
 | 
						|
import 'package:island/models/realm.dart';
 | 
						|
import 'package:island/models/webfeed.dart';
 | 
						|
import 'package:island/pods/event_calendar.dart';
 | 
						|
import 'package:island/pods/userinfo.dart';
 | 
						|
import 'package:island/screens/notification.dart';
 | 
						|
import 'package:island/services/responsive.dart';
 | 
						|
import 'package:island/widgets/app_scaffold.dart';
 | 
						|
import 'package:island/models/post.dart';
 | 
						|
import 'package:island/widgets/check_in.dart';
 | 
						|
import 'package:island/widgets/post/post_featured.dart';
 | 
						|
import 'package:island/widgets/post/post_item.dart';
 | 
						|
import 'package:island/widgets/post/compose_card.dart';
 | 
						|
import 'package:material_symbols_icons/symbols.dart';
 | 
						|
import 'package:riverpod_annotation/riverpod_annotation.dart';
 | 
						|
import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
 | 
						|
import 'package:island/pods/network.dart';
 | 
						|
import 'package:island/widgets/realm/realm_card.dart';
 | 
						|
import 'package:island/widgets/publisher/publisher_card.dart';
 | 
						|
import 'package:island/widgets/web_article_card.dart';
 | 
						|
import 'package:island/widgets/extended_refresh_indicator.dart';
 | 
						|
import 'package:styled_widget/styled_widget.dart';
 | 
						|
 | 
						|
part 'explore.g.dart';
 | 
						|
 | 
						|
Widget notificationIndicatorWidget(
 | 
						|
  BuildContext context, {
 | 
						|
  required int count,
 | 
						|
  EdgeInsets? margin,
 | 
						|
}) => Card(
 | 
						|
  margin: margin,
 | 
						|
  child: ListTile(
 | 
						|
    shape: const RoundedRectangleBorder(
 | 
						|
      borderRadius: BorderRadius.all(Radius.circular(8)),
 | 
						|
    ),
 | 
						|
    minTileHeight: 48,
 | 
						|
    leading: const Icon(Symbols.notifications),
 | 
						|
    title: Row(
 | 
						|
      children: [
 | 
						|
        Text('notifications').tr().fontSize(14),
 | 
						|
        const Gap(8),
 | 
						|
        Badge(label: Text(count.toString())),
 | 
						|
      ],
 | 
						|
    ),
 | 
						|
    trailing: const Icon(Symbols.chevron_right),
 | 
						|
    contentPadding: EdgeInsets.only(left: 16, right: 15),
 | 
						|
    onTap: () {
 | 
						|
      showModalBottomSheet(
 | 
						|
        context: context,
 | 
						|
        isScrollControlled: true,
 | 
						|
        useRootNavigator: true,
 | 
						|
        builder: (context) => const NotificationSheet(),
 | 
						|
      );
 | 
						|
    },
 | 
						|
  ),
 | 
						|
);
 | 
						|
 | 
						|
class ExploreScreen extends HookConsumerWidget {
 | 
						|
  const ExploreScreen({super.key});
 | 
						|
 | 
						|
  @override
 | 
						|
  Widget build(BuildContext context, WidgetRef ref) {
 | 
						|
    final tabController = useTabController(initialLength: 3);
 | 
						|
    final currentFilter = useState<String?>(null);
 | 
						|
 | 
						|
    useEffect(() {
 | 
						|
      void listener() {
 | 
						|
        switch (tabController.index) {
 | 
						|
          case 0:
 | 
						|
            currentFilter.value = null;
 | 
						|
            break;
 | 
						|
          case 1:
 | 
						|
            currentFilter.value = 'subscriptions';
 | 
						|
            break;
 | 
						|
          case 2:
 | 
						|
            currentFilter.value = 'friends';
 | 
						|
            break;
 | 
						|
        }
 | 
						|
      }
 | 
						|
 | 
						|
      tabController.addListener(listener);
 | 
						|
      return () => tabController.removeListener(listener);
 | 
						|
    }, [tabController]);
 | 
						|
 | 
						|
    final now = DateTime.now();
 | 
						|
 | 
						|
    final query = useState(
 | 
						|
      EventCalendarQuery(uname: 'me', year: now.year, month: now.month),
 | 
						|
    );
 | 
						|
 | 
						|
    final events = ref.watch(eventCalendarProvider(query.value));
 | 
						|
 | 
						|
    final selectedDay = useState(now);
 | 
						|
 | 
						|
    final user = ref.watch(userInfoProvider);
 | 
						|
 | 
						|
    final notificationCount = ref.watch(
 | 
						|
      notificationUnreadCountNotifierProvider,
 | 
						|
    );
 | 
						|
 | 
						|
    final isWide = isWideScreen(context);
 | 
						|
 | 
						|
    final filterBar = Card(
 | 
						|
      margin: EdgeInsets.only(top: MediaQuery.of(context).padding.top),
 | 
						|
      child: Row(
 | 
						|
        children: [
 | 
						|
          Expanded(
 | 
						|
            child: TabBar(
 | 
						|
              controller: tabController,
 | 
						|
              tabAlignment: TabAlignment.start,
 | 
						|
              isScrollable: true,
 | 
						|
              dividerColor: Colors.transparent,
 | 
						|
              tabs: [
 | 
						|
                Tab(
 | 
						|
                  icon: Tooltip(
 | 
						|
                    message: 'explore'.tr(),
 | 
						|
                    child: Icon(Symbols.explore),
 | 
						|
                  ),
 | 
						|
                ),
 | 
						|
                Tab(
 | 
						|
                  icon: Tooltip(
 | 
						|
                    message: 'exploreFilterSubscriptions'.tr(),
 | 
						|
                    child: Icon(Symbols.subscriptions),
 | 
						|
                  ),
 | 
						|
                ),
 | 
						|
                Tab(
 | 
						|
                  icon: Tooltip(
 | 
						|
                    message: 'exploreFilterFriends'.tr(),
 | 
						|
                    child: Icon(Symbols.people),
 | 
						|
                  ),
 | 
						|
                ),
 | 
						|
              ],
 | 
						|
            ),
 | 
						|
          ),
 | 
						|
          IconButton(
 | 
						|
            onPressed: () {
 | 
						|
              context.pushNamed('articles');
 | 
						|
            },
 | 
						|
            icon: Icon(Symbols.auto_stories),
 | 
						|
            tooltip: 'webArticlesStand'.tr(),
 | 
						|
          ),
 | 
						|
          PopupMenuButton(
 | 
						|
            itemBuilder:
 | 
						|
                (context) => [
 | 
						|
                  PopupMenuItem(
 | 
						|
                    child: Row(
 | 
						|
                      children: [
 | 
						|
                        const Icon(Symbols.category),
 | 
						|
                        const Gap(12),
 | 
						|
                        Text('categories').tr(),
 | 
						|
                      ],
 | 
						|
                    ),
 | 
						|
                    onTap: () {
 | 
						|
                      context.pushNamed('postCategories');
 | 
						|
                    },
 | 
						|
                  ),
 | 
						|
                  PopupMenuItem(
 | 
						|
                    child: Row(
 | 
						|
                      children: [
 | 
						|
                        const Icon(Symbols.label),
 | 
						|
                        const Gap(12),
 | 
						|
                        Text('tags').tr(),
 | 
						|
                      ],
 | 
						|
                    ),
 | 
						|
                    onTap: () {
 | 
						|
                      context.pushNamed('postTags');
 | 
						|
                    },
 | 
						|
                  ),
 | 
						|
                  PopupMenuItem(
 | 
						|
                    child: Row(
 | 
						|
                      children: [
 | 
						|
                        const Icon(Symbols.shuffle),
 | 
						|
                        const Gap(12),
 | 
						|
                        Text('postShuffle').tr(),
 | 
						|
                      ],
 | 
						|
                    ),
 | 
						|
                    onTap: () {
 | 
						|
                      context.pushNamed('postShuffle');
 | 
						|
                    },
 | 
						|
                  ),
 | 
						|
                  PopupMenuItem(
 | 
						|
                    child: Row(
 | 
						|
                      children: [
 | 
						|
                        const Icon(Symbols.search),
 | 
						|
                        const Gap(12),
 | 
						|
                        Text('search').tr(),
 | 
						|
                      ],
 | 
						|
                    ),
 | 
						|
                    onTap: () {
 | 
						|
                      context.pushNamed('postSearch');
 | 
						|
                    },
 | 
						|
                  ),
 | 
						|
                ],
 | 
						|
            icon: Icon(Symbols.action_key),
 | 
						|
            tooltip: 'search'.tr(),
 | 
						|
          ),
 | 
						|
        ],
 | 
						|
      ).padding(horizontal: 8),
 | 
						|
    );
 | 
						|
 | 
						|
    final appBar = isWide ? null : _buildAppBar(tabController, context);
 | 
						|
 | 
						|
    return AppScaffold(
 | 
						|
      isNoBackground: false,
 | 
						|
      appBar: appBar,
 | 
						|
      body:
 | 
						|
          isWide
 | 
						|
              ? _buildWideBody(
 | 
						|
                context,
 | 
						|
                ref,
 | 
						|
                filterBar,
 | 
						|
                user,
 | 
						|
                notificationCount,
 | 
						|
                query,
 | 
						|
                events,
 | 
						|
                selectedDay,
 | 
						|
                currentFilter.value,
 | 
						|
              )
 | 
						|
              : _buildNarrowBody(context, ref, currentFilter.value),
 | 
						|
    );
 | 
						|
  }
 | 
						|
 | 
						|
  Widget _buildActivityList(
 | 
						|
    BuildContext context,
 | 
						|
    WidgetRef ref,
 | 
						|
    String? filter,
 | 
						|
  ) {
 | 
						|
    final activitiesNotifier = ref.watch(
 | 
						|
      activityListNotifierProvider(filter).notifier,
 | 
						|
    );
 | 
						|
 | 
						|
    final isWide = isWideScreen(context);
 | 
						|
 | 
						|
    return PagingHelperSliverView(
 | 
						|
      provider: activityListNotifierProvider(filter),
 | 
						|
      futureRefreshable: activityListNotifierProvider(filter).future,
 | 
						|
      notifierRefreshable: activityListNotifierProvider(filter).notifier,
 | 
						|
      contentBuilder:
 | 
						|
          (data, widgetCount, endItemView) => _ActivityListView(
 | 
						|
            data: data,
 | 
						|
            widgetCount: widgetCount,
 | 
						|
            endItemView: endItemView,
 | 
						|
            activitiesNotifier: activitiesNotifier,
 | 
						|
            isWide: isWide,
 | 
						|
          ),
 | 
						|
    );
 | 
						|
  }
 | 
						|
 | 
						|
  Widget _buildWideBody(
 | 
						|
    BuildContext context,
 | 
						|
    WidgetRef ref,
 | 
						|
    Widget filterBar,
 | 
						|
    AsyncValue<dynamic> user,
 | 
						|
    AsyncValue<int?> notificationCount,
 | 
						|
    ValueNotifier<EventCalendarQuery> query,
 | 
						|
    AsyncValue<List<dynamic>> events,
 | 
						|
    ValueNotifier<DateTime> selectedDay,
 | 
						|
    String? currentFilter,
 | 
						|
  ) {
 | 
						|
    final bodyView = _buildActivityList(context, ref, currentFilter);
 | 
						|
 | 
						|
    final activitiesNotifier = ref.watch(
 | 
						|
      activityListNotifierProvider(currentFilter).notifier,
 | 
						|
    );
 | 
						|
 | 
						|
    return Row(
 | 
						|
      spacing: 12,
 | 
						|
      children: [
 | 
						|
        Flexible(
 | 
						|
          flex: 3,
 | 
						|
          child: ExtendedRefreshIndicator(
 | 
						|
            onRefresh: () => Future.sync(activitiesNotifier.forceRefresh),
 | 
						|
            child: CustomScrollView(
 | 
						|
              slivers: [
 | 
						|
                const SliverGap(12),
 | 
						|
                SliverToBoxAdapter(child: filterBar),
 | 
						|
                const SliverGap(8),
 | 
						|
                bodyView,
 | 
						|
              ],
 | 
						|
            ),
 | 
						|
          ),
 | 
						|
        ),
 | 
						|
        if (user.value != null)
 | 
						|
          Flexible(
 | 
						|
            flex: 2,
 | 
						|
            child: Align(
 | 
						|
              alignment: Alignment.topCenter,
 | 
						|
              child: SingleChildScrollView(
 | 
						|
                child: Column(
 | 
						|
                  spacing: 8,
 | 
						|
                  children: [
 | 
						|
                    CheckInWidget(
 | 
						|
                      margin: EdgeInsets.only(top: 12),
 | 
						|
                      onChecked: () {
 | 
						|
                        ref.invalidate(eventCalendarProvider(query.value));
 | 
						|
                      },
 | 
						|
                    ),
 | 
						|
                    if (notificationCount.value != null &&
 | 
						|
                        notificationCount.value! > 0)
 | 
						|
                      notificationIndicatorWidget(
 | 
						|
                        context,
 | 
						|
                        count: notificationCount.value ?? 0,
 | 
						|
                        margin: EdgeInsets.zero,
 | 
						|
                      ),
 | 
						|
                    PostFeaturedList(),
 | 
						|
                    const PostComposeCard(),
 | 
						|
                  ],
 | 
						|
                ),
 | 
						|
              ),
 | 
						|
            ),
 | 
						|
          )
 | 
						|
        else
 | 
						|
          Flexible(
 | 
						|
            flex: 2,
 | 
						|
            child: Column(
 | 
						|
              mainAxisAlignment: MainAxisAlignment.center,
 | 
						|
              crossAxisAlignment: CrossAxisAlignment.start,
 | 
						|
              children: [
 | 
						|
                Text(
 | 
						|
                  'Welcome to\nthe Solar Network',
 | 
						|
                  style: Theme.of(context).textTheme.titleLarge,
 | 
						|
                ).bold(),
 | 
						|
                const Gap(2),
 | 
						|
                Text(
 | 
						|
                  'Login to explore more!',
 | 
						|
                  style: Theme.of(context).textTheme.bodyLarge,
 | 
						|
                ),
 | 
						|
              ],
 | 
						|
            ).padding(horizontal: 36, vertical: 16),
 | 
						|
          ),
 | 
						|
      ],
 | 
						|
    ).padding(horizontal: 12);
 | 
						|
  }
 | 
						|
 | 
						|
  PreferredSizeWidget _buildAppBar(
 | 
						|
    TabController tabController,
 | 
						|
    BuildContext context,
 | 
						|
  ) {
 | 
						|
    final foregroundColor = Theme.of(context).appBarTheme.foregroundColor;
 | 
						|
 | 
						|
    return AppBar(
 | 
						|
      toolbarHeight: 48 + 4,
 | 
						|
      flexibleSpace: Container(
 | 
						|
        height: 48,
 | 
						|
        margin: EdgeInsets.only(
 | 
						|
          left: 8,
 | 
						|
          right: 8,
 | 
						|
          top: 4 + MediaQuery.of(context).padding.top,
 | 
						|
        ),
 | 
						|
        child: Row(
 | 
						|
          children: [
 | 
						|
            Expanded(
 | 
						|
              child: TabBar(
 | 
						|
                controller: tabController,
 | 
						|
                tabAlignment: TabAlignment.start,
 | 
						|
                isScrollable: true,
 | 
						|
                dividerColor: Colors.transparent,
 | 
						|
                tabs: [
 | 
						|
                  Tab(
 | 
						|
                    icon: Tooltip(
 | 
						|
                      message: 'explore'.tr(),
 | 
						|
                      child: Icon(Symbols.explore, color: foregroundColor),
 | 
						|
                    ),
 | 
						|
                  ),
 | 
						|
                  Tab(
 | 
						|
                    icon: Tooltip(
 | 
						|
                      message: 'exploreFilterSubscriptions'.tr(),
 | 
						|
                      child: Icon(
 | 
						|
                        Symbols.subscriptions,
 | 
						|
                        color: foregroundColor,
 | 
						|
                      ),
 | 
						|
                    ),
 | 
						|
                  ),
 | 
						|
                  Tab(
 | 
						|
                    icon: Tooltip(
 | 
						|
                      message: 'exploreFilterFriends'.tr(),
 | 
						|
                      child: Icon(Symbols.people, color: foregroundColor),
 | 
						|
                    ),
 | 
						|
                  ),
 | 
						|
                ],
 | 
						|
              ),
 | 
						|
            ),
 | 
						|
            IconButton(
 | 
						|
              onPressed: () {
 | 
						|
                context.pushNamed('articles');
 | 
						|
              },
 | 
						|
              icon: Icon(Symbols.auto_stories, color: foregroundColor),
 | 
						|
              tooltip: 'webArticlesStand'.tr(),
 | 
						|
            ),
 | 
						|
            PopupMenuButton(
 | 
						|
              itemBuilder:
 | 
						|
                  (context) => [
 | 
						|
                    PopupMenuItem(
 | 
						|
                      child: Row(
 | 
						|
                        children: [
 | 
						|
                          const Icon(Symbols.category),
 | 
						|
                          const Gap(12),
 | 
						|
                          Text('categories').tr(),
 | 
						|
                        ],
 | 
						|
                      ),
 | 
						|
                      onTap: () {
 | 
						|
                        context.pushNamed('postCategories');
 | 
						|
                      },
 | 
						|
                    ),
 | 
						|
                    PopupMenuItem(
 | 
						|
                      child: Row(
 | 
						|
                        children: [
 | 
						|
                          const Icon(Symbols.label),
 | 
						|
                          const Gap(12),
 | 
						|
                          Text('tags').tr(),
 | 
						|
                        ],
 | 
						|
                      ),
 | 
						|
                      onTap: () {
 | 
						|
                        context.pushNamed('postTags');
 | 
						|
                      },
 | 
						|
                    ),
 | 
						|
                    PopupMenuItem(
 | 
						|
                      child: Row(
 | 
						|
                        children: [
 | 
						|
                          const Icon(Symbols.shuffle),
 | 
						|
                          const Gap(12),
 | 
						|
                          Text('postShuffle').tr(),
 | 
						|
                        ],
 | 
						|
                      ),
 | 
						|
                      onTap: () {
 | 
						|
                        context.pushNamed('postShuffle');
 | 
						|
                      },
 | 
						|
                    ),
 | 
						|
                    PopupMenuItem(
 | 
						|
                      child: Row(
 | 
						|
                        children: [
 | 
						|
                          const Icon(Symbols.search),
 | 
						|
                          const Gap(12),
 | 
						|
                          Text('search').tr(),
 | 
						|
                        ],
 | 
						|
                      ),
 | 
						|
                      onTap: () {
 | 
						|
                        context.pushNamed('postSearch');
 | 
						|
                      },
 | 
						|
                    ),
 | 
						|
                  ],
 | 
						|
              icon: Icon(Symbols.action_key, color: foregroundColor),
 | 
						|
              tooltip: 'search'.tr(),
 | 
						|
            ),
 | 
						|
          ],
 | 
						|
        ),
 | 
						|
      ),
 | 
						|
    );
 | 
						|
  }
 | 
						|
 | 
						|
  Widget _buildNarrowBody(
 | 
						|
    BuildContext context,
 | 
						|
    WidgetRef ref,
 | 
						|
    String? currentFilter,
 | 
						|
  ) {
 | 
						|
    final user = ref.watch(userInfoProvider);
 | 
						|
    final notificationCount = ref.watch(
 | 
						|
      notificationUnreadCountNotifierProvider,
 | 
						|
    );
 | 
						|
 | 
						|
    final activitiesNotifier = ref.watch(
 | 
						|
      activityListNotifierProvider(currentFilter).notifier,
 | 
						|
    );
 | 
						|
 | 
						|
    final bodyView = _buildActivityList(context, ref, currentFilter);
 | 
						|
 | 
						|
    return Expanded(
 | 
						|
      child: ExtendedRefreshIndicator(
 | 
						|
        onRefresh: () => Future.sync(activitiesNotifier.forceRefresh),
 | 
						|
        child: ClipRRect(
 | 
						|
          borderRadius: const BorderRadius.all(Radius.circular(8)),
 | 
						|
          child: CustomScrollView(
 | 
						|
            slivers: [
 | 
						|
              if (user.value != null)
 | 
						|
                SliverToBoxAdapter(
 | 
						|
                  child: CheckInWidget(
 | 
						|
                    margin: const EdgeInsets.only(bottom: 8, top: 8),
 | 
						|
                  ),
 | 
						|
                ),
 | 
						|
              SliverToBoxAdapter(
 | 
						|
                child: Padding(
 | 
						|
                  padding: const EdgeInsets.only(bottom: 8),
 | 
						|
                  child: PostFeaturedList(),
 | 
						|
                ),
 | 
						|
              ),
 | 
						|
              if (notificationCount.value != null &&
 | 
						|
                  notificationCount.value! > 0)
 | 
						|
                SliverToBoxAdapter(
 | 
						|
                  child: notificationIndicatorWidget(
 | 
						|
                    context,
 | 
						|
                    count: notificationCount.value ?? 0,
 | 
						|
                    margin: const EdgeInsets.only(bottom: 8),
 | 
						|
                  ),
 | 
						|
                ),
 | 
						|
              bodyView,
 | 
						|
            ],
 | 
						|
          ),
 | 
						|
        ).padding(horizontal: 8),
 | 
						|
      ),
 | 
						|
    );
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
class _DiscoveryActivityItem extends StatelessWidget {
 | 
						|
  final Map<String, dynamic> data;
 | 
						|
 | 
						|
  const _DiscoveryActivityItem({required this.data});
 | 
						|
 | 
						|
  @override
 | 
						|
  Widget build(BuildContext context) {
 | 
						|
    final items = data['items'] as List;
 | 
						|
    final type = items.firstOrNull?['type'] ?? 'unknown';
 | 
						|
 | 
						|
    var flexWeights = isWideScreen(context) ? <int>[3, 2, 1] : <int>[4, 1];
 | 
						|
    if (type == 'post') flexWeights = <int>[3, 2];
 | 
						|
 | 
						|
    final height = type == 'post' ? 280.0 : 180.0;
 | 
						|
 | 
						|
    final contentWidget = switch (type) {
 | 
						|
      'post' => ListView.separated(
 | 
						|
        scrollDirection: Axis.horizontal,
 | 
						|
        itemCount: items.length,
 | 
						|
        separatorBuilder: (context, index) => const Gap(12),
 | 
						|
        padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
 | 
						|
        itemBuilder: (context, index) {
 | 
						|
          final item = items[index];
 | 
						|
          return Container(
 | 
						|
            width: 320,
 | 
						|
            decoration: BoxDecoration(
 | 
						|
              border: Border.all(
 | 
						|
                width: 1 / MediaQuery.of(context).devicePixelRatio,
 | 
						|
                color: Theme.of(context).dividerColor.withOpacity(0.5),
 | 
						|
              ),
 | 
						|
              borderRadius: const BorderRadius.all(Radius.circular(8)),
 | 
						|
            ),
 | 
						|
            child: ClipRRect(
 | 
						|
              borderRadius: const BorderRadius.all(Radius.circular(8)),
 | 
						|
              child: SingleChildScrollView(
 | 
						|
                child: PostActionableItem(
 | 
						|
                  item: SnPost.fromJson(item['data']),
 | 
						|
                  isCompact: true,
 | 
						|
                ),
 | 
						|
              ),
 | 
						|
            ),
 | 
						|
          );
 | 
						|
        },
 | 
						|
      ),
 | 
						|
      _ => CarouselView.weighted(
 | 
						|
        flexWeights: flexWeights,
 | 
						|
        consumeMaxWeight: false,
 | 
						|
        enableSplash: false,
 | 
						|
        shape: const RoundedRectangleBorder(
 | 
						|
          borderRadius: BorderRadius.all(Radius.circular(8)),
 | 
						|
        ),
 | 
						|
        itemSnapping: false,
 | 
						|
        children: [
 | 
						|
          for (final item in items)
 | 
						|
            switch (type) {
 | 
						|
              'realm' => RealmCard(
 | 
						|
                realm: SnRealm.fromJson(item['data']),
 | 
						|
                maxWidth: 280,
 | 
						|
              ),
 | 
						|
              'publisher' => PublisherCard(
 | 
						|
                publisher: SnPublisher.fromJson(item['data']),
 | 
						|
                maxWidth: 280,
 | 
						|
              ),
 | 
						|
              'article' => WebArticleCard(
 | 
						|
                article: SnWebArticle.fromJson(item['data']),
 | 
						|
                maxWidth: 280,
 | 
						|
              ),
 | 
						|
              _ => const Placeholder(),
 | 
						|
            },
 | 
						|
        ],
 | 
						|
      ),
 | 
						|
    };
 | 
						|
 | 
						|
    return Card(
 | 
						|
      margin: EdgeInsets.zero,
 | 
						|
      child: Column(
 | 
						|
        crossAxisAlignment: CrossAxisAlignment.start,
 | 
						|
        children: [
 | 
						|
          Row(
 | 
						|
            crossAxisAlignment: CrossAxisAlignment.center,
 | 
						|
            children: [
 | 
						|
              Icon(switch (type) {
 | 
						|
                'realm' => Symbols.public,
 | 
						|
                'publisher' => Symbols.account_circle,
 | 
						|
                'article' => Symbols.auto_stories,
 | 
						|
                'post' => Symbols.shuffle,
 | 
						|
                _ => Symbols.explore,
 | 
						|
              }, size: 19),
 | 
						|
              const Gap(8),
 | 
						|
              Text(
 | 
						|
                (switch (type) {
 | 
						|
                  'realm' => 'discoverRealms',
 | 
						|
                  'publisher' => 'discoverPublishers',
 | 
						|
                  'article' => 'discoverWebArticles',
 | 
						|
                  'post' => 'discoverShuffledPost',
 | 
						|
                  _ => 'unknown',
 | 
						|
                }).tr(),
 | 
						|
                style: Theme.of(context).textTheme.titleMedium,
 | 
						|
              ).padding(top: 1),
 | 
						|
            ],
 | 
						|
          ).padding(horizontal: 20, top: 8, bottom: 4),
 | 
						|
          SizedBox(
 | 
						|
            height: height,
 | 
						|
            child: contentWidget,
 | 
						|
          ).padding(bottom: 8, horizontal: 8),
 | 
						|
        ],
 | 
						|
      ),
 | 
						|
    );
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
class _ActivityListView extends HookConsumerWidget {
 | 
						|
  final CursorPagingData<SnActivity> data;
 | 
						|
  final int widgetCount;
 | 
						|
  final Widget endItemView;
 | 
						|
  final ActivityListNotifier activitiesNotifier;
 | 
						|
  final bool isWide;
 | 
						|
 | 
						|
  const _ActivityListView({
 | 
						|
    required this.data,
 | 
						|
    required this.widgetCount,
 | 
						|
    required this.endItemView,
 | 
						|
    required this.activitiesNotifier,
 | 
						|
    required this.isWide,
 | 
						|
  });
 | 
						|
 | 
						|
  @override
 | 
						|
  Widget build(BuildContext context, WidgetRef ref) {
 | 
						|
    return SliverList.separated(
 | 
						|
      itemCount: widgetCount,
 | 
						|
      separatorBuilder: (_, _) => const Gap(8),
 | 
						|
      itemBuilder: (context, index) {
 | 
						|
        if (index == widgetCount - 1) {
 | 
						|
          return endItemView;
 | 
						|
        }
 | 
						|
 | 
						|
        final item = data.items[index];
 | 
						|
        if (item.data == null) {
 | 
						|
          return const SizedBox.shrink();
 | 
						|
        }
 | 
						|
        Widget itemWidget;
 | 
						|
 | 
						|
        switch (item.type) {
 | 
						|
          case 'posts.new':
 | 
						|
          case 'posts.new.replies':
 | 
						|
            itemWidget = PostActionableItem(
 | 
						|
              borderRadius: 8,
 | 
						|
              item: SnPost.fromJson(item.data!),
 | 
						|
              onRefresh: () {
 | 
						|
                activitiesNotifier.forceRefresh();
 | 
						|
              },
 | 
						|
              onUpdate: (post) {
 | 
						|
                activitiesNotifier.updateOne(
 | 
						|
                  index,
 | 
						|
                  item.copyWith(data: post.toJson()),
 | 
						|
                );
 | 
						|
              },
 | 
						|
            );
 | 
						|
            itemWidget = Card(margin: EdgeInsets.zero, child: itemWidget);
 | 
						|
            break;
 | 
						|
          case 'discovery':
 | 
						|
            itemWidget = _DiscoveryActivityItem(data: item.data!);
 | 
						|
            break;
 | 
						|
          default:
 | 
						|
            itemWidget = const Placeholder();
 | 
						|
        }
 | 
						|
 | 
						|
        return itemWidget;
 | 
						|
      },
 | 
						|
    );
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
@riverpod
 | 
						|
class ActivityListNotifier extends _$ActivityListNotifier
 | 
						|
    with CursorPagingNotifierMixin<SnActivity> {
 | 
						|
  @override
 | 
						|
  Future<CursorPagingData<SnActivity>> build(String? filter) =>
 | 
						|
      fetch(cursor: null);
 | 
						|
 | 
						|
  @override
 | 
						|
  Future<CursorPagingData<SnActivity>> fetch({required String? cursor}) async {
 | 
						|
    final client = ref.read(apiClientProvider);
 | 
						|
    final take = 20;
 | 
						|
 | 
						|
    final queryParameters = {
 | 
						|
      if (cursor != null) 'cursor': cursor,
 | 
						|
      'take': take,
 | 
						|
      if (filter != null) 'filter': filter,
 | 
						|
      if (kDebugMode)
 | 
						|
        'debugInclude': 'realms,publishers,articles,shuffledPosts',
 | 
						|
    };
 | 
						|
 | 
						|
    final response = await client.get(
 | 
						|
      '/sphere/activities',
 | 
						|
      queryParameters: queryParameters,
 | 
						|
    );
 | 
						|
 | 
						|
    final List<SnActivity> items =
 | 
						|
        (response.data as List)
 | 
						|
            .map((e) => SnActivity.fromJson(e as Map<String, dynamic>))
 | 
						|
            .toList();
 | 
						|
 | 
						|
    final hasMore = (items.firstOrNull?.type ?? 'empty') != 'empty';
 | 
						|
    final nextCursor =
 | 
						|
        items.isNotEmpty
 | 
						|
            ? items
 | 
						|
                .map((x) => x.createdAt)
 | 
						|
                .reduce((a, b) => a.isBefore(b) ? a : b)
 | 
						|
                .toUtc()
 | 
						|
                .toIso8601String()
 | 
						|
            : null;
 | 
						|
 | 
						|
    return CursorPagingData(
 | 
						|
      items: items,
 | 
						|
      hasMore: hasMore,
 | 
						|
      nextCursor: nextCursor,
 | 
						|
    );
 | 
						|
  }
 | 
						|
 | 
						|
  void updateOne(int index, SnActivity activity) {
 | 
						|
    final currentState = state.valueOrNull;
 | 
						|
    if (currentState == null) return;
 | 
						|
 | 
						|
    final updatedItems = [...currentState.items];
 | 
						|
    updatedItems[index] = activity;
 | 
						|
 | 
						|
    state = AsyncData(
 | 
						|
      CursorPagingData(
 | 
						|
        items: updatedItems,
 | 
						|
        hasMore: currentState.hasMore,
 | 
						|
        nextCursor: currentState.nextCursor,
 | 
						|
      ),
 | 
						|
    );
 | 
						|
  }
 | 
						|
}
 |