diff --git a/assets/i18n/en-US.json b/assets/i18n/en-US.json index 7c5f4a94..318237b9 100644 --- a/assets/i18n/en-US.json +++ b/assets/i18n/en-US.json @@ -570,6 +570,7 @@ "checkInResultT2": "Mid", "checkInResultT3": "Good", "checkInResultT4": "Best", + "checkInResultT5": "Birthday", "accountProfileView": "View Profile", "unspecified": "Unspecified", "added": "Added", diff --git a/lib/models/account.dart b/lib/models/account.dart index dc12d9be..4c95dfc2 100644 --- a/lib/models/account.dart +++ b/lib/models/account.dart @@ -98,6 +98,7 @@ sealed class SnAccountStatus with _$SnAccountStatus { required bool isNotDisturb, required bool isCustomized, @Default("") String label, + required Map? meta, required DateTime? clearedAt, required String accountId, required DateTime createdAt, diff --git a/lib/models/account.freezed.dart b/lib/models/account.freezed.dart index cc4a035c..045de96d 100644 --- a/lib/models/account.freezed.dart +++ b/lib/models/account.freezed.dart @@ -1053,7 +1053,7 @@ $SnVerificationMarkCopyWith<$Res>? get verification { /// @nodoc mixin _$SnAccountStatus { - String get id; int get attitude; bool get isOnline; bool get isInvisible; bool get isNotDisturb; bool get isCustomized; String get label; DateTime? get clearedAt; String get accountId; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; + String get id; int get attitude; bool get isOnline; bool get isInvisible; bool get isNotDisturb; bool get isCustomized; String get label; Map? get meta; DateTime? get clearedAt; String get accountId; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; /// Create a copy of SnAccountStatus /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @@ -1066,16 +1066,16 @@ $SnAccountStatusCopyWith get copyWith => _$SnAccountStatusCopyW @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is SnAccountStatus&&(identical(other.id, id) || other.id == id)&&(identical(other.attitude, attitude) || other.attitude == attitude)&&(identical(other.isOnline, isOnline) || other.isOnline == isOnline)&&(identical(other.isInvisible, isInvisible) || other.isInvisible == isInvisible)&&(identical(other.isNotDisturb, isNotDisturb) || other.isNotDisturb == isNotDisturb)&&(identical(other.isCustomized, isCustomized) || other.isCustomized == isCustomized)&&(identical(other.label, label) || other.label == label)&&(identical(other.clearedAt, clearedAt) || other.clearedAt == clearedAt)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is SnAccountStatus&&(identical(other.id, id) || other.id == id)&&(identical(other.attitude, attitude) || other.attitude == attitude)&&(identical(other.isOnline, isOnline) || other.isOnline == isOnline)&&(identical(other.isInvisible, isInvisible) || other.isInvisible == isInvisible)&&(identical(other.isNotDisturb, isNotDisturb) || other.isNotDisturb == isNotDisturb)&&(identical(other.isCustomized, isCustomized) || other.isCustomized == isCustomized)&&(identical(other.label, label) || other.label == label)&&const DeepCollectionEquality().equals(other.meta, meta)&&(identical(other.clearedAt, clearedAt) || other.clearedAt == clearedAt)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)); } @JsonKey(includeFromJson: false, includeToJson: false) @override -int get hashCode => Object.hash(runtimeType,id,attitude,isOnline,isInvisible,isNotDisturb,isCustomized,label,clearedAt,accountId,createdAt,updatedAt,deletedAt); +int get hashCode => Object.hash(runtimeType,id,attitude,isOnline,isInvisible,isNotDisturb,isCustomized,label,const DeepCollectionEquality().hash(meta),clearedAt,accountId,createdAt,updatedAt,deletedAt); @override String toString() { - return 'SnAccountStatus(id: $id, attitude: $attitude, isOnline: $isOnline, isInvisible: $isInvisible, isNotDisturb: $isNotDisturb, isCustomized: $isCustomized, label: $label, clearedAt: $clearedAt, accountId: $accountId, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; + return 'SnAccountStatus(id: $id, attitude: $attitude, isOnline: $isOnline, isInvisible: $isInvisible, isNotDisturb: $isNotDisturb, isCustomized: $isCustomized, label: $label, meta: $meta, clearedAt: $clearedAt, accountId: $accountId, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; } @@ -1086,7 +1086,7 @@ abstract mixin class $SnAccountStatusCopyWith<$Res> { factory $SnAccountStatusCopyWith(SnAccountStatus value, $Res Function(SnAccountStatus) _then) = _$SnAccountStatusCopyWithImpl; @useResult $Res call({ - String id, int attitude, bool isOnline, bool isInvisible, bool isNotDisturb, bool isCustomized, String label, DateTime? clearedAt, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt + String id, int attitude, bool isOnline, bool isInvisible, bool isNotDisturb, bool isCustomized, String label, Map? meta, DateTime? clearedAt, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt }); @@ -1103,7 +1103,7 @@ class _$SnAccountStatusCopyWithImpl<$Res> /// Create a copy of SnAccountStatus /// with the given fields replaced by the non-null parameter values. -@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? attitude = null,Object? isOnline = null,Object? isInvisible = null,Object? isNotDisturb = null,Object? isCustomized = null,Object? label = null,Object? clearedAt = freezed,Object? accountId = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) { +@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? attitude = null,Object? isOnline = null,Object? isInvisible = null,Object? isNotDisturb = null,Object? isCustomized = null,Object? label = null,Object? meta = freezed,Object? clearedAt = freezed,Object? accountId = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) { return _then(_self.copyWith( id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable as String,attitude: null == attitude ? _self.attitude : attitude // ignore: cast_nullable_to_non_nullable @@ -1112,7 +1112,8 @@ as bool,isInvisible: null == isInvisible ? _self.isInvisible : isInvisible // ig as bool,isNotDisturb: null == isNotDisturb ? _self.isNotDisturb : isNotDisturb // ignore: cast_nullable_to_non_nullable as bool,isCustomized: null == isCustomized ? _self.isCustomized : isCustomized // ignore: cast_nullable_to_non_nullable as bool,label: null == label ? _self.label : label // ignore: cast_nullable_to_non_nullable -as String,clearedAt: freezed == clearedAt ? _self.clearedAt : clearedAt // ignore: cast_nullable_to_non_nullable +as String,meta: freezed == meta ? _self.meta : meta // ignore: cast_nullable_to_non_nullable +as Map?,clearedAt: freezed == clearedAt ? _self.clearedAt : clearedAt // ignore: cast_nullable_to_non_nullable as DateTime?,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable as String,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable @@ -1199,10 +1200,10 @@ return $default(_that);case _: /// } /// ``` -@optionalTypeArgs TResult maybeWhen(TResult Function( String id, int attitude, bool isOnline, bool isInvisible, bool isNotDisturb, bool isCustomized, String label, DateTime? clearedAt, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,{required TResult orElse(),}) {final _that = this; +@optionalTypeArgs TResult maybeWhen(TResult Function( String id, int attitude, bool isOnline, bool isInvisible, bool isNotDisturb, bool isCustomized, String label, Map? meta, DateTime? clearedAt, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,{required TResult orElse(),}) {final _that = this; switch (_that) { case _SnAccountStatus() when $default != null: -return $default(_that.id,_that.attitude,_that.isOnline,_that.isInvisible,_that.isNotDisturb,_that.isCustomized,_that.label,_that.clearedAt,_that.accountId,_that.createdAt,_that.updatedAt,_that.deletedAt);case _: +return $default(_that.id,_that.attitude,_that.isOnline,_that.isInvisible,_that.isNotDisturb,_that.isCustomized,_that.label,_that.meta,_that.clearedAt,_that.accountId,_that.createdAt,_that.updatedAt,_that.deletedAt);case _: return orElse(); } @@ -1220,10 +1221,10 @@ return $default(_that.id,_that.attitude,_that.isOnline,_that.isInvisible,_that.i /// } /// ``` -@optionalTypeArgs TResult when(TResult Function( String id, int attitude, bool isOnline, bool isInvisible, bool isNotDisturb, bool isCustomized, String label, DateTime? clearedAt, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt) $default,) {final _that = this; +@optionalTypeArgs TResult when(TResult Function( String id, int attitude, bool isOnline, bool isInvisible, bool isNotDisturb, bool isCustomized, String label, Map? meta, DateTime? clearedAt, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt) $default,) {final _that = this; switch (_that) { case _SnAccountStatus(): -return $default(_that.id,_that.attitude,_that.isOnline,_that.isInvisible,_that.isNotDisturb,_that.isCustomized,_that.label,_that.clearedAt,_that.accountId,_that.createdAt,_that.updatedAt,_that.deletedAt);} +return $default(_that.id,_that.attitude,_that.isOnline,_that.isInvisible,_that.isNotDisturb,_that.isCustomized,_that.label,_that.meta,_that.clearedAt,_that.accountId,_that.createdAt,_that.updatedAt,_that.deletedAt);} } /// A variant of `when` that fallback to returning `null` /// @@ -1237,10 +1238,10 @@ return $default(_that.id,_that.attitude,_that.isOnline,_that.isInvisible,_that.i /// } /// ``` -@optionalTypeArgs TResult? whenOrNull(TResult? Function( String id, int attitude, bool isOnline, bool isInvisible, bool isNotDisturb, bool isCustomized, String label, DateTime? clearedAt, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,) {final _that = this; +@optionalTypeArgs TResult? whenOrNull(TResult? Function( String id, int attitude, bool isOnline, bool isInvisible, bool isNotDisturb, bool isCustomized, String label, Map? meta, DateTime? clearedAt, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,) {final _that = this; switch (_that) { case _SnAccountStatus() when $default != null: -return $default(_that.id,_that.attitude,_that.isOnline,_that.isInvisible,_that.isNotDisturb,_that.isCustomized,_that.label,_that.clearedAt,_that.accountId,_that.createdAt,_that.updatedAt,_that.deletedAt);case _: +return $default(_that.id,_that.attitude,_that.isOnline,_that.isInvisible,_that.isNotDisturb,_that.isCustomized,_that.label,_that.meta,_that.clearedAt,_that.accountId,_that.createdAt,_that.updatedAt,_that.deletedAt);case _: return null; } @@ -1252,7 +1253,7 @@ return $default(_that.id,_that.attitude,_that.isOnline,_that.isInvisible,_that.i @JsonSerializable() class _SnAccountStatus implements SnAccountStatus { - const _SnAccountStatus({required this.id, required this.attitude, required this.isOnline, required this.isInvisible, required this.isNotDisturb, required this.isCustomized, this.label = "", required this.clearedAt, required this.accountId, required this.createdAt, required this.updatedAt, required this.deletedAt}); + const _SnAccountStatus({required this.id, required this.attitude, required this.isOnline, required this.isInvisible, required this.isNotDisturb, required this.isCustomized, this.label = "", required final Map? meta, required this.clearedAt, required this.accountId, required this.createdAt, required this.updatedAt, required this.deletedAt}): _meta = meta; factory _SnAccountStatus.fromJson(Map json) => _$SnAccountStatusFromJson(json); @override final String id; @@ -1262,6 +1263,15 @@ class _SnAccountStatus implements SnAccountStatus { @override final bool isNotDisturb; @override final bool isCustomized; @override@JsonKey() final String label; + final Map? _meta; +@override Map? get meta { + final value = _meta; + if (value == null) return null; + if (_meta is EqualUnmodifiableMapView) return _meta; + // ignore: implicit_dynamic_type + return EqualUnmodifiableMapView(value); +} + @override final DateTime? clearedAt; @override final String accountId; @override final DateTime createdAt; @@ -1281,16 +1291,16 @@ Map toJson() { @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnAccountStatus&&(identical(other.id, id) || other.id == id)&&(identical(other.attitude, attitude) || other.attitude == attitude)&&(identical(other.isOnline, isOnline) || other.isOnline == isOnline)&&(identical(other.isInvisible, isInvisible) || other.isInvisible == isInvisible)&&(identical(other.isNotDisturb, isNotDisturb) || other.isNotDisturb == isNotDisturb)&&(identical(other.isCustomized, isCustomized) || other.isCustomized == isCustomized)&&(identical(other.label, label) || other.label == label)&&(identical(other.clearedAt, clearedAt) || other.clearedAt == clearedAt)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnAccountStatus&&(identical(other.id, id) || other.id == id)&&(identical(other.attitude, attitude) || other.attitude == attitude)&&(identical(other.isOnline, isOnline) || other.isOnline == isOnline)&&(identical(other.isInvisible, isInvisible) || other.isInvisible == isInvisible)&&(identical(other.isNotDisturb, isNotDisturb) || other.isNotDisturb == isNotDisturb)&&(identical(other.isCustomized, isCustomized) || other.isCustomized == isCustomized)&&(identical(other.label, label) || other.label == label)&&const DeepCollectionEquality().equals(other._meta, _meta)&&(identical(other.clearedAt, clearedAt) || other.clearedAt == clearedAt)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)); } @JsonKey(includeFromJson: false, includeToJson: false) @override -int get hashCode => Object.hash(runtimeType,id,attitude,isOnline,isInvisible,isNotDisturb,isCustomized,label,clearedAt,accountId,createdAt,updatedAt,deletedAt); +int get hashCode => Object.hash(runtimeType,id,attitude,isOnline,isInvisible,isNotDisturb,isCustomized,label,const DeepCollectionEquality().hash(_meta),clearedAt,accountId,createdAt,updatedAt,deletedAt); @override String toString() { - return 'SnAccountStatus(id: $id, attitude: $attitude, isOnline: $isOnline, isInvisible: $isInvisible, isNotDisturb: $isNotDisturb, isCustomized: $isCustomized, label: $label, clearedAt: $clearedAt, accountId: $accountId, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; + return 'SnAccountStatus(id: $id, attitude: $attitude, isOnline: $isOnline, isInvisible: $isInvisible, isNotDisturb: $isNotDisturb, isCustomized: $isCustomized, label: $label, meta: $meta, clearedAt: $clearedAt, accountId: $accountId, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; } @@ -1301,7 +1311,7 @@ abstract mixin class _$SnAccountStatusCopyWith<$Res> implements $SnAccountStatus factory _$SnAccountStatusCopyWith(_SnAccountStatus value, $Res Function(_SnAccountStatus) _then) = __$SnAccountStatusCopyWithImpl; @override @useResult $Res call({ - String id, int attitude, bool isOnline, bool isInvisible, bool isNotDisturb, bool isCustomized, String label, DateTime? clearedAt, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt + String id, int attitude, bool isOnline, bool isInvisible, bool isNotDisturb, bool isCustomized, String label, Map? meta, DateTime? clearedAt, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt }); @@ -1318,7 +1328,7 @@ class __$SnAccountStatusCopyWithImpl<$Res> /// Create a copy of SnAccountStatus /// with the given fields replaced by the non-null parameter values. -@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? attitude = null,Object? isOnline = null,Object? isInvisible = null,Object? isNotDisturb = null,Object? isCustomized = null,Object? label = null,Object? clearedAt = freezed,Object? accountId = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) { +@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? attitude = null,Object? isOnline = null,Object? isInvisible = null,Object? isNotDisturb = null,Object? isCustomized = null,Object? label = null,Object? meta = freezed,Object? clearedAt = freezed,Object? accountId = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) { return _then(_SnAccountStatus( id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable as String,attitude: null == attitude ? _self.attitude : attitude // ignore: cast_nullable_to_non_nullable @@ -1327,7 +1337,8 @@ as bool,isInvisible: null == isInvisible ? _self.isInvisible : isInvisible // ig as bool,isNotDisturb: null == isNotDisturb ? _self.isNotDisturb : isNotDisturb // ignore: cast_nullable_to_non_nullable as bool,isCustomized: null == isCustomized ? _self.isCustomized : isCustomized // ignore: cast_nullable_to_non_nullable as bool,label: null == label ? _self.label : label // ignore: cast_nullable_to_non_nullable -as String,clearedAt: freezed == clearedAt ? _self.clearedAt : clearedAt // ignore: cast_nullable_to_non_nullable +as String,meta: freezed == meta ? _self._meta : meta // ignore: cast_nullable_to_non_nullable +as Map?,clearedAt: freezed == clearedAt ? _self.clearedAt : clearedAt // ignore: cast_nullable_to_non_nullable as DateTime?,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable as String,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable diff --git a/lib/models/account.g.dart b/lib/models/account.g.dart index 0bf4f96b..8970ff92 100644 --- a/lib/models/account.g.dart +++ b/lib/models/account.g.dart @@ -158,6 +158,7 @@ _SnAccountStatus _$SnAccountStatusFromJson(Map json) => isNotDisturb: json['is_not_disturb'] as bool, isCustomized: json['is_customized'] as bool, label: json['label'] as String? ?? "", + meta: json['meta'] as Map?, clearedAt: json['cleared_at'] == null ? null @@ -180,6 +181,7 @@ Map _$SnAccountStatusToJson(_SnAccountStatus instance) => 'is_not_disturb': instance.isNotDisturb, 'is_customized': instance.isCustomized, 'label': instance.label, + 'meta': instance.meta, 'cleared_at': instance.clearedAt?.toIso8601String(), 'account_id': instance.accountId, 'created_at': instance.createdAt.toIso8601String(), diff --git a/lib/pods/activity/activity_rpc.dart b/lib/pods/activity/activity_rpc.dart index 075d087b..3a3b2590 100644 --- a/lib/pods/activity/activity_rpc.dart +++ b/lib/pods/activity/activity_rpc.dart @@ -5,6 +5,7 @@ import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:island/pods/network.dart'; +import 'package:island/widgets/account/status.dart'; import 'package:shelf/shelf.dart'; import 'package:shelf/shelf_io.dart' as shelf_io; import 'package:shelf_web_socket/shelf_web_socket.dart'; @@ -390,9 +391,9 @@ final rpcServerStateProvider = 'message': (socket, dynamic data) async { if (data['cmd'] == 'SET_ACTIVITY') { notifier.addActivity( - 'Activity: ${data['args']['activity']['details'] ?? 'Unknown'}', + 'Activity: ${data['args']['activity']['details'] ?? ''}', ); - final label = data['args']['activity']['details'] ?? 'Unknown'; + final label = data['args']['activity']['details'] ?? ''; final appId = socket.clientId; try { await setRemoteActivityStatus( @@ -401,6 +402,7 @@ final rpcServerStateProvider = appId, data['args']['activity'], ); + ref.invalidate(accountStatusProvider('me')); } catch (e) { developer.log( 'Failed to set remote activity status: $e', @@ -420,6 +422,7 @@ final rpcServerStateProvider = final appId = socket.clientId; try { await unsetRemoteActivityStatus(ref, appId); + ref.invalidate(accountStatusProvider('me')); } catch (e) { developer.log( 'Failed to unset remote activity status: $e', diff --git a/lib/widgets/account/status.dart b/lib/widgets/account/status.dart index 4e73f5f5..71124124 100644 --- a/lib/widgets/account/status.dart +++ b/lib/widgets/account/status.dart @@ -113,6 +113,126 @@ class AccountStatusWidget extends HookConsumerWidget { final status = 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( @@ -133,10 +253,31 @@ class AccountStatusWidget extends HookConsumerWidget { ).padding(right: 4), if (status.value?.isCustomized ?? false) Flexible( - child: Text( - status.value?.label ?? 'unknown'.tr(), - maxLines: 1, - overflow: TextOverflow.ellipsis, + child: GestureDetector( + onLongPress: () { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text('Activity Details'), + content: _buildActivityDetails(status.value), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text('Close'), + ), + ], + ), + ); + }, + child: Tooltip( + richMessage: getActivityFullMessage(status.value), + child: Text( + getActivityTitle(status.value?.label, status.value?.meta) ?? + 'unknown'.tr(), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), ), ) else @@ -148,7 +289,13 @@ class AccountStatusWidget extends HookConsumerWidget { overflow: TextOverflow.ellipsis, ).tr(), ), - if (!(status.value?.isOnline ?? false) && + if (getActivitySubtitle(status.value?.meta) != null) + Flexible( + child: Text( + getActivitySubtitle(status.value?.meta)!, + ).opacity(0.75), + ) + else if (!(status.value?.isOnline ?? false) && account.value?.profile.lastSeenAt != null) Flexible( child: Text(