Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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<List<CommunityTokenBase>>? _realtimeSubscription;
Timer? _sessionExpiryTimer;
bool _disposed = false;
late final TokenCategoryType _type;
String? _tokenType;

Expand All @@ -21,6 +25,9 @@ class CategoryTokensNotifier extends _$CategoryTokensNotifier {
_type = type;

ref.onDispose(() async {
_disposed = true;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't we have an issue with this _disposed - u set it to true but u never reset it.
ref.onDispose is called, for example, when a dependency is changed and the same instance is used, so the provider will remain in _disposed->true state in this case.
And why do we need this flag in the first place? The only place where it is used is in the Timer callback and u cancel all the timers in the same place u set this flag, so isn't it redundant?

_sessionExpiryTimer?.cancel();
_sessionExpiryTimer = null;
await _realtimeSubscription?.close();
_realtimeSubscription = null;
});
Expand All @@ -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);

@ice-orion ice-orion Mar 16, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason we refresh the token exactly 30 seconds before it expires?

_sessionExpiryTimer = Timer(Duration(seconds: refreshInSec), () {
Comment on lines +40 to +46
if (_disposed) return;
unawaited(refresh());
});
}

Future<void> _initialize() async {
if (state.sessionId != null) return;

Expand All @@ -40,29 +59,31 @@ class CategoryTokensNotifier extends _$CategoryTokensNotifier {
);

state = state.copyWith(sessionId: session.id);
_scheduleSessionRecreate(session);

unawaited(_subscribeToRealtimeUpdates(session.id, _type));
unawaited(_loadInitial());
}

Future<void> _loadInitial() async {
Future<void> _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,
Expand Down Expand Up @@ -236,6 +257,8 @@ class CategoryTokensNotifier extends _$CategoryTokensNotifier {
}

Future<void> refresh() async {
_sessionExpiryTimer?.cancel();
_sessionExpiryTimer = null;
await _realtimeSubscription?.close();
_realtimeSubscription = null;
state = const CategoryTokensState();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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',
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Comment on lines +21 to +22
class CreatorTokensTabContent extends HookConsumerWidget {
const CreatorTokensTabContent({
required this.tabType,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
);

Expand All @@ -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,
},
);

Expand Down
Loading