diff --git a/lib/pods/activity/activity_rpc.dart b/lib/pods/activity/activity_rpc.dart index 3a3b2590..978d4bdf 100644 --- a/lib/pods/activity/activity_rpc.dart +++ b/lib/pods/activity/activity_rpc.dart @@ -4,6 +4,7 @@ import 'dart:developer' as developer; import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:island/models/account.dart'; import 'package:island/pods/network.dart'; import 'package:island/widgets/account/status.dart'; import 'package:shelf/shelf.dart'; @@ -395,14 +396,31 @@ final rpcServerStateProvider = ); final label = data['args']['activity']['details'] ?? ''; final appId = socket.clientId; + final meta = data['args']['activity']; try { await setRemoteActivityStatus( ref, label, appId, - data['args']['activity'], + meta, ); - ref.invalidate(accountStatusProvider('me')); + final now = DateTime.now(); + final status = SnAccountStatus( + id: 'local_$appId', + attitude: 0, + isOnline: true, + isInvisible: false, + isNotDisturb: false, + isCustomized: true, + label: label, + meta: meta, + clearedAt: null, + accountId: 'me', + createdAt: now, + updatedAt: now, + deletedAt: null, + ); + ref.read(currentAccountStatusProvider.notifier).setStatus(status); } catch (e) { developer.log( 'Failed to set remote activity status: $e', @@ -422,7 +440,7 @@ final rpcServerStateProvider = final appId = socket.clientId; try { await unsetRemoteActivityStatus(ref, appId); - ref.invalidate(accountStatusProvider('me')); + ref.read(currentAccountStatusProvider.notifier).clearStatus(); } catch (e) { developer.log( 'Failed to unset remote activity status: $e', diff --git a/lib/utils/activity_utils.dart b/lib/utils/activity_utils.dart new file mode 100644 index 00000000..8cc79532 --- /dev/null +++ b/lib/utils/activity_utils.dart @@ -0,0 +1,122 @@ +import 'package:flutter/material.dart'; +import 'package:island/models/account.dart'; + +String? getActivityTitle(String? label, Map? meta) { + if (meta == null) return label; + if (meta['assets']?['large_text'] is String) { + return meta['assets']?['large_text']; + } + return label; +} + +String? getActivitySubtitle(Map? meta) { + if (meta == null) return null; + if (meta['assets']?['small_text'] is String) { + return meta['assets']?['small_text']; + } + return null; +} + +InlineSpan getActivityFullMessage(SnAccountStatus? status) { + if (status?.meta == null) return TextSpan(text: 'No activity details available'); + final meta = status!.meta!; + final List spans = []; + if (meta.containsKey('assets') && meta['assets'] is Map) { + final assets = meta['assets'] as Map; + if (assets.containsKey('large_text')) { + spans.add(TextSpan(text: assets['large_text'], style: TextStyle(fontWeight: FontWeight.bold))); + } + if (assets.containsKey('small_text')) { + if (spans.isNotEmpty) spans.add(TextSpan(text: '\n')); + spans.add(TextSpan(text: assets['small_text'])); + } + } + String normalText = ''; + if (meta.containsKey('details')) { + normalText += 'Details: ${meta['details']}\n'; + } + if (meta.containsKey('state')) { + normalText += 'State: ${meta['state']}\n'; + } + if (meta.containsKey('timestamps') && meta['timestamps'] is Map) { + final ts = meta['timestamps'] as Map; + if (ts.containsKey('start') && ts['start'] is int) { + final start = DateTime.fromMillisecondsSinceEpoch(ts['start'] * 1000); + normalText += 'Started: ${start.toLocal()}\n'; + } + if (ts.containsKey('end') && ts['end'] is int) { + final end = DateTime.fromMillisecondsSinceEpoch(ts['end'] * 1000); + normalText += 'Ends: ${end.toLocal()}\n'; + } + } + if (meta.containsKey('party') && meta['party'] is Map) { + final party = meta['party'] as Map; + if (party.containsKey('size') && party['size'] is List && party['size'].length >= 2) { + final size = party['size'] as List; + normalText += 'Party: ${size[0]}/${size[1]}\n'; + } + } + if (meta.containsKey('instance')) { + normalText += 'Instance: ${meta['instance']}\n'; + } + // Add other keys if present + meta.forEach((key, value) { + if (!['details', 'state', 'timestamps', 'assets', 'party', 'secrets', 'instance'].contains(key)) { + normalText += '$key: $value\n'; + } + }); + if (normalText.isNotEmpty) { + if (spans.isNotEmpty) spans.add(TextSpan(text: '\n')); + spans.add(TextSpan(text: normalText.trimRight())); + } + return TextSpan(children: spans); +} + +Widget buildActivityDetails(SnAccountStatus? status) { + if (status?.meta == null) return Text('No activity details available'); + final meta = status!.meta!; + final List children = []; + if (meta.containsKey('assets') && meta['assets'] is Map) { + final assets = meta['assets'] as Map; + if (assets.containsKey('large_text')) { + children.add(Text(assets['large_text'])); + } + if (assets.containsKey('small_text')) { + children.add(Text(assets['small_text'])); + } + } + if (meta.containsKey('details')) { + children.add(Text('Details: ${meta['details']}')); + } + if (meta.containsKey('state')) { + children.add(Text('State: ${meta['state']}')); + } + if (meta.containsKey('timestamps') && meta['timestamps'] is Map) { + final ts = meta['timestamps'] as Map; + if (ts.containsKey('start') && ts['start'] is int) { + final start = DateTime.fromMillisecondsSinceEpoch(ts['start'] * 1000); + children.add(Text('Started: ${start.toLocal()}')); + } + if (ts.containsKey('end') && ts['end'] is int) { + final end = DateTime.fromMillisecondsSinceEpoch(ts['end'] * 1000); + children.add(Text('Ends: ${end.toLocal()}')); + } + } + if (meta.containsKey('party') && meta['party'] is Map) { + final party = meta['party'] as Map; + if (party.containsKey('size') && party['size'] is List && party['size'].length >= 2) { + final size = party['size'] as List; + children.add(Text('Party: ${size[0]}/${size[1]}')); + } + } + if (meta.containsKey('instance')) { + children.add(Text('Instance: ${meta['instance']}')); + } + // Add other keys if present + children.addAll(meta.entries.where((e) => !['details', 'state', 'timestamps', 'assets', 'party', 'secrets', 'instance'].contains(e.key)).map((e) => Text('${e.key}: ${e.value}'))); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: children, + ); +} diff --git a/lib/widgets/account/event_details_widget.dart b/lib/widgets/account/event_details_widget.dart index 25102daf..3dc47032 100644 --- a/lib/widgets/account/event_details_widget.dart +++ b/lib/widgets/account/event_details_widget.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:gap/gap.dart'; import 'package:island/models/activity.dart'; import 'package:island/services/time.dart'; +import 'package:island/utils/activity_utils.dart'; import 'package:material_symbols_icons/symbols.dart'; import 'package:styled_widget/styled_widget.dart'; @@ -75,7 +76,10 @@ class EventDetailsWidget extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(status.label), + if ((getActivityTitle(status.label, status.meta) ?? status.label).isNotEmpty) + Text(getActivityTitle(status.label, status.meta) ?? status.label), + if (getActivitySubtitle(status.meta) != null) + Text(getActivitySubtitle(status.meta)!).fontSize(11).opacity(0.8), Text( '${status.createdAt.formatSystem()} - ${status.clearedAt?.formatSystem() ?? 'present'.tr()}', ).fontSize(11).opacity(0.8), diff --git a/lib/widgets/account/status.dart b/lib/widgets/account/status.dart index 71124124..b002e7f1 100644 --- a/lib/widgets/account/status.dart +++ b/lib/widgets/account/status.dart @@ -4,8 +4,10 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:island/models/account.dart'; import 'package:island/pods/network.dart'; +import 'package:island/pods/userinfo.dart'; import 'package:island/screens/account/profile.dart'; import 'package:island/services/time.dart'; +import 'package:island/utils/activity_utils.dart'; import 'package:island/widgets/account/status_creation.dart'; import 'package:material_symbols_icons/symbols.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -13,8 +15,31 @@ import 'package:styled_widget/styled_widget.dart'; part 'status.g.dart'; +class CurrentAccountStatusNotifier extends StateNotifier { + CurrentAccountStatusNotifier() : super(null); + + void setStatus(SnAccountStatus status) { + state = status; + } + + void clearStatus() { + state = null; + } +} + +final currentAccountStatusProvider = StateNotifierProvider((ref) { + return CurrentAccountStatusNotifier(); +}); + @riverpod Future accountStatus(Ref ref, String uname) async { + final userInfo = ref.watch(userInfoProvider); + if (uname == 'me' || (userInfo.value != null && uname == userInfo.value!.name)) { + final local = ref.watch(currentAccountStatusProvider); + if (local != null) { + return local; + } + } final apiClient = ref.watch(apiClientProvider); try { final resp = await apiClient.get('/id/accounts/$uname/statuses'); @@ -110,129 +135,13 @@ class AccountStatusWidget extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final status = ref.watch(accountStatusProvider(uname)); + final userInfo = ref.watch(userInfoProvider); + final localStatus = ref.watch(currentAccountStatusProvider); + final status = (uname == 'me' || (userInfo.value != null && uname == userInfo.value!.name && localStatus != null)) + ? AsyncValue.data(localStatus) + : ref.watch(accountStatusProvider(uname)); final account = ref.watch(accountProvider(uname)); - String? getActivityTitle(String? label, Map? meta) { - if (meta == null) return label; - if (meta['assets']?['large_text'] is String) { - return meta['assets']?['large_text']; - } - return label; - } - - String? getActivitySubtitle(Map? meta) { - if (meta == null) return null; - if (meta['assets']?['small_text'] is String) { - return meta['assets']?['small_text']; - } - return null; - } - - InlineSpan getActivityFullMessage(SnAccountStatus? status) { - if (status?.meta == null) return TextSpan(text: 'No activity details available'); - final meta = status!.meta!; - final List spans = []; - if (meta.containsKey('assets') && meta['assets'] is Map) { - final assets = meta['assets'] as Map; - if (assets.containsKey('large_text')) { - spans.add(TextSpan(text: assets['large_text'], style: TextStyle(fontWeight: FontWeight.bold))); - } - if (assets.containsKey('small_text')) { - if (spans.isNotEmpty) spans.add(TextSpan(text: '\n')); - spans.add(TextSpan(text: assets['small_text'])); - } - } - String normalText = ''; - if (meta.containsKey('details')) { - normalText += 'Details: ${meta['details']}\n'; - } - if (meta.containsKey('state')) { - normalText += 'State: ${meta['state']}\n'; - } - if (meta.containsKey('timestamps') && meta['timestamps'] is Map) { - final ts = meta['timestamps'] as Map; - if (ts.containsKey('start') && ts['start'] is int) { - final start = DateTime.fromMillisecondsSinceEpoch(ts['start'] * 1000); - normalText += 'Started: ${start.toLocal()}\n'; - } - if (ts.containsKey('end') && ts['end'] is int) { - final end = DateTime.fromMillisecondsSinceEpoch(ts['end'] * 1000); - normalText += 'Ends: ${end.toLocal()}\n'; - } - } - if (meta.containsKey('party') && meta['party'] is Map) { - final party = meta['party'] as Map; - if (party.containsKey('size') && party['size'] is List && party['size'].length >= 2) { - final size = party['size'] as List; - normalText += 'Party: ${size[0]}/${size[1]}\n'; - } - } - if (meta.containsKey('instance')) { - normalText += 'Instance: ${meta['instance']}\n'; - } - // Add other keys if present - meta.forEach((key, value) { - if (!['details', 'state', 'timestamps', 'assets', 'party', 'secrets', 'instance'].contains(key)) { - normalText += '$key: $value\n'; - } - }); - if (normalText.isNotEmpty) { - if (spans.isNotEmpty) spans.add(TextSpan(text: '\n')); - spans.add(TextSpan(text: normalText.trimRight())); - } - return TextSpan(children: spans); - } - - Widget _buildActivityDetails(SnAccountStatus? status) { - if (status?.meta == null) return Text('No activity details available'); - final meta = status!.meta!; - final List children = []; - if (meta.containsKey('assets') && meta['assets'] is Map) { - final assets = meta['assets'] as Map; - if (assets.containsKey('large_text')) { - children.add(Text(assets['large_text'])); - } - if (assets.containsKey('small_text')) { - children.add(Text(assets['small_text'])); - } - } - if (meta.containsKey('details')) { - children.add(Text('Details: ${meta['details']}')); - } - if (meta.containsKey('state')) { - children.add(Text('State: ${meta['state']}')); - } - if (meta.containsKey('timestamps') && meta['timestamps'] is Map) { - final ts = meta['timestamps'] as Map; - if (ts.containsKey('start') && ts['start'] is int) { - final start = DateTime.fromMillisecondsSinceEpoch(ts['start'] * 1000); - children.add(Text('Started: ${start.toLocal()}')); - } - if (ts.containsKey('end') && ts['end'] is int) { - final end = DateTime.fromMillisecondsSinceEpoch(ts['end'] * 1000); - children.add(Text('Ends: ${end.toLocal()}')); - } - } - if (meta.containsKey('party') && meta['party'] is Map) { - final party = meta['party'] as Map; - if (party.containsKey('size') && party['size'] is List && party['size'].length >= 2) { - final size = party['size'] as List; - children.add(Text('Party: ${size[0]}/${size[1]}')); - } - } - if (meta.containsKey('instance')) { - children.add(Text('Instance: ${meta['instance']}')); - } - // Add other keys if present - children.addAll(meta.entries.where((e) => !['details', 'state', 'timestamps', 'assets', 'party', 'secrets', 'instance'].contains(e.key)).map((e) => Text('${e.key}: ${e.value}'))); - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: children, - ); - } - return Padding( padding: padding ?? EdgeInsets.symmetric(horizontal: 27, vertical: 4), child: Row( @@ -259,7 +168,7 @@ class AccountStatusWidget extends HookConsumerWidget { context: context, builder: (context) => AlertDialog( title: Text('Activity Details'), - content: _buildActivityDetails(status.value), + content: buildActivityDetails(status.value), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), diff --git a/lib/widgets/check_in.dart b/lib/widgets/check_in.dart index 41f7cd5b..afd43d2f 100644 --- a/lib/widgets/check_in.dart +++ b/lib/widgets/check_in.dart @@ -229,6 +229,7 @@ class CheckInWidget extends HookConsumerWidget { Column( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.end, + spacing: 4, children: [ AnimatedSwitcher( duration: const Duration(milliseconds: 300),