diff --git a/lib/screens/post/post_detail.dart b/lib/screens/post/post_detail.dart index 4ded56d..49ca251 100644 --- a/lib/screens/post/post_detail.dart +++ b/lib/screens/post/post_detail.dart @@ -47,13 +47,11 @@ class _PostDetailScreenState extends State { resp.data['body']['attachments']?.cast() ?? [], ); if (!mounted) return; - setState(() { - _data = SnPost.fromJson(resp.data).copyWith( - preload: SnPostPreload( - attachments: attachments, - ), - ); - }); + _data = SnPost.fromJson(resp.data).copyWith( + preload: SnPostPreload( + attachments: attachments, + ), + ); } catch (err) { context.showErrorDialog(err); } finally { @@ -87,13 +85,19 @@ class _PostDetailScreenState extends State { }, ), flexibleSpace: Column( + mainAxisAlignment: MainAxisAlignment.center, children: [ - Text(_data?.body['title'] ?? 'postNoun'.tr()) - .textStyle(Theme.of(context).textTheme.titleLarge!) - .textColor(Colors.white), - Text('postDetail') - .tr() - .textColor(Colors.white.withAlpha((255 * 0.9).round())), + if (_data?.body['title'] != null) + Text(_data?.body['title'] ?? 'postNoun'.tr()) + .textStyle(Theme.of(context).textTheme.titleLarge!) + .textColor(Colors.white), + if (_data?.body['title'] != null) + Text('postDetail'.tr()) + .textColor(Colors.white.withAlpha((255 * 0.9).round())) + else + Text('postDetail'.tr()) + .textStyle(Theme.of(context).textTheme.titleLarge!) + .textColor(Colors.white), ], ).padding(top: math.max(MediaQuery.of(context).padding.top, 8)), ), @@ -104,7 +108,10 @@ class _PostDetailScreenState extends State { ), if (_data != null) SliverToBoxAdapter( - child: PostItem(data: _data!, showComments: false), + child: PostItem( + data: _data!, + showComments: false, + ), ), const SliverToBoxAdapter(child: Divider(height: 1)), if (_data != null) diff --git a/lib/types/post.dart b/lib/types/post.dart index 8107f8f..b57dfce 100644 --- a/lib/types/post.dart +++ b/lib/types/post.dart @@ -20,13 +20,13 @@ class SnPost with _$SnPost { required String? aliasPrefix, required List tags, required List categories, - required dynamic replies, - required dynamic replyId, - required dynamic repostId, - required dynamic replyTo, - required dynamic repostTo, - required dynamic visibleUsersList, - required dynamic invisibleUsersList, + required List? replies, + required int? replyId, + required int? repostId, + required SnPost? replyTo, + required SnPost? repostTo, + required List? visibleUsersList, + required List? invisibleUsersList, required int visibility, required DateTime? editedAt, required DateTime? pinnedAt, diff --git a/lib/types/post.freezed.dart b/lib/types/post.freezed.dart index 2f59d58..507556e 100644 --- a/lib/types/post.freezed.dart +++ b/lib/types/post.freezed.dart @@ -31,13 +31,13 @@ mixin _$SnPost { String? get aliasPrefix => throw _privateConstructorUsedError; List get tags => throw _privateConstructorUsedError; List get categories => throw _privateConstructorUsedError; - dynamic get replies => throw _privateConstructorUsedError; - dynamic get replyId => throw _privateConstructorUsedError; - dynamic get repostId => throw _privateConstructorUsedError; - dynamic get replyTo => throw _privateConstructorUsedError; - dynamic get repostTo => throw _privateConstructorUsedError; - dynamic get visibleUsersList => throw _privateConstructorUsedError; - dynamic get invisibleUsersList => throw _privateConstructorUsedError; + List? get replies => throw _privateConstructorUsedError; + int? get replyId => throw _privateConstructorUsedError; + int? get repostId => throw _privateConstructorUsedError; + SnPost? get replyTo => throw _privateConstructorUsedError; + SnPost? get repostTo => throw _privateConstructorUsedError; + List? get visibleUsersList => throw _privateConstructorUsedError; + List? get invisibleUsersList => throw _privateConstructorUsedError; int get visibility => throw _privateConstructorUsedError; DateTime? get editedAt => throw _privateConstructorUsedError; DateTime? get pinnedAt => throw _privateConstructorUsedError; @@ -78,13 +78,13 @@ abstract class $SnPostCopyWith<$Res> { String? aliasPrefix, List tags, List categories, - dynamic replies, - dynamic replyId, - dynamic repostId, - dynamic replyTo, - dynamic repostTo, - dynamic visibleUsersList, - dynamic invisibleUsersList, + List? replies, + int? replyId, + int? repostId, + SnPost? replyTo, + SnPost? repostTo, + List? visibleUsersList, + List? invisibleUsersList, int visibility, DateTime? editedAt, DateTime? pinnedAt, @@ -99,6 +99,8 @@ abstract class $SnPostCopyWith<$Res> { SnMetric metric, SnPostPreload? preload}); + $SnPostCopyWith<$Res>? get replyTo; + $SnPostCopyWith<$Res>? get repostTo; $SnPublisherCopyWith<$Res> get publisher; $SnMetricCopyWith<$Res> get metric; $SnPostPreloadCopyWith<$Res>? get preload; @@ -199,31 +201,31 @@ class _$SnPostCopyWithImpl<$Res, $Val extends SnPost> replies: freezed == replies ? _value.replies : replies // ignore: cast_nullable_to_non_nullable - as dynamic, + as List?, replyId: freezed == replyId ? _value.replyId : replyId // ignore: cast_nullable_to_non_nullable - as dynamic, + as int?, repostId: freezed == repostId ? _value.repostId : repostId // ignore: cast_nullable_to_non_nullable - as dynamic, + as int?, replyTo: freezed == replyTo ? _value.replyTo : replyTo // ignore: cast_nullable_to_non_nullable - as dynamic, + as SnPost?, repostTo: freezed == repostTo ? _value.repostTo : repostTo // ignore: cast_nullable_to_non_nullable - as dynamic, + as SnPost?, visibleUsersList: freezed == visibleUsersList ? _value.visibleUsersList : visibleUsersList // ignore: cast_nullable_to_non_nullable - as dynamic, + as List?, invisibleUsersList: freezed == invisibleUsersList ? _value.invisibleUsersList : invisibleUsersList // ignore: cast_nullable_to_non_nullable - as dynamic, + as List?, visibility: null == visibility ? _value.visibility : visibility // ignore: cast_nullable_to_non_nullable @@ -279,6 +281,34 @@ class _$SnPostCopyWithImpl<$Res, $Val extends SnPost> ) as $Val); } + /// Create a copy of SnPost + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $SnPostCopyWith<$Res>? get replyTo { + if (_value.replyTo == null) { + return null; + } + + return $SnPostCopyWith<$Res>(_value.replyTo!, (value) { + return _then(_value.copyWith(replyTo: value) as $Val); + }); + } + + /// Create a copy of SnPost + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $SnPostCopyWith<$Res>? get repostTo { + if (_value.repostTo == null) { + return null; + } + + return $SnPostCopyWith<$Res>(_value.repostTo!, (value) { + return _then(_value.copyWith(repostTo: value) as $Val); + }); + } + /// Create a copy of SnPost /// with the given fields replaced by the non-null parameter values. @override @@ -333,13 +363,13 @@ abstract class _$$SnPostImplCopyWith<$Res> implements $SnPostCopyWith<$Res> { String? aliasPrefix, List tags, List categories, - dynamic replies, - dynamic replyId, - dynamic repostId, - dynamic replyTo, - dynamic repostTo, - dynamic visibleUsersList, - dynamic invisibleUsersList, + List? replies, + int? replyId, + int? repostId, + SnPost? replyTo, + SnPost? repostTo, + List? visibleUsersList, + List? invisibleUsersList, int visibility, DateTime? editedAt, DateTime? pinnedAt, @@ -354,6 +384,10 @@ abstract class _$$SnPostImplCopyWith<$Res> implements $SnPostCopyWith<$Res> { SnMetric metric, SnPostPreload? preload}); + @override + $SnPostCopyWith<$Res>? get replyTo; + @override + $SnPostCopyWith<$Res>? get repostTo; @override $SnPublisherCopyWith<$Res> get publisher; @override @@ -453,33 +487,33 @@ class __$$SnPostImplCopyWithImpl<$Res> : categories // ignore: cast_nullable_to_non_nullable as List, replies: freezed == replies - ? _value.replies + ? _value._replies : replies // ignore: cast_nullable_to_non_nullable - as dynamic, + as List?, replyId: freezed == replyId ? _value.replyId : replyId // ignore: cast_nullable_to_non_nullable - as dynamic, + as int?, repostId: freezed == repostId ? _value.repostId : repostId // ignore: cast_nullable_to_non_nullable - as dynamic, + as int?, replyTo: freezed == replyTo ? _value.replyTo : replyTo // ignore: cast_nullable_to_non_nullable - as dynamic, + as SnPost?, repostTo: freezed == repostTo ? _value.repostTo : repostTo // ignore: cast_nullable_to_non_nullable - as dynamic, + as SnPost?, visibleUsersList: freezed == visibleUsersList - ? _value.visibleUsersList + ? _value._visibleUsersList : visibleUsersList // ignore: cast_nullable_to_non_nullable - as dynamic, + as List?, invisibleUsersList: freezed == invisibleUsersList - ? _value.invisibleUsersList + ? _value._invisibleUsersList : invisibleUsersList // ignore: cast_nullable_to_non_nullable - as dynamic, + as List?, visibility: null == visibility ? _value.visibility : visibility // ignore: cast_nullable_to_non_nullable @@ -551,13 +585,13 @@ class _$SnPostImpl extends _SnPost { required this.aliasPrefix, required final List tags, required final List categories, - required this.replies, + required final List? replies, required this.replyId, required this.repostId, required this.replyTo, required this.repostTo, - required this.visibleUsersList, - required this.invisibleUsersList, + required final List? visibleUsersList, + required final List? invisibleUsersList, required this.visibility, required this.editedAt, required this.pinnedAt, @@ -574,6 +608,9 @@ class _$SnPostImpl extends _SnPost { : _body = body, _tags = tags, _categories = categories, + _replies = replies, + _visibleUsersList = visibleUsersList, + _invisibleUsersList = invisibleUsersList, super._(); factory _$SnPostImpl.fromJson(Map json) => @@ -619,20 +656,46 @@ class _$SnPostImpl extends _SnPost { return EqualUnmodifiableListView(_categories); } + final List? _replies; @override - final dynamic replies; + List? get replies { + final value = _replies; + if (value == null) return null; + if (_replies is EqualUnmodifiableListView) return _replies; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(value); + } + @override - final dynamic replyId; + final int? replyId; @override - final dynamic repostId; + final int? repostId; @override - final dynamic replyTo; + final SnPost? replyTo; @override - final dynamic repostTo; + final SnPost? repostTo; + final List? _visibleUsersList; @override - final dynamic visibleUsersList; + List? get visibleUsersList { + final value = _visibleUsersList; + if (value == null) return null; + if (_visibleUsersList is EqualUnmodifiableListView) + return _visibleUsersList; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(value); + } + + final List? _invisibleUsersList; @override - final dynamic invisibleUsersList; + List? get invisibleUsersList { + final value = _invisibleUsersList; + if (value == null) return null; + if (_invisibleUsersList is EqualUnmodifiableListView) + return _invisibleUsersList; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(value); + } + @override final int visibility; @override @@ -687,15 +750,17 @@ class _$SnPostImpl extends _SnPost { const DeepCollectionEquality().equals(other._tags, _tags) && const DeepCollectionEquality() .equals(other._categories, _categories) && - const DeepCollectionEquality().equals(other.replies, replies) && - const DeepCollectionEquality().equals(other.replyId, replyId) && - const DeepCollectionEquality().equals(other.repostId, repostId) && - const DeepCollectionEquality().equals(other.replyTo, replyTo) && - const DeepCollectionEquality().equals(other.repostTo, repostTo) && + const DeepCollectionEquality().equals(other._replies, _replies) && + (identical(other.replyId, replyId) || other.replyId == replyId) && + (identical(other.repostId, repostId) || + other.repostId == repostId) && + (identical(other.replyTo, replyTo) || other.replyTo == replyTo) && + (identical(other.repostTo, repostTo) || + other.repostTo == repostTo) && const DeepCollectionEquality() - .equals(other.visibleUsersList, visibleUsersList) && + .equals(other._visibleUsersList, _visibleUsersList) && const DeepCollectionEquality() - .equals(other.invisibleUsersList, invisibleUsersList) && + .equals(other._invisibleUsersList, _invisibleUsersList) && (identical(other.visibility, visibility) || other.visibility == visibility) && (identical(other.editedAt, editedAt) || @@ -736,13 +801,13 @@ class _$SnPostImpl extends _SnPost { aliasPrefix, const DeepCollectionEquality().hash(_tags), const DeepCollectionEquality().hash(_categories), - const DeepCollectionEquality().hash(replies), - const DeepCollectionEquality().hash(replyId), - const DeepCollectionEquality().hash(repostId), - const DeepCollectionEquality().hash(replyTo), - const DeepCollectionEquality().hash(repostTo), - const DeepCollectionEquality().hash(visibleUsersList), - const DeepCollectionEquality().hash(invisibleUsersList), + const DeepCollectionEquality().hash(_replies), + replyId, + repostId, + replyTo, + repostTo, + const DeepCollectionEquality().hash(_visibleUsersList), + const DeepCollectionEquality().hash(_invisibleUsersList), visibility, editedAt, pinnedAt, @@ -787,13 +852,13 @@ abstract class _SnPost extends SnPost { required final String? aliasPrefix, required final List tags, required final List categories, - required final dynamic replies, - required final dynamic replyId, - required final dynamic repostId, - required final dynamic replyTo, - required final dynamic repostTo, - required final dynamic visibleUsersList, - required final dynamic invisibleUsersList, + required final List? replies, + required final int? replyId, + required final int? repostId, + required final SnPost? replyTo, + required final SnPost? repostTo, + required final List? visibleUsersList, + required final List? invisibleUsersList, required final int visibility, required final DateTime? editedAt, required final DateTime? pinnedAt, @@ -834,19 +899,19 @@ abstract class _SnPost extends SnPost { @override List get categories; @override - dynamic get replies; + List? get replies; @override - dynamic get replyId; + int? get replyId; @override - dynamic get repostId; + int? get repostId; @override - dynamic get replyTo; + SnPost? get replyTo; @override - dynamic get repostTo; + SnPost? get repostTo; @override - dynamic get visibleUsersList; + List? get visibleUsersList; @override - dynamic get invisibleUsersList; + List? get invisibleUsersList; @override int get visibility; @override diff --git a/lib/types/post.g.dart b/lib/types/post.g.dart index 2dd8b31..c3e07ab 100644 --- a/lib/types/post.g.dart +++ b/lib/types/post.g.dart @@ -20,13 +20,23 @@ _$SnPostImpl _$$SnPostImplFromJson(Map json) => _$SnPostImpl( aliasPrefix: json['alias_prefix'] as String?, tags: json['tags'] as List, categories: json['categories'] as List, - replies: json['replies'], - replyId: json['reply_id'], - repostId: json['repost_id'], - replyTo: json['reply_to'], - repostTo: json['repost_to'], - visibleUsersList: json['visible_users_list'], - invisibleUsersList: json['invisible_users_list'], + replies: (json['replies'] as List?) + ?.map((e) => SnPost.fromJson(e as Map)) + .toList(), + replyId: (json['reply_id'] as num?)?.toInt(), + repostId: (json['repost_id'] as num?)?.toInt(), + replyTo: json['reply_to'] == null + ? null + : SnPost.fromJson(json['reply_to'] as Map), + repostTo: json['repost_to'] == null + ? null + : SnPost.fromJson(json['repost_to'] as Map), + visibleUsersList: (json['visible_users_list'] as List?) + ?.map((e) => (e as num).toInt()) + .toList(), + invisibleUsersList: (json['invisible_users_list'] as List?) + ?.map((e) => (e as num).toInt()) + .toList(), visibility: (json['visibility'] as num).toInt(), editedAt: json['edited_at'] == null ? null @@ -68,11 +78,11 @@ Map _$$SnPostImplToJson(_$SnPostImpl instance) => 'alias_prefix': instance.aliasPrefix, 'tags': instance.tags, 'categories': instance.categories, - 'replies': instance.replies, + 'replies': instance.replies?.map((e) => e.toJson()).toList(), 'reply_id': instance.replyId, 'repost_id': instance.repostId, - 'reply_to': instance.replyTo, - 'repost_to': instance.repostTo, + 'reply_to': instance.replyTo?.toJson(), + 'repost_to': instance.repostTo?.toJson(), 'visible_users_list': instance.visibleUsersList, 'invisible_users_list': instance.invisibleUsersList, 'visibility': instance.visibility, diff --git a/lib/widgets/post/post_item.dart b/lib/widgets/post/post_item.dart index b5eabf7..3455dfe 100644 --- a/lib/widgets/post/post_item.dart +++ b/lib/widgets/post/post_item.dart @@ -38,6 +38,11 @@ class PostItem extends StatelessWidget { children: [ _PostContentHeader(data: data).padding(horizontal: 12, vertical: 8), _PostContentBody(data: data.body).padding(horizontal: 16, bottom: 6), + if (data.repostTo != null) + _PostQuoteContent(child: data.repostTo!).padding( + horizontal: 8, + bottom: 4, + ), if (data.preload?.attachments?.isNotEmpty ?? true) AttachmentList( data: data.preload!.attachments!, @@ -148,7 +153,13 @@ class _PostBottomAction extends StatelessWidget { class _PostContentHeader extends StatelessWidget { final SnPost data; - const _PostContentHeader({required this.data}); + final bool isCompact; + final bool showActions; + const _PostContentHeader({ + required this.data, + this.isCompact = false, + this.showActions = true, + }); @override Widget build(BuildContext context) { @@ -157,13 +168,16 @@ class _PostContentHeader extends StatelessWidget { return Row( children: [ - AccountImage(content: data.publisher.avatar), - const Gap(12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + AccountImage( + content: data.publisher.avatar, + radius: isCompact ? 12 : 20, + ), + Gap(isCompact ? 8 : 12), + if (isCompact) + Row( children: [ Text(data.publisher.nick).bold(), + const Gap(4), Row( children: [ Text('@${data.publisher.name}').fontSize(13), @@ -174,86 +188,104 @@ class _PostContentHeader extends StatelessWidget { ], ).opacity(0.8), ], + ) + else + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(data.publisher.nick).bold(), + Row( + children: [ + Text('@${data.publisher.name}').fontSize(13), + const Gap(4), + Text(RelativeTime(context).format( + data.publishedAt ?? data.createdAt, + )).fontSize(13), + ], + ).opacity(0.8), + ], + ), ), - ), - PopupMenuButton( - icon: const Icon(Symbols.more_horiz), - style: const ButtonStyle( - visualDensity: VisualDensity(horizontal: -4, vertical: -4), - ), - itemBuilder: (BuildContext context) => [ - if (isAuthor) + if (showActions) + PopupMenuButton( + icon: const Icon(Symbols.more_horiz), + style: const ButtonStyle( + visualDensity: VisualDensity(horizontal: -4, vertical: -4), + ), + itemBuilder: (BuildContext context) => [ + if (isAuthor) + PopupMenuItem( + child: Row( + children: [ + const Icon(Symbols.edit), + const Gap(16), + Text('edit').tr(), + ], + ), + onTap: () { + GoRouter.of(context).pushNamed( + 'postEditor', + pathParameters: {'mode': data.typePlural}, + queryParameters: {'editing': data.id.toString()}, + ); + }, + ), + if (isAuthor) + PopupMenuItem( + child: Row( + children: [ + const Icon(Symbols.delete), + const Gap(16), + Text('delete').tr(), + ], + ), + ), + if (isAuthor) const PopupMenuDivider(), PopupMenuItem( child: Row( children: [ - const Icon(Symbols.edit), + const Icon(Symbols.reply), const Gap(16), - Text('edit').tr(), + Text('reply').tr(), ], ), onTap: () { GoRouter.of(context).pushNamed( 'postEditor', pathParameters: {'mode': data.typePlural}, - queryParameters: {'editing': data.id.toString()}, + queryParameters: {'replying': data.id.toString()}, ); }, ), - if (isAuthor) PopupMenuItem( child: Row( children: [ - const Icon(Symbols.delete), + const Icon(Symbols.forward), const Gap(16), - Text('delete').tr(), + Text('repost').tr(), + ], + ), + onTap: () { + GoRouter.of(context).pushNamed( + 'postEditor', + pathParameters: {'mode': data.typePlural}, + queryParameters: {'reposting': data.id.toString()}, + ); + }, + ), + const PopupMenuDivider(), + PopupMenuItem( + child: Row( + children: [ + const Icon(Symbols.flag), + const Gap(16), + Text('report').tr(), ], ), ), - if (isAuthor) const PopupMenuDivider(), - PopupMenuItem( - child: Row( - children: [ - const Icon(Symbols.reply), - const Gap(16), - Text('reply').tr(), - ], - ), - onTap: () { - GoRouter.of(context).pushNamed( - 'postEditor', - pathParameters: {'mode': data.typePlural}, - queryParameters: {'replying': data.id.toString()}, - ); - }, - ), - PopupMenuItem( - child: Row( - children: [ - const Icon(Symbols.forward), - const Gap(16), - Text('repost').tr(), - ], - ), - onTap: () { - GoRouter.of(context).pushNamed( - 'postEditor', - pathParameters: {'mode': data.typePlural}, - queryParameters: {'reposting': data.id.toString()}, - ); - }, - ), - const PopupMenuDivider(), - PopupMenuItem( - child: Row( - children: [ - const Icon(Symbols.flag), - const Gap(16), - Text('report').tr(), - ], - ), - ), - ], - ), + ], + ), ], ); } @@ -269,3 +301,29 @@ class _PostContentBody extends StatelessWidget { return MarkdownTextContent(content: data['content']); } } + +class _PostQuoteContent extends StatelessWidget { + final SnPost child; + const _PostQuoteContent({super.key, required this.child}); + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(8)), + border: Border.all( + color: Theme.of(context).dividerColor, + width: 1, + ), + ), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Column( + children: [ + _PostContentHeader(data: child, isCompact: true, showActions: false) + .padding(bottom: 4), + _PostContentBody(data: child.body), + ], + ), + ); + } +}