diff --git a/lib/app/features/tokenized_communities/providers/category_tokens_provider.r.dart b/lib/app/features/tokenized_communities/providers/category_tokens_provider.r.dart index b4d1893ead2..a17341e2a6f 100644 --- a/lib/app/features/tokenized_communities/providers/category_tokens_provider.r.dart +++ b/lib/app/features/tokenized_communities/providers/category_tokens_provider.r.dart @@ -12,7 +12,11 @@ part 'category_tokens_provider.r.g.dart'; @riverpod class CategoryTokensNotifier extends _$CategoryTokensNotifier { static const int _limit = 10; + static const int _sessionRefreshBeforeExpirySec = 30; + NetworkSubscription>? _realtimeSubscription; + Timer? _sessionExpiryTimer; + bool _disposed = false; late final TokenCategoryType _type; String? _tokenType; @@ -21,6 +25,9 @@ class CategoryTokensNotifier extends _$CategoryTokensNotifier { _type = type; ref.onDispose(() async { + _disposed = true; + _sessionExpiryTimer?.cancel(); + _sessionExpiryTimer = null; await _realtimeSubscription?.close(); _realtimeSubscription = null; }); @@ -30,6 +37,18 @@ class CategoryTokensNotifier extends _$CategoryTokensNotifier { return const CategoryTokensState(); } + /// TTL from API: typically seconds (e.g. 300 = 5 min); + void _scheduleSessionRecreate(ViewingSession session) { + _sessionExpiryTimer?.cancel(); + _sessionExpiryTimer = null; + final ttlSec = session.ttl >= 1000 ? session.ttl ~/ 1000 : session.ttl; + final refreshInSec = (ttlSec - _sessionRefreshBeforeExpirySec).clamp(1, ttlSec); + _sessionExpiryTimer = Timer(Duration(seconds: refreshInSec), () { + if (_disposed) return; + unawaited(refresh()); + }); + } + Future _initialize() async { if (state.sessionId != null) return; @@ -40,29 +59,31 @@ class CategoryTokensNotifier extends _$CategoryTokensNotifier { ); state = state.copyWith(sessionId: session.id); + _scheduleSessionRecreate(session); unawaited(_subscribeToRealtimeUpdates(session.id, _type)); unawaited(_loadInitial()); } - Future _loadInitial() async { + Future _loadInitial({String? sessionId}) async { if (state.browsingIsLoading || state.browsingIsInitialLoading) return; + final effectiveSessionId = sessionId ?? state.sessionId; + if (effectiveSessionId == null) { + state = state.copyWith( + browsingIsLoading: false, + browsingIsInitialLoading: false, + ); + return; + } + state = state.copyWith(browsingIsInitialLoading: true, browsingIsLoading: true); try { final client = await ref.read(ionTokenAnalyticsClientProvider.future); - final sessionId = state.sessionId; - if (sessionId == null) { - state = state.copyWith( - browsingIsLoading: false, - browsingIsInitialLoading: false, - ); - return; - } final page = await client.communityTokens.getCategoryTokens( - sessionId: sessionId, + sessionId: effectiveSessionId, type: _type, tokenType: _tokenType, limit: _limit, @@ -236,6 +257,8 @@ class CategoryTokensNotifier extends _$CategoryTokensNotifier { } Future refresh() async { + _sessionExpiryTimer?.cancel(); + _sessionExpiryTimer = null; await _realtimeSubscription?.close(); _realtimeSubscription = null; state = const CategoryTokensState(); diff --git a/lib/app/features/user/pages/creator_tokens/models/token_type_filter.dart b/lib/app/features/user/pages/creator_tokens/models/token_type_filter.dart index fd36fc5efc0..53d6bee67cd 100644 --- a/lib/app/features/user/pages/creator_tokens/models/token_type_filter.dart +++ b/lib/app/features/user/pages/creator_tokens/models/token_type_filter.dart @@ -36,12 +36,17 @@ enum TokenTypeFilter { }; } + /// Backend type filter for tokens (analytics API). + /// Single source of truth for the "type" query param used by: + /// - Trending & Top tabs (viewing-sessions), + /// - Latest tab (getLatestTokens / subscribeToLatestTokens). + /// Values: "onlineplus_creator", "onlineplus_content", "xcom" (from Swagger). String? get requestType { return switch (this) { TokenTypeFilter.all => null, TokenTypeFilter.general => null, - TokenTypeFilter.creator => 'profile', - TokenTypeFilter.content => 'anyPost', + TokenTypeFilter.creator => 'onlineplus_creator', + TokenTypeFilter.content => 'onlineplus_content', TokenTypeFilter.x => 'xcom', }; } diff --git a/lib/app/features/user/pages/creator_tokens/views/creator_tokens_page/components/tabs/creator_tokens_tab_content.dart b/lib/app/features/user/pages/creator_tokens/views/creator_tokens_page/components/tabs/creator_tokens_tab_content.dart index 5bbd5cbef74..a2efbedfa96 100644 --- a/lib/app/features/user/pages/creator_tokens/views/creator_tokens_page/components/tabs/creator_tokens_tab_content.dart +++ b/lib/app/features/user/pages/creator_tokens/views/creator_tokens_page/components/tabs/creator_tokens_tab_content.dart @@ -15,6 +15,11 @@ import 'package:ion/app/features/user/pages/creator_tokens/providers/creator_tok import 'package:ion/app/features/user/pages/creator_tokens/views/creator_tokens_page/components/list/creator_tokens_list.dart'; import 'package:ion_token_analytics/ion_token_analytics.dart'; +/// Content for Trending, Top, and Latest tabs. +/// +/// Filtering by token type (Creator / Content / X) is done entirely on the backend: +/// [requestType] is sent to the analytics API for category (Trending/Top) and +/// latest endpoints; the UI shows [state.activeItems] with no client-side filtering. class CreatorTokensTabContent extends HookConsumerWidget { const CreatorTokensTabContent({ required this.tabType, diff --git a/packages/ion_token_analytics/lib/src/community_tokens/latest_tokens/latest_tokens_repository_impl.dart b/packages/ion_token_analytics/lib/src/community_tokens/latest_tokens/latest_tokens_repository_impl.dart index b10fd515165..79f3005f731 100644 --- a/packages/ion_token_analytics/lib/src/community_tokens/latest_tokens/latest_tokens_repository_impl.dart +++ b/packages/ion_token_analytics/lib/src/community_tokens/latest_tokens/latest_tokens_repository_impl.dart @@ -23,7 +23,7 @@ class LatestTokensRepositoryImpl implements LatestTokensRepository { 'limit': limit, 'offset': offset, if (keyword != null && keyword.isNotEmpty) 'keyword': keyword, - if (type != null) 'type': type, + if (type != null && type.isNotEmpty) 'type': type, }, ); @@ -43,7 +43,7 @@ class LatestTokensRepositoryImpl implements LatestTokensRepository { '/v1sse/community-tokens/latest', queryParameters: { if (keyword != null && keyword.isNotEmpty) 'keyword': keyword, - if (type != null) 'type': type, + if (type != null && type.isNotEmpty) 'type': type, }, );