Quoted (repost) post

This commit is contained in:
LittleSheep 2024-11-13 22:05:40 +08:00
parent 4884d04a51
commit af044a86bc
5 changed files with 312 additions and 172 deletions

View File

@ -47,13 +47,11 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
resp.data['body']['attachments']?.cast<String>() ?? [],
);
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<PostDetailScreen> {
},
),
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<PostDetailScreen> {
),
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)

View File

@ -20,13 +20,13 @@ class SnPost with _$SnPost {
required String? aliasPrefix,
required List<dynamic> tags,
required List<dynamic> categories,
required dynamic replies,
required dynamic replyId,
required dynamic repostId,
required dynamic replyTo,
required dynamic repostTo,
required dynamic visibleUsersList,
required dynamic invisibleUsersList,
required List<SnPost>? replies,
required int? replyId,
required int? repostId,
required SnPost? replyTo,
required SnPost? repostTo,
required List<int>? visibleUsersList,
required List<int>? invisibleUsersList,
required int visibility,
required DateTime? editedAt,
required DateTime? pinnedAt,

View File

@ -31,13 +31,13 @@ mixin _$SnPost {
String? get aliasPrefix => throw _privateConstructorUsedError;
List<dynamic> get tags => throw _privateConstructorUsedError;
List<dynamic> 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<SnPost>? 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<int>? get visibleUsersList => throw _privateConstructorUsedError;
List<int>? 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<dynamic> tags,
List<dynamic> categories,
dynamic replies,
dynamic replyId,
dynamic repostId,
dynamic replyTo,
dynamic repostTo,
dynamic visibleUsersList,
dynamic invisibleUsersList,
List<SnPost>? replies,
int? replyId,
int? repostId,
SnPost? replyTo,
SnPost? repostTo,
List<int>? visibleUsersList,
List<int>? 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<SnPost>?,
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<int>?,
invisibleUsersList: freezed == invisibleUsersList
? _value.invisibleUsersList
: invisibleUsersList // ignore: cast_nullable_to_non_nullable
as dynamic,
as List<int>?,
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<dynamic> tags,
List<dynamic> categories,
dynamic replies,
dynamic replyId,
dynamic repostId,
dynamic replyTo,
dynamic repostTo,
dynamic visibleUsersList,
dynamic invisibleUsersList,
List<SnPost>? replies,
int? replyId,
int? repostId,
SnPost? replyTo,
SnPost? repostTo,
List<int>? visibleUsersList,
List<int>? 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<dynamic>,
replies: freezed == replies
? _value.replies
? _value._replies
: replies // ignore: cast_nullable_to_non_nullable
as dynamic,
as List<SnPost>?,
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<int>?,
invisibleUsersList: freezed == invisibleUsersList
? _value.invisibleUsersList
? _value._invisibleUsersList
: invisibleUsersList // ignore: cast_nullable_to_non_nullable
as dynamic,
as List<int>?,
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<dynamic> tags,
required final List<dynamic> categories,
required this.replies,
required final List<SnPost>? replies,
required this.replyId,
required this.repostId,
required this.replyTo,
required this.repostTo,
required this.visibleUsersList,
required this.invisibleUsersList,
required final List<int>? visibleUsersList,
required final List<int>? 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<String, dynamic> json) =>
@ -619,20 +656,46 @@ class _$SnPostImpl extends _SnPost {
return EqualUnmodifiableListView(_categories);
}
final List<SnPost>? _replies;
@override
final dynamic replies;
List<SnPost>? 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<int>? _visibleUsersList;
@override
final dynamic visibleUsersList;
List<int>? 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<int>? _invisibleUsersList;
@override
final dynamic invisibleUsersList;
List<int>? 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<dynamic> tags,
required final List<dynamic> 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<SnPost>? replies,
required final int? replyId,
required final int? repostId,
required final SnPost? replyTo,
required final SnPost? repostTo,
required final List<int>? visibleUsersList,
required final List<int>? invisibleUsersList,
required final int visibility,
required final DateTime? editedAt,
required final DateTime? pinnedAt,
@ -834,19 +899,19 @@ abstract class _SnPost extends SnPost {
@override
List<dynamic> get categories;
@override
dynamic get replies;
List<SnPost>? 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<int>? get visibleUsersList;
@override
dynamic get invisibleUsersList;
List<int>? get invisibleUsersList;
@override
int get visibility;
@override

View File

@ -20,13 +20,23 @@ _$SnPostImpl _$$SnPostImplFromJson(Map<String, dynamic> json) => _$SnPostImpl(
aliasPrefix: json['alias_prefix'] as String?,
tags: json['tags'] as List<dynamic>,
categories: json['categories'] as List<dynamic>,
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<dynamic>?)
?.map((e) => SnPost.fromJson(e as Map<String, dynamic>))
.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<String, dynamic>),
repostTo: json['repost_to'] == null
? null
: SnPost.fromJson(json['repost_to'] as Map<String, dynamic>),
visibleUsersList: (json['visible_users_list'] as List<dynamic>?)
?.map((e) => (e as num).toInt())
.toList(),
invisibleUsersList: (json['invisible_users_list'] as List<dynamic>?)
?.map((e) => (e as num).toInt())
.toList(),
visibility: (json['visibility'] as num).toInt(),
editedAt: json['edited_at'] == null
? null
@ -68,11 +78,11 @@ Map<String, dynamic> _$$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,

View File

@ -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) => <PopupMenuEntry>[
if (isAuthor)
if (showActions)
PopupMenuButton(
icon: const Icon(Symbols.more_horiz),
style: const ButtonStyle(
visualDensity: VisualDensity(horizontal: -4, vertical: -4),
),
itemBuilder: (BuildContext context) => <PopupMenuEntry>[
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),
],
),
);
}
}