✨ Notification UI
This commit is contained in:
		@@ -152,7 +152,7 @@
 | 
				
			|||||||
  "status": "Status",
 | 
					  "status": "Status",
 | 
				
			||||||
  "statusActivityTitle": "{} is {} {}",
 | 
					  "statusActivityTitle": "{} is {} {}",
 | 
				
			||||||
  "statusActivityEndedTitle": "{} is {} {} until {}",
 | 
					  "statusActivityEndedTitle": "{} is {} {} until {}",
 | 
				
			||||||
  "appSettings": "App Settings",
 | 
					  "appSettings": "App settings",
 | 
				
			||||||
  "accountSettings": "Account Settings",
 | 
					  "accountSettings": "Account Settings",
 | 
				
			||||||
  "settings": "Settings",
 | 
					  "settings": "Settings",
 | 
				
			||||||
  "language": "Language",
 | 
					  "language": "Language",
 | 
				
			||||||
@@ -258,5 +258,7 @@
 | 
				
			|||||||
  "walletNotFound": "Wallet not found",
 | 
					  "walletNotFound": "Wallet not found",
 | 
				
			||||||
  "walletCreateHint": "You don't have a wallet yet. Create one to start using the Solar Network eWallet.",
 | 
					  "walletCreateHint": "You don't have a wallet yet. Create one to start using the Solar Network eWallet.",
 | 
				
			||||||
  "walletCreate": "Create a Wallet",
 | 
					  "walletCreate": "Create a Wallet",
 | 
				
			||||||
  "settingsServerUrl": "Server URL"
 | 
					  "settingsServerUrl": "Server URL",
 | 
				
			||||||
 | 
					  "settingsApplied": "The settings has been applied.",
 | 
				
			||||||
 | 
					  "notifications": "Notifications"
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -86,3 +86,24 @@ abstract class SnAccountBadge with _$SnAccountBadge {
 | 
				
			|||||||
  factory SnAccountBadge.fromJson(Map<String, dynamic> json) =>
 | 
					  factory SnAccountBadge.fromJson(Map<String, dynamic> json) =>
 | 
				
			||||||
      _$SnAccountBadgeFromJson(json);
 | 
					      _$SnAccountBadgeFromJson(json);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@freezed
 | 
				
			||||||
 | 
					abstract class SnNotification with _$SnNotification {
 | 
				
			||||||
 | 
					  const factory SnNotification({
 | 
				
			||||||
 | 
					    required DateTime createdAt,
 | 
				
			||||||
 | 
					    required DateTime updatedAt,
 | 
				
			||||||
 | 
					    required DateTime? deletedAt,
 | 
				
			||||||
 | 
					    required String id,
 | 
				
			||||||
 | 
					    required String topic,
 | 
				
			||||||
 | 
					    required String title,
 | 
				
			||||||
 | 
					    @Default('') String subtitle,
 | 
				
			||||||
 | 
					    required String content,
 | 
				
			||||||
 | 
					    @Default({}) Map<String, dynamic> meta,
 | 
				
			||||||
 | 
					    required int priority,
 | 
				
			||||||
 | 
					    required DateTime? viewedAt,
 | 
				
			||||||
 | 
					    required String accountId,
 | 
				
			||||||
 | 
					  }) = _SnNotification;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  factory SnNotification.fromJson(Map<String, dynamic> json) =>
 | 
				
			||||||
 | 
					      _$SnNotificationFromJson(json);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -749,6 +749,178 @@ as DateTime?,
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// @nodoc
 | 
				
			||||||
 | 
					mixin _$SnNotification {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; String get id; String get topic; String get title; String get subtitle; String get content; Map<String, dynamic> get meta; int get priority; DateTime? get viewedAt; String get accountId;
 | 
				
			||||||
 | 
					/// Create a copy of SnNotification
 | 
				
			||||||
 | 
					/// with the given fields replaced by the non-null parameter values.
 | 
				
			||||||
 | 
					@JsonKey(includeFromJson: false, includeToJson: false)
 | 
				
			||||||
 | 
					@pragma('vm:prefer-inline')
 | 
				
			||||||
 | 
					$SnNotificationCopyWith<SnNotification> get copyWith => _$SnNotificationCopyWithImpl<SnNotification>(this as SnNotification, _$identity);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Serializes this SnNotification to a JSON map.
 | 
				
			||||||
 | 
					  Map<String, dynamic> toJson();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@override
 | 
				
			||||||
 | 
					bool operator ==(Object other) {
 | 
				
			||||||
 | 
					  return identical(this, other) || (other.runtimeType == runtimeType&&other is SnNotification&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&(identical(other.id, id) || other.id == id)&&(identical(other.topic, topic) || other.topic == topic)&&(identical(other.title, title) || other.title == title)&&(identical(other.subtitle, subtitle) || other.subtitle == subtitle)&&(identical(other.content, content) || other.content == content)&&const DeepCollectionEquality().equals(other.meta, meta)&&(identical(other.priority, priority) || other.priority == priority)&&(identical(other.viewedAt, viewedAt) || other.viewedAt == viewedAt)&&(identical(other.accountId, accountId) || other.accountId == accountId));
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@JsonKey(includeFromJson: false, includeToJson: false)
 | 
				
			||||||
 | 
					@override
 | 
				
			||||||
 | 
					int get hashCode => Object.hash(runtimeType,createdAt,updatedAt,deletedAt,id,topic,title,subtitle,content,const DeepCollectionEquality().hash(meta),priority,viewedAt,accountId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@override
 | 
				
			||||||
 | 
					String toString() {
 | 
				
			||||||
 | 
					  return 'SnNotification(createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, id: $id, topic: $topic, title: $title, subtitle: $subtitle, content: $content, meta: $meta, priority: $priority, viewedAt: $viewedAt, accountId: $accountId)';
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// @nodoc
 | 
				
			||||||
 | 
					abstract mixin class $SnNotificationCopyWith<$Res>  {
 | 
				
			||||||
 | 
					  factory $SnNotificationCopyWith(SnNotification value, $Res Function(SnNotification) _then) = _$SnNotificationCopyWithImpl;
 | 
				
			||||||
 | 
					@useResult
 | 
				
			||||||
 | 
					$Res call({
 | 
				
			||||||
 | 
					 DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, String id, String topic, String title, String subtitle, String content, Map<String, dynamic> meta, int priority, DateTime? viewedAt, String accountId
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					/// @nodoc
 | 
				
			||||||
 | 
					class _$SnNotificationCopyWithImpl<$Res>
 | 
				
			||||||
 | 
					    implements $SnNotificationCopyWith<$Res> {
 | 
				
			||||||
 | 
					  _$SnNotificationCopyWithImpl(this._self, this._then);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  final SnNotification _self;
 | 
				
			||||||
 | 
					  final $Res Function(SnNotification) _then;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Create a copy of SnNotification
 | 
				
			||||||
 | 
					/// with the given fields replaced by the non-null parameter values.
 | 
				
			||||||
 | 
					@pragma('vm:prefer-inline') @override $Res call({Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,Object? id = null,Object? topic = null,Object? title = null,Object? subtitle = null,Object? content = null,Object? meta = null,Object? priority = null,Object? viewedAt = freezed,Object? accountId = null,}) {
 | 
				
			||||||
 | 
					  return _then(_self.copyWith(
 | 
				
			||||||
 | 
					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
 | 
				
			||||||
 | 
					as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
 | 
					as DateTime?,id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
 | 
					as String,topic: null == topic ? _self.topic : topic // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
 | 
					as String,title: null == title ? _self.title : title // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
 | 
					as String,subtitle: null == subtitle ? _self.subtitle : subtitle // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
 | 
					as String,content: null == content ? _self.content : content // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
 | 
					as String,meta: null == meta ? _self.meta : meta // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
 | 
					as Map<String, dynamic>,priority: null == priority ? _self.priority : priority // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
 | 
					as int,viewedAt: freezed == viewedAt ? _self.viewedAt : viewedAt // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
 | 
					as DateTime?,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
 | 
					as String,
 | 
				
			||||||
 | 
					  ));
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// @nodoc
 | 
				
			||||||
 | 
					@JsonSerializable()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class _SnNotification implements SnNotification {
 | 
				
			||||||
 | 
					  const _SnNotification({required this.createdAt, required this.updatedAt, required this.deletedAt, required this.id, required this.topic, required this.title, this.subtitle = '', required this.content, final  Map<String, dynamic> meta = const {}, required this.priority, required this.viewedAt, required this.accountId}): _meta = meta;
 | 
				
			||||||
 | 
					  factory _SnNotification.fromJson(Map<String, dynamic> json) => _$SnNotificationFromJson(json);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@override final  DateTime createdAt;
 | 
				
			||||||
 | 
					@override final  DateTime updatedAt;
 | 
				
			||||||
 | 
					@override final  DateTime? deletedAt;
 | 
				
			||||||
 | 
					@override final  String id;
 | 
				
			||||||
 | 
					@override final  String topic;
 | 
				
			||||||
 | 
					@override final  String title;
 | 
				
			||||||
 | 
					@override@JsonKey() final  String subtitle;
 | 
				
			||||||
 | 
					@override final  String content;
 | 
				
			||||||
 | 
					 final  Map<String, dynamic> _meta;
 | 
				
			||||||
 | 
					@override@JsonKey() Map<String, dynamic> get meta {
 | 
				
			||||||
 | 
					  if (_meta is EqualUnmodifiableMapView) return _meta;
 | 
				
			||||||
 | 
					  // ignore: implicit_dynamic_type
 | 
				
			||||||
 | 
					  return EqualUnmodifiableMapView(_meta);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@override final  int priority;
 | 
				
			||||||
 | 
					@override final  DateTime? viewedAt;
 | 
				
			||||||
 | 
					@override final  String accountId;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Create a copy of SnNotification
 | 
				
			||||||
 | 
					/// with the given fields replaced by the non-null parameter values.
 | 
				
			||||||
 | 
					@override @JsonKey(includeFromJson: false, includeToJson: false)
 | 
				
			||||||
 | 
					@pragma('vm:prefer-inline')
 | 
				
			||||||
 | 
					_$SnNotificationCopyWith<_SnNotification> get copyWith => __$SnNotificationCopyWithImpl<_SnNotification>(this, _$identity);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@override
 | 
				
			||||||
 | 
					Map<String, dynamic> toJson() {
 | 
				
			||||||
 | 
					  return _$SnNotificationToJson(this, );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@override
 | 
				
			||||||
 | 
					bool operator ==(Object other) {
 | 
				
			||||||
 | 
					  return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnNotification&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&(identical(other.id, id) || other.id == id)&&(identical(other.topic, topic) || other.topic == topic)&&(identical(other.title, title) || other.title == title)&&(identical(other.subtitle, subtitle) || other.subtitle == subtitle)&&(identical(other.content, content) || other.content == content)&&const DeepCollectionEquality().equals(other._meta, _meta)&&(identical(other.priority, priority) || other.priority == priority)&&(identical(other.viewedAt, viewedAt) || other.viewedAt == viewedAt)&&(identical(other.accountId, accountId) || other.accountId == accountId));
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@JsonKey(includeFromJson: false, includeToJson: false)
 | 
				
			||||||
 | 
					@override
 | 
				
			||||||
 | 
					int get hashCode => Object.hash(runtimeType,createdAt,updatedAt,deletedAt,id,topic,title,subtitle,content,const DeepCollectionEquality().hash(_meta),priority,viewedAt,accountId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@override
 | 
				
			||||||
 | 
					String toString() {
 | 
				
			||||||
 | 
					  return 'SnNotification(createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, id: $id, topic: $topic, title: $title, subtitle: $subtitle, content: $content, meta: $meta, priority: $priority, viewedAt: $viewedAt, accountId: $accountId)';
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// @nodoc
 | 
				
			||||||
 | 
					abstract mixin class _$SnNotificationCopyWith<$Res> implements $SnNotificationCopyWith<$Res> {
 | 
				
			||||||
 | 
					  factory _$SnNotificationCopyWith(_SnNotification value, $Res Function(_SnNotification) _then) = __$SnNotificationCopyWithImpl;
 | 
				
			||||||
 | 
					@override @useResult
 | 
				
			||||||
 | 
					$Res call({
 | 
				
			||||||
 | 
					 DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, String id, String topic, String title, String subtitle, String content, Map<String, dynamic> meta, int priority, DateTime? viewedAt, String accountId
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					/// @nodoc
 | 
				
			||||||
 | 
					class __$SnNotificationCopyWithImpl<$Res>
 | 
				
			||||||
 | 
					    implements _$SnNotificationCopyWith<$Res> {
 | 
				
			||||||
 | 
					  __$SnNotificationCopyWithImpl(this._self, this._then);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  final _SnNotification _self;
 | 
				
			||||||
 | 
					  final $Res Function(_SnNotification) _then;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Create a copy of SnNotification
 | 
				
			||||||
 | 
					/// with the given fields replaced by the non-null parameter values.
 | 
				
			||||||
 | 
					@override @pragma('vm:prefer-inline') $Res call({Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,Object? id = null,Object? topic = null,Object? title = null,Object? subtitle = null,Object? content = null,Object? meta = null,Object? priority = null,Object? viewedAt = freezed,Object? accountId = null,}) {
 | 
				
			||||||
 | 
					  return _then(_SnNotification(
 | 
				
			||||||
 | 
					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
 | 
				
			||||||
 | 
					as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
 | 
					as DateTime?,id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
 | 
					as String,topic: null == topic ? _self.topic : topic // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
 | 
					as String,title: null == title ? _self.title : title // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
 | 
					as String,subtitle: null == subtitle ? _self.subtitle : subtitle // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
 | 
					as String,content: null == content ? _self.content : content // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
 | 
					as String,meta: null == meta ? _self._meta : meta // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
 | 
					as Map<String, dynamic>,priority: null == priority ? _self.priority : priority // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
 | 
					as int,viewedAt: freezed == viewedAt ? _self.viewedAt : viewedAt // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
 | 
					as DateTime?,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
 | 
					as String,
 | 
				
			||||||
 | 
					  ));
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// dart format on
 | 
					// dart format on
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -160,3 +160,41 @@ Map<String, dynamic> _$SnAccountBadgeToJson(_SnAccountBadge instance) =>
 | 
				
			|||||||
      'updated_at': instance.updatedAt.toIso8601String(),
 | 
					      'updated_at': instance.updatedAt.toIso8601String(),
 | 
				
			||||||
      'deleted_at': instance.deletedAt?.toIso8601String(),
 | 
					      'deleted_at': instance.deletedAt?.toIso8601String(),
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					_SnNotification _$SnNotificationFromJson(Map<String, dynamic> json) =>
 | 
				
			||||||
 | 
					    _SnNotification(
 | 
				
			||||||
 | 
					      createdAt: DateTime.parse(json['created_at'] as String),
 | 
				
			||||||
 | 
					      updatedAt: DateTime.parse(json['updated_at'] as String),
 | 
				
			||||||
 | 
					      deletedAt:
 | 
				
			||||||
 | 
					          json['deleted_at'] == null
 | 
				
			||||||
 | 
					              ? null
 | 
				
			||||||
 | 
					              : DateTime.parse(json['deleted_at'] as String),
 | 
				
			||||||
 | 
					      id: json['id'] as String,
 | 
				
			||||||
 | 
					      topic: json['topic'] as String,
 | 
				
			||||||
 | 
					      title: json['title'] as String,
 | 
				
			||||||
 | 
					      subtitle: json['subtitle'] as String? ?? '',
 | 
				
			||||||
 | 
					      content: json['content'] as String,
 | 
				
			||||||
 | 
					      meta: json['meta'] as Map<String, dynamic>? ?? const {},
 | 
				
			||||||
 | 
					      priority: (json['priority'] as num).toInt(),
 | 
				
			||||||
 | 
					      viewedAt:
 | 
				
			||||||
 | 
					          json['viewed_at'] == null
 | 
				
			||||||
 | 
					              ? null
 | 
				
			||||||
 | 
					              : DateTime.parse(json['viewed_at'] as String),
 | 
				
			||||||
 | 
					      accountId: json['account_id'] as String,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Map<String, dynamic> _$SnNotificationToJson(_SnNotification instance) =>
 | 
				
			||||||
 | 
					    <String, dynamic>{
 | 
				
			||||||
 | 
					      'created_at': instance.createdAt.toIso8601String(),
 | 
				
			||||||
 | 
					      'updated_at': instance.updatedAt.toIso8601String(),
 | 
				
			||||||
 | 
					      'deleted_at': instance.deletedAt?.toIso8601String(),
 | 
				
			||||||
 | 
					      'id': instance.id,
 | 
				
			||||||
 | 
					      'topic': instance.topic,
 | 
				
			||||||
 | 
					      'title': instance.title,
 | 
				
			||||||
 | 
					      'subtitle': instance.subtitle,
 | 
				
			||||||
 | 
					      'content': instance.content,
 | 
				
			||||||
 | 
					      'meta': instance.meta,
 | 
				
			||||||
 | 
					      'priority': instance.priority,
 | 
				
			||||||
 | 
					      'viewed_at': instance.viewedAt?.toIso8601String(),
 | 
				
			||||||
 | 
					      'account_id': instance.accountId,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -38,36 +38,31 @@ final websocketProvider = Provider<WebSocketService>((ref) {
 | 
				
			|||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class WebSocketService {
 | 
					class WebSocketService {
 | 
				
			||||||
 | 
					  late Ref _ref;
 | 
				
			||||||
  WebSocketChannel? _channel;
 | 
					  WebSocketChannel? _channel;
 | 
				
			||||||
  final StreamController<WebSocketPacket> _streamController =
 | 
					  final StreamController<WebSocketPacket> _streamController =
 | 
				
			||||||
      StreamController<WebSocketPacket>.broadcast();
 | 
					      StreamController<WebSocketPacket>.broadcast();
 | 
				
			||||||
  final StreamController<WebSocketState> _statusStreamController =
 | 
					  final StreamController<WebSocketState> _statusStreamController =
 | 
				
			||||||
      StreamController<WebSocketState>.broadcast();
 | 
					      StreamController<WebSocketState>.broadcast();
 | 
				
			||||||
  String? _lastUrl;
 | 
					 | 
				
			||||||
  String? _lastAtk;
 | 
					 | 
				
			||||||
  Timer? _reconnectTimer;
 | 
					  Timer? _reconnectTimer;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Stream<WebSocketPacket> get dataStream => _streamController.stream;
 | 
					  Stream<WebSocketPacket> get dataStream => _streamController.stream;
 | 
				
			||||||
  Stream<WebSocketState> get statusStream => _statusStreamController.stream;
 | 
					  Stream<WebSocketState> get statusStream => _statusStreamController.stream;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Future<void> connect(String url, String atk, {Ref? ref}) async {
 | 
					  Future<void> connect(Ref ref) async {
 | 
				
			||||||
    _lastUrl = url;
 | 
					    _ref = ref;
 | 
				
			||||||
    _lastAtk = atk;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (ref != null) {
 | 
					    final baseUrl = ref.watch(serverUrlProvider);
 | 
				
			||||||
      final freshAtk = await getFreshAtk(
 | 
					    final atk = await getFreshAtk(
 | 
				
			||||||
        ref.watch(tokenPairProvider),
 | 
					      ref.watch(tokenPairProvider),
 | 
				
			||||||
        url.replaceFirst('ws', 'http').replaceFirst('/ws', ''),
 | 
					      baseUrl,
 | 
				
			||||||
        onRefreshed: (atk, rtk) {
 | 
					      onRefreshed: (atk, rtk) {
 | 
				
			||||||
          setTokenPair(ref.watch(sharedPreferencesProvider), atk, rtk);
 | 
					        setTokenPair(ref.watch(sharedPreferencesProvider), atk, rtk);
 | 
				
			||||||
          ref.invalidate(tokenPairProvider);
 | 
					        ref.invalidate(tokenPairProvider);
 | 
				
			||||||
        },
 | 
					      },
 | 
				
			||||||
      );
 | 
					    );
 | 
				
			||||||
      if (freshAtk != null) {
 | 
					
 | 
				
			||||||
        atk = freshAtk;
 | 
					    final url = '$baseUrl/ws'.replaceFirst('http', 'ws');
 | 
				
			||||||
        _lastAtk = freshAtk;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    log('[WebSocket] Trying connecting to $url');
 | 
					    log('[WebSocket] Trying connecting to $url');
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
@@ -113,10 +108,8 @@ class WebSocketService {
 | 
				
			|||||||
  void _scheduleReconnect() {
 | 
					  void _scheduleReconnect() {
 | 
				
			||||||
    _reconnectTimer?.cancel();
 | 
					    _reconnectTimer?.cancel();
 | 
				
			||||||
    _reconnectTimer = Timer(const Duration(milliseconds: 500), () {
 | 
					    _reconnectTimer = Timer(const Duration(milliseconds: 500), () {
 | 
				
			||||||
      if (_lastUrl != null && _lastAtk != null) {
 | 
					      _statusStreamController.sink.add(WebSocketState.connecting());
 | 
				
			||||||
        _statusStreamController.sink.add(WebSocketState.connecting());
 | 
					      connect(_ref);
 | 
				
			||||||
        connect(_lastUrl!, _lastAtk!);
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -128,8 +121,6 @@ class WebSocketService {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  void close() {
 | 
					  void close() {
 | 
				
			||||||
    _reconnectTimer?.cancel();
 | 
					    _reconnectTimer?.cancel();
 | 
				
			||||||
    _lastUrl = null;
 | 
					 | 
				
			||||||
    _lastAtk = null;
 | 
					 | 
				
			||||||
    _channel?.sink.close();
 | 
					    _channel?.sink.close();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -162,11 +153,7 @@ class WebSocketStateNotifier extends StateNotifier<WebSocketState> {
 | 
				
			|||||||
        state = const WebSocketState.error('Unauthorized');
 | 
					        state = const WebSocketState.error('Unauthorized');
 | 
				
			||||||
        return;
 | 
					        return;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      await service.connect(
 | 
					      await service.connect(ref);
 | 
				
			||||||
        '$baseUrl/ws'.replaceFirst('http', 'ws'),
 | 
					 | 
				
			||||||
        atk,
 | 
					 | 
				
			||||||
        ref: ref,
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
      state = const WebSocketState.connected();
 | 
					      state = const WebSocketState.connected();
 | 
				
			||||||
      service.statusStream.listen((event) {
 | 
					      service.statusStream.listen((event) {
 | 
				
			||||||
        state = event;
 | 
					        state = event;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -14,15 +14,15 @@ class AppRouter extends RootStackRouter {
 | 
				
			|||||||
      page: AccountShellRoute.page,
 | 
					      page: AccountShellRoute.page,
 | 
				
			||||||
      path: '/account',
 | 
					      path: '/account',
 | 
				
			||||||
      children: [
 | 
					      children: [
 | 
				
			||||||
 | 
					        AutoRoute(page: AccountRoute.page, path: ''),
 | 
				
			||||||
 | 
					        AutoRoute(page: NotificationRoute.page, path: 'notifications'),
 | 
				
			||||||
        AutoRoute(page: WalletRoute.page, path: 'wallet'),
 | 
					        AutoRoute(page: WalletRoute.page, path: 'wallet'),
 | 
				
			||||||
        AutoRoute(page: RelationshipRoute.page, path: 'relationships'),
 | 
					        AutoRoute(page: RelationshipRoute.page, path: 'relationships'),
 | 
				
			||||||
        AutoRoute(page: AccountRoute.page, path: ''),
 | 
					 | 
				
			||||||
        AutoRoute(page: AccountSettingsRoute.page, path: 'settings'),
 | 
					 | 
				
			||||||
        AutoRoute(page: AccountProfileRoute.page, path: ':name'),
 | 
					        AutoRoute(page: AccountProfileRoute.page, path: ':name'),
 | 
				
			||||||
        AutoRoute(page: PublisherProfileRoute.page, path: ':name/calendar'),
 | 
					        AutoRoute(page: PublisherProfileRoute.page, path: ':name/calendar'),
 | 
				
			||||||
        AutoRoute(page: MyselfEventCalendarRoute.page, path: 'me/calendar'),
 | 
					        AutoRoute(page: MyselfEventCalendarRoute.page, path: 'me/calendar'),
 | 
				
			||||||
        AutoRoute(page: UpdateProfileRoute.page, path: 'me/update'),
 | 
					        AutoRoute(page: UpdateProfileRoute.page, path: 'me/update'),
 | 
				
			||||||
        AutoRoute(page: ManagedPublisherRoute.page, path: 'me/publishers'),
 | 
					        AutoRoute(page: AccountSettingsRoute.page, path: 'settings'),
 | 
				
			||||||
      ],
 | 
					      ],
 | 
				
			||||||
    ),
 | 
					    ),
 | 
				
			||||||
    AutoRoute(page: RealmListRoute.page, path: '/realms'),
 | 
					    AutoRoute(page: RealmListRoute.page, path: '/realms'),
 | 
				
			||||||
@@ -37,10 +37,9 @@ class AppRouter extends RootStackRouter {
 | 
				
			|||||||
        AutoRoute(page: ChatDetailRoute.page, path: ':id/detail'),
 | 
					        AutoRoute(page: ChatDetailRoute.page, path: ':id/detail'),
 | 
				
			||||||
      ],
 | 
					      ],
 | 
				
			||||||
    ),
 | 
					    ),
 | 
				
			||||||
    AutoRoute(page: SettingsRoute.page, path: '/settings'),
 | 
					 | 
				
			||||||
    AutoRoute(page: LoginRoute.page, path: '/auth/login'),
 | 
					    AutoRoute(page: LoginRoute.page, path: '/auth/login'),
 | 
				
			||||||
    AutoRoute(page: CreateAccountRoute.page, path: '/auth/create-account'),
 | 
					    AutoRoute(page: CreateAccountRoute.page, path: '/auth/create-account'),
 | 
				
			||||||
    AutoRoute(page: AccountSettingsRoute.page, path: '/account/settings'),
 | 
					    AutoRoute(page: SettingsRoute.page, path: '/settings'),
 | 
				
			||||||
    AutoRoute(page: PostComposeRoute.page, path: '/posts/compose'),
 | 
					    AutoRoute(page: PostComposeRoute.page, path: '/posts/compose'),
 | 
				
			||||||
    AutoRoute(page: PostDetailRoute.page, path: '/posts/:id'),
 | 
					    AutoRoute(page: PostDetailRoute.page, path: '/posts/:id'),
 | 
				
			||||||
    AutoRoute(page: PostEditRoute.page, path: '/posts/:id/edit'),
 | 
					    AutoRoute(page: PostEditRoute.page, path: '/posts/:id/edit'),
 | 
				
			||||||
 
 | 
				
			|||||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -9,6 +9,7 @@ import 'package:island/pods/message.dart';
 | 
				
			|||||||
import 'package:island/pods/network.dart';
 | 
					import 'package:island/pods/network.dart';
 | 
				
			||||||
import 'package:island/pods/userinfo.dart';
 | 
					import 'package:island/pods/userinfo.dart';
 | 
				
			||||||
import 'package:island/route.gr.dart';
 | 
					import 'package:island/route.gr.dart';
 | 
				
			||||||
 | 
					import 'package:island/screens/notification.dart';
 | 
				
			||||||
import 'package:island/services/responsive.dart';
 | 
					import 'package:island/services/responsive.dart';
 | 
				
			||||||
import 'package:island/widgets/account/status.dart';
 | 
					import 'package:island/widgets/account/status.dart';
 | 
				
			||||||
import 'package:island/widgets/account/leveling_progress.dart';
 | 
					import 'package:island/widgets/account/leveling_progress.dart';
 | 
				
			||||||
@@ -52,6 +53,9 @@ class AccountScreen extends HookConsumerWidget {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    final user = ref.watch(userInfoProvider);
 | 
					    final user = ref.watch(userInfoProvider);
 | 
				
			||||||
 | 
					    final notificationUnreadCount = ref.watch(
 | 
				
			||||||
 | 
					      notificationUnreadCountNotifierProvider,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (!user.hasValue || user.value == null) {
 | 
					    if (!user.hasValue || user.value == null) {
 | 
				
			||||||
      return _UnauthorizedAccountScreen();
 | 
					      return _UnauthorizedAccountScreen();
 | 
				
			||||||
@@ -168,12 +172,20 @@ class AccountScreen extends HookConsumerWidget {
 | 
				
			|||||||
            const Gap(8),
 | 
					            const Gap(8),
 | 
				
			||||||
            ListTile(
 | 
					            ListTile(
 | 
				
			||||||
              minTileHeight: 48,
 | 
					              minTileHeight: 48,
 | 
				
			||||||
              leading: const Icon(Symbols.public),
 | 
					              leading: const Icon(Symbols.notifications),
 | 
				
			||||||
              trailing: const Icon(Symbols.chevron_right),
 | 
					              trailing: const Icon(Symbols.chevron_right),
 | 
				
			||||||
              contentPadding: EdgeInsets.symmetric(horizontal: 24),
 | 
					              contentPadding: EdgeInsets.symmetric(horizontal: 24),
 | 
				
			||||||
              title: Text('publishers').tr(),
 | 
					              title: Row(
 | 
				
			||||||
 | 
					                children: [
 | 
				
			||||||
 | 
					                  Expanded(child: Text('notifications').tr()),
 | 
				
			||||||
 | 
					                  Badge.count(
 | 
				
			||||||
 | 
					                    count: notificationUnreadCount.value ?? 0,
 | 
				
			||||||
 | 
					                    isLabelVisible: (notificationUnreadCount.value ?? 0) > 0,
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					                ],
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
              onTap: () {
 | 
					              onTap: () {
 | 
				
			||||||
                context.router.push(ManagedPublisherRoute());
 | 
					                context.router.push(NotificationRoute());
 | 
				
			||||||
              },
 | 
					              },
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
            ListTile(
 | 
					            ListTile(
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,6 +5,7 @@ import 'package:gap/gap.dart';
 | 
				
			|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
					import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
				
			||||||
import 'package:island/route.dart';
 | 
					import 'package:island/route.dart';
 | 
				
			||||||
import 'package:island/route.gr.dart';
 | 
					import 'package:island/route.gr.dart';
 | 
				
			||||||
 | 
					import 'package:island/screens/notification.dart';
 | 
				
			||||||
import 'package:island/services/responsive.dart';
 | 
					import 'package:island/services/responsive.dart';
 | 
				
			||||||
import 'package:material_symbols_icons/symbols.dart';
 | 
					import 'package:material_symbols_icons/symbols.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -17,7 +18,6 @@ class TabNavigationObserver extends AutoRouterObserver {
 | 
				
			|||||||
  @override
 | 
					  @override
 | 
				
			||||||
  void didPush(Route route, Route? previousRoute) {
 | 
					  void didPush(Route route, Route? previousRoute) {
 | 
				
			||||||
    Future(() {
 | 
					    Future(() {
 | 
				
			||||||
      print('didPush: ${route.settings.name}');
 | 
					 | 
				
			||||||
      onChange(route.settings.name);
 | 
					      onChange(route.settings.name);
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
@@ -25,7 +25,6 @@ class TabNavigationObserver extends AutoRouterObserver {
 | 
				
			|||||||
  @override
 | 
					  @override
 | 
				
			||||||
  void didPop(Route route, Route? previousRoute) {
 | 
					  void didPop(Route route, Route? previousRoute) {
 | 
				
			||||||
    Future(() {
 | 
					    Future(() {
 | 
				
			||||||
      print('didPop: ${previousRoute?.settings.name}');
 | 
					 | 
				
			||||||
      onChange(previousRoute?.settings.name);
 | 
					      onChange(previousRoute?.settings.name);
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
@@ -46,7 +45,10 @@ class TabsNavigationWidget extends HookConsumerWidget {
 | 
				
			|||||||
    final useHorizontalLayout = isWideScreen(context);
 | 
					    final useHorizontalLayout = isWideScreen(context);
 | 
				
			||||||
    final useExpandableLayout = isWidestScreen(context);
 | 
					    final useExpandableLayout = isWidestScreen(context);
 | 
				
			||||||
    final currentRoute = ref.watch(currentRouteProvider);
 | 
					    final currentRoute = ref.watch(currentRouteProvider);
 | 
				
			||||||
    print('currentRoute: $currentRoute');
 | 
					
 | 
				
			||||||
 | 
					    final notificationUnreadCount = ref.watch(
 | 
				
			||||||
 | 
					      notificationUnreadCountNotifierProvider,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    int activeIndex = 0;
 | 
					    int activeIndex = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -62,7 +64,11 @@ class TabsNavigationWidget extends HookConsumerWidget {
 | 
				
			|||||||
      ),
 | 
					      ),
 | 
				
			||||||
      NavigationDestination(
 | 
					      NavigationDestination(
 | 
				
			||||||
        label: 'account'.tr(),
 | 
					        label: 'account'.tr(),
 | 
				
			||||||
        icon: const Icon(Symbols.account_circle),
 | 
					        icon: Badge.count(
 | 
				
			||||||
 | 
					          count: notificationUnreadCount.value ?? 0,
 | 
				
			||||||
 | 
					          isLabelVisible: (notificationUnreadCount.value ?? 0) > 0,
 | 
				
			||||||
 | 
					          child: const Icon(Symbols.account_circle),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
    ];
 | 
					    ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										194
									
								
								lib/screens/notification.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										194
									
								
								lib/screens/notification.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,194 @@
 | 
				
			|||||||
 | 
					import 'dart:math' as math;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import 'package:auto_route/auto_route.dart';
 | 
				
			||||||
 | 
					import 'package:easy_localization/easy_localization.dart';
 | 
				
			||||||
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
 | 
					import 'package:flutter/services.dart';
 | 
				
			||||||
 | 
					import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
				
			||||||
 | 
					import 'package:island/models/user.dart';
 | 
				
			||||||
 | 
					import 'package:island/pods/network.dart';
 | 
				
			||||||
 | 
					import 'package:island/widgets/alert.dart';
 | 
				
			||||||
 | 
					import 'package:island/widgets/content/markdown.dart';
 | 
				
			||||||
 | 
					import 'package:relative_time/relative_time.dart';
 | 
				
			||||||
 | 
					import 'package:riverpod_annotation/riverpod_annotation.dart';
 | 
				
			||||||
 | 
					import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
 | 
				
			||||||
 | 
					import 'package:styled_widget/styled_widget.dart';
 | 
				
			||||||
 | 
					import 'package:url_launcher/url_launcher.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					part 'notification.g.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@riverpod
 | 
				
			||||||
 | 
					class NotificationUnreadCountNotifier
 | 
				
			||||||
 | 
					    extends _$NotificationUnreadCountNotifier {
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  Future<int> build() async {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      final client = ref.read(apiClientProvider);
 | 
				
			||||||
 | 
					      final response = await client.get('/notifications/count');
 | 
				
			||||||
 | 
					      return (response.data as num).toInt();
 | 
				
			||||||
 | 
					    } catch (_) {
 | 
				
			||||||
 | 
					      return 0;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Future<void> decrement(int count) async {
 | 
				
			||||||
 | 
					    final current = await future;
 | 
				
			||||||
 | 
					    state = AsyncData(math.min(current - count, 0));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@riverpod
 | 
				
			||||||
 | 
					class NotificationListNotifier extends _$NotificationListNotifier
 | 
				
			||||||
 | 
					    with CursorPagingNotifierMixin<SnNotification> {
 | 
				
			||||||
 | 
					  static const int _pageSize = 5;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  Future<CursorPagingData<SnNotification>> build() => fetch(cursor: null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  Future<CursorPagingData<SnNotification>> fetch({
 | 
				
			||||||
 | 
					    required String? cursor,
 | 
				
			||||||
 | 
					  }) async {
 | 
				
			||||||
 | 
					    final client = ref.read(apiClientProvider);
 | 
				
			||||||
 | 
					    final offset = cursor == null ? 0 : int.parse(cursor);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final queryParams = {'offset': offset, 'take': _pageSize};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final response = await client.get(
 | 
				
			||||||
 | 
					      '/notifications',
 | 
				
			||||||
 | 
					      queryParameters: queryParams,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    final total = int.parse(response.headers.value('X-Total') ?? '0');
 | 
				
			||||||
 | 
					    final List<dynamic> data = response.data;
 | 
				
			||||||
 | 
					    final notifications =
 | 
				
			||||||
 | 
					        data.map((json) => SnNotification.fromJson(json)).toList();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final hasMore = offset + notifications.length < total;
 | 
				
			||||||
 | 
					    final nextCursor =
 | 
				
			||||||
 | 
					        hasMore ? (offset + notifications.length).toString() : null;
 | 
				
			||||||
 | 
					    final unreadCount = notifications.where((n) => n.viewedAt == null).length;
 | 
				
			||||||
 | 
					    ref
 | 
				
			||||||
 | 
					        .read(notificationUnreadCountNotifierProvider.notifier)
 | 
				
			||||||
 | 
					        .decrement(unreadCount);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return CursorPagingData(
 | 
				
			||||||
 | 
					      items: notifications,
 | 
				
			||||||
 | 
					      hasMore: hasMore,
 | 
				
			||||||
 | 
					      nextCursor: nextCursor,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@RoutePage()
 | 
				
			||||||
 | 
					class NotificationScreen extends HookConsumerWidget {
 | 
				
			||||||
 | 
					  const NotificationScreen({super.key});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  Widget build(BuildContext context, WidgetRef ref) {
 | 
				
			||||||
 | 
					    return Scaffold(
 | 
				
			||||||
 | 
					      appBar: AppBar(title: const Text('notifications').tr()),
 | 
				
			||||||
 | 
					      body: PagingHelperView(
 | 
				
			||||||
 | 
					        provider: notificationListNotifierProvider,
 | 
				
			||||||
 | 
					        futureRefreshable: notificationListNotifierProvider.future,
 | 
				
			||||||
 | 
					        notifierRefreshable: notificationListNotifierProvider.notifier,
 | 
				
			||||||
 | 
					        contentBuilder:
 | 
				
			||||||
 | 
					            (data, widgetCount, endItemView) => ListView.builder(
 | 
				
			||||||
 | 
					              itemCount: widgetCount,
 | 
				
			||||||
 | 
					              itemBuilder: (context, index) {
 | 
				
			||||||
 | 
					                if (index == widgetCount - 1) {
 | 
				
			||||||
 | 
					                  return endItemView;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                final notification = data.items[index];
 | 
				
			||||||
 | 
					                return ListTile(
 | 
				
			||||||
 | 
					                  isThreeLine: true,
 | 
				
			||||||
 | 
					                  contentPadding: EdgeInsets.symmetric(
 | 
				
			||||||
 | 
					                    horizontal: 16,
 | 
				
			||||||
 | 
					                    vertical: 8,
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					                  title: Text(notification.title),
 | 
				
			||||||
 | 
					                  subtitle: Column(
 | 
				
			||||||
 | 
					                    crossAxisAlignment: CrossAxisAlignment.stretch,
 | 
				
			||||||
 | 
					                    children: [
 | 
				
			||||||
 | 
					                      if (notification.subtitle.isNotEmpty)
 | 
				
			||||||
 | 
					                        Text(notification.subtitle).bold(),
 | 
				
			||||||
 | 
					                      Row(
 | 
				
			||||||
 | 
					                        spacing: 6,
 | 
				
			||||||
 | 
					                        children: [
 | 
				
			||||||
 | 
					                          Text(
 | 
				
			||||||
 | 
					                            DateFormat().format(
 | 
				
			||||||
 | 
					                              notification.createdAt.toLocal(),
 | 
				
			||||||
 | 
					                            ),
 | 
				
			||||||
 | 
					                          ).fontSize(11),
 | 
				
			||||||
 | 
					                          Text('·').fontSize(11).bold(),
 | 
				
			||||||
 | 
					                          Text(
 | 
				
			||||||
 | 
					                            RelativeTime(
 | 
				
			||||||
 | 
					                              context,
 | 
				
			||||||
 | 
					                            ).format(notification.createdAt.toLocal()),
 | 
				
			||||||
 | 
					                          ).fontSize(11),
 | 
				
			||||||
 | 
					                        ],
 | 
				
			||||||
 | 
					                      ).opacity(0.75).padding(bottom: 4),
 | 
				
			||||||
 | 
					                      MarkdownTextContent(
 | 
				
			||||||
 | 
					                        content: notification.content,
 | 
				
			||||||
 | 
					                        textStyle: Theme.of(
 | 
				
			||||||
 | 
					                          context,
 | 
				
			||||||
 | 
					                        ).textTheme.bodyMedium?.copyWith(
 | 
				
			||||||
 | 
					                          color: Theme.of(
 | 
				
			||||||
 | 
					                            context,
 | 
				
			||||||
 | 
					                          ).colorScheme.onSurface.withOpacity(0.8),
 | 
				
			||||||
 | 
					                        ),
 | 
				
			||||||
 | 
					                      ),
 | 
				
			||||||
 | 
					                    ],
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					                  trailing:
 | 
				
			||||||
 | 
					                      notification.viewedAt == null
 | 
				
			||||||
 | 
					                          ? null
 | 
				
			||||||
 | 
					                          : Container(
 | 
				
			||||||
 | 
					                            width: 12,
 | 
				
			||||||
 | 
					                            height: 12,
 | 
				
			||||||
 | 
					                            decoration: const BoxDecoration(
 | 
				
			||||||
 | 
					                              color: Colors.blue,
 | 
				
			||||||
 | 
					                              shape: BoxShape.circle,
 | 
				
			||||||
 | 
					                            ),
 | 
				
			||||||
 | 
					                          ),
 | 
				
			||||||
 | 
					                  onTap: () {
 | 
				
			||||||
 | 
					                    if (notification.meta['link'] is String) {
 | 
				
			||||||
 | 
					                      final href = notification.meta['link'];
 | 
				
			||||||
 | 
					                      final uri = Uri.tryParse(href);
 | 
				
			||||||
 | 
					                      if (uri == null) {
 | 
				
			||||||
 | 
					                        showSnackBar(
 | 
				
			||||||
 | 
					                          context,
 | 
				
			||||||
 | 
					                          'brokenLink'.tr(args: []),
 | 
				
			||||||
 | 
					                          action: SnackBarAction(
 | 
				
			||||||
 | 
					                            label: 'copyToClipboard'.tr(),
 | 
				
			||||||
 | 
					                            onPressed: () {
 | 
				
			||||||
 | 
					                              Clipboard.setData(ClipboardData(text: href));
 | 
				
			||||||
 | 
					                              clearSnackBar(context);
 | 
				
			||||||
 | 
					                            },
 | 
				
			||||||
 | 
					                          ),
 | 
				
			||||||
 | 
					                        );
 | 
				
			||||||
 | 
					                        return;
 | 
				
			||||||
 | 
					                      }
 | 
				
			||||||
 | 
					                      if (uri.scheme == 'solian') {
 | 
				
			||||||
 | 
					                        context.router.pushPath(
 | 
				
			||||||
 | 
					                          ['', uri.host, ...uri.pathSegments].join('/'),
 | 
				
			||||||
 | 
					                        );
 | 
				
			||||||
 | 
					                        return;
 | 
				
			||||||
 | 
					                      }
 | 
				
			||||||
 | 
					                      showConfirmAlert(
 | 
				
			||||||
 | 
					                        'openLinkConfirmDescription'.tr(args: [href]),
 | 
				
			||||||
 | 
					                        'openLinkConfirm'.tr(),
 | 
				
			||||||
 | 
					                      ).then((value) {
 | 
				
			||||||
 | 
					                        if (value) {
 | 
				
			||||||
 | 
					                          launchUrl(uri, mode: LaunchMode.externalApplication);
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                      });
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                  },
 | 
				
			||||||
 | 
					                );
 | 
				
			||||||
 | 
					              },
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										52
									
								
								lib/screens/notification.g.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								lib/screens/notification.g.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,52 @@
 | 
				
			|||||||
 | 
					// GENERATED CODE - DO NOT MODIFY BY HAND
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					part of 'notification.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// **************************************************************************
 | 
				
			||||||
 | 
					// RiverpodGenerator
 | 
				
			||||||
 | 
					// **************************************************************************
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					String _$notificationUnreadCountNotifierHash() =>
 | 
				
			||||||
 | 
					    r'ddec25e8e693b8feb800c085ef87d65f0d172341';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// See also [NotificationUnreadCountNotifier].
 | 
				
			||||||
 | 
					@ProviderFor(NotificationUnreadCountNotifier)
 | 
				
			||||||
 | 
					final notificationUnreadCountNotifierProvider =
 | 
				
			||||||
 | 
					    AutoDisposeAsyncNotifierProvider<
 | 
				
			||||||
 | 
					      NotificationUnreadCountNotifier,
 | 
				
			||||||
 | 
					      int
 | 
				
			||||||
 | 
					    >.internal(
 | 
				
			||||||
 | 
					      NotificationUnreadCountNotifier.new,
 | 
				
			||||||
 | 
					      name: r'notificationUnreadCountNotifierProvider',
 | 
				
			||||||
 | 
					      debugGetCreateSourceHash:
 | 
				
			||||||
 | 
					          const bool.fromEnvironment('dart.vm.product')
 | 
				
			||||||
 | 
					              ? null
 | 
				
			||||||
 | 
					              : _$notificationUnreadCountNotifierHash,
 | 
				
			||||||
 | 
					      dependencies: null,
 | 
				
			||||||
 | 
					      allTransitiveDependencies: null,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					typedef _$NotificationUnreadCountNotifier = AutoDisposeAsyncNotifier<int>;
 | 
				
			||||||
 | 
					String _$notificationListNotifierHash() =>
 | 
				
			||||||
 | 
					    r'934a47bc2ce9e75699a4f53e2169470fd0c04a53';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// See also [NotificationListNotifier].
 | 
				
			||||||
 | 
					@ProviderFor(NotificationListNotifier)
 | 
				
			||||||
 | 
					final notificationListNotifierProvider = AutoDisposeAsyncNotifierProvider<
 | 
				
			||||||
 | 
					  NotificationListNotifier,
 | 
				
			||||||
 | 
					  CursorPagingData<SnNotification>
 | 
				
			||||||
 | 
					>.internal(
 | 
				
			||||||
 | 
					  NotificationListNotifier.new,
 | 
				
			||||||
 | 
					  name: r'notificationListNotifierProvider',
 | 
				
			||||||
 | 
					  debugGetCreateSourceHash:
 | 
				
			||||||
 | 
					      const bool.fromEnvironment('dart.vm.product')
 | 
				
			||||||
 | 
					          ? null
 | 
				
			||||||
 | 
					          : _$notificationListNotifierHash,
 | 
				
			||||||
 | 
					  dependencies: null,
 | 
				
			||||||
 | 
					  allTransitiveDependencies: null,
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					typedef _$NotificationListNotifier =
 | 
				
			||||||
 | 
					    AutoDisposeAsyncNotifier<CursorPagingData<SnNotification>>;
 | 
				
			||||||
 | 
					// ignore_for_file: type=lint
 | 
				
			||||||
 | 
					// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
 | 
				
			||||||
@@ -4,6 +4,7 @@ import 'package:dropdown_button2/dropdown_button2.dart';
 | 
				
			|||||||
import 'package:easy_localization/easy_localization.dart';
 | 
					import 'package:easy_localization/easy_localization.dart';
 | 
				
			||||||
import 'package:flutter/material.dart';
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
					import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
				
			||||||
 | 
					import 'package:island/widgets/alert.dart';
 | 
				
			||||||
import 'package:island/widgets/app_scaffold.dart';
 | 
					import 'package:island/widgets/app_scaffold.dart';
 | 
				
			||||||
import 'package:material_symbols_icons/symbols.dart';
 | 
					import 'package:material_symbols_icons/symbols.dart';
 | 
				
			||||||
import 'package:styled_widget/styled_widget.dart';
 | 
					import 'package:styled_widget/styled_widget.dart';
 | 
				
			||||||
@@ -87,6 +88,7 @@ class SettingsScreen extends HookConsumerWidget {
 | 
				
			|||||||
                          kNetworkServerDefault,
 | 
					                          kNetworkServerDefault,
 | 
				
			||||||
                        );
 | 
					                        );
 | 
				
			||||||
                        ref.invalidate(serverUrlProvider);
 | 
					                        ref.invalidate(serverUrlProvider);
 | 
				
			||||||
 | 
					                        showSnackBar(context, 'settingsApplied'.tr());
 | 
				
			||||||
                      },
 | 
					                      },
 | 
				
			||||||
                    ),
 | 
					                    ),
 | 
				
			||||||
                    border: OutlineInputBorder(
 | 
					                    border: OutlineInputBorder(
 | 
				
			||||||
@@ -98,6 +100,7 @@ class SettingsScreen extends HookConsumerWidget {
 | 
				
			|||||||
                    if (value.isNotEmpty) {
 | 
					                    if (value.isNotEmpty) {
 | 
				
			||||||
                      prefs.setString(kNetworkServerStoreKey, value);
 | 
					                      prefs.setString(kNetworkServerStoreKey, value);
 | 
				
			||||||
                      ref.invalidate(serverUrlProvider);
 | 
					                      ref.invalidate(serverUrlProvider);
 | 
				
			||||||
 | 
					                      showSnackBar(context, 'settingsApplied'.tr());
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                  },
 | 
					                  },
 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -71,8 +71,8 @@ class MarkdownTextContent extends HookConsumerWidget {
 | 
				
			|||||||
            style:
 | 
					            style:
 | 
				
			||||||
                linkStyle ??
 | 
					                linkStyle ??
 | 
				
			||||||
                TextStyle(color: Theme.of(context).colorScheme.primary),
 | 
					                TextStyle(color: Theme.of(context).colorScheme.primary),
 | 
				
			||||||
            onTap: (herf) {
 | 
					            onTap: (href) {
 | 
				
			||||||
              final url = Uri.tryParse(herf);
 | 
					              final url = Uri.tryParse(href);
 | 
				
			||||||
              if (url != null) {
 | 
					              if (url != null) {
 | 
				
			||||||
                if (url.scheme == 'solian') {
 | 
					                if (url.scheme == 'solian') {
 | 
				
			||||||
                  context.router.pushPath(
 | 
					                  context.router.pushPath(
 | 
				
			||||||
@@ -96,11 +96,11 @@ class MarkdownTextContent extends HookConsumerWidget {
 | 
				
			|||||||
              } else {
 | 
					              } else {
 | 
				
			||||||
                showSnackBar(
 | 
					                showSnackBar(
 | 
				
			||||||
                  context,
 | 
					                  context,
 | 
				
			||||||
                  'brokenLink'.tr(args: [herf]),
 | 
					                  'brokenLink'.tr(args: [href]),
 | 
				
			||||||
                  action: SnackBarAction(
 | 
					                  action: SnackBarAction(
 | 
				
			||||||
                    label: 'copyToClipboard'.tr(),
 | 
					                    label: 'copyToClipboard'.tr(),
 | 
				
			||||||
                    onPressed: () {
 | 
					                    onPressed: () {
 | 
				
			||||||
                      Clipboard.setData(ClipboardData(text: herf));
 | 
					                      Clipboard.setData(ClipboardData(text: href));
 | 
				
			||||||
                      clearSnackBar(context);
 | 
					                      clearSnackBar(context);
 | 
				
			||||||
                    },
 | 
					                    },
 | 
				
			||||||
                  ),
 | 
					                  ),
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user