From 972b304969d6012ea2c86b000e42f2a3cc1634e1 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Tue, 18 Feb 2025 00:38:32 +0800 Subject: [PATCH] :sparkles: Flag posts --- assets/translations/en-US.json | 11 +++++- assets/translations/zh-CN.json | 11 +++++- lib/screens/notification.dart | 62 +++++++++++++++++---------------- lib/types/post.dart | 2 ++ lib/types/post.freezed.dart | 48 ++++++++++++++++++++++++- lib/types/post.g.dart | 5 +++ lib/widgets/post/post_item.dart | 41 +++++++++++++++++++++- 7 files changed, 146 insertions(+), 34 deletions(-) diff --git a/assets/translations/en-US.json b/assets/translations/en-US.json index ff6ca97..7ffd480 100644 --- a/assets/translations/en-US.json +++ b/assets/translations/en-US.json @@ -655,5 +655,14 @@ "checkInResultTier2": "Worse", "checkInResultTier3": "Normal", "checkInResultTier4": "Better", - "checkInResultTier5": "Best" + "checkInResultTier5": "Best", + "flagPostAction": "Flag the Post", + "flagPost": "Flag objectionable content", + "flagPostDescription": "If flagged users takes 50% or more of the views, the post will be collapsed. You cannot revoke the action.", + "flaggedPost": "Post has been flagged.", + "postViews": { + "zero": "No views", + "one": "{} view", + "other": "{} views" + } } diff --git a/assets/translations/zh-CN.json b/assets/translations/zh-CN.json index 46aa86e..ce70973 100644 --- a/assets/translations/zh-CN.json +++ b/assets/translations/zh-CN.json @@ -654,5 +654,14 @@ "checkInResultTier2": "凶", "checkInResultTier3": "中平", "checkInResultTier4": "吉", - "checkInResultTier5": "大吉" + "checkInResultTier5": "大吉", + "flagPostAction": "吹哨", + "flagPost": "吹哨不良内容", + "flagPostDescription": "吹哨不良内容,如果吹哨用户占浏览量的 50% 或以上,则帖子会被折叠。吹哨后不可撤销。", + "flaggedPost": "哨子已经吹响。", + "postViews": { + "zero": "{} 次浏览", + "one": "{} 次浏览", + "other": "{} 次浏览" + } } diff --git a/lib/screens/notification.dart b/lib/screens/notification.dart index 5536668..b856bc1 100644 --- a/lib/screens/notification.dart +++ b/lib/screens/notification.dart @@ -3,6 +3,7 @@ import 'dart:math' as math; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:gap/gap.dart'; +import 'package:go_router/go_router.dart'; import 'package:material_symbols_icons/symbols.dart'; import 'package:provider/provider.dart'; import 'package:relative_time/relative_time.dart'; @@ -59,10 +60,7 @@ class _NotificationScreenState extends State { final resp = await sn.client.get('/cgi/id/notifications?take=10'); _totalCount = resp.data['count']; _notifications.addAll( - resp.data['data'] - ?.map((e) => SnNotification.fromJson(e)) - .cast() ?? - [], + resp.data['data']?.map((e) => SnNotification.fromJson(e)).cast() ?? [], ); nty.updateTray(); } catch (err) { @@ -188,8 +186,7 @@ class _NotificationScreenState extends State { _fetchNotifications(); }, isLoading: _isBusy, - hasReachedMax: _totalCount != null && - _notifications.length >= _totalCount!, + hasReachedMax: _totalCount != null && _notifications.length >= _totalCount!, itemBuilder: (context, idx) { final nty = _notifications[idx]; return Row( @@ -221,29 +218,36 @@ class _NotificationScreenState extends State { isAutoWarp: true, ), ), - if ([ - 'interactive.feedback', - 'interactive.subscription' - ].contains(nty.topic) && + if (['interactive.reply', 'interactive.feedback', 'interactive.subscription'] + .contains(nty.topic) && nty.metadata['related_post'] != null) - StyledWidget(Container( - decoration: BoxDecoration( - borderRadius: const BorderRadius.all( - Radius.circular(8)), - border: Border.all( - color: Theme.of(context).dividerColor, - width: 1, + GestureDetector( + child: Container( + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(8)), + border: Border.all( + color: Theme.of(context).dividerColor, + width: 1, + ), + ), + child: PostItem( + data: SnPost.fromJson( + nty.metadata['related_post']!, + ), + showComments: false, + showReactions: false, + showMenu: false, ), ), - child: PostItem( - data: SnPost.fromJson( - nty.metadata['related_post']!, - ), - showComments: false, - showReactions: false, - showMenu: false, - ), - )).padding(top: 8), + onTap: () { + GoRouter.of(context).pushNamed( + 'postDetail', + pathParameters: { + 'slug': nty.metadata['related_post']!['id'].toString(), + }, + ); + }, + ).padding(top: 8), const Gap(8), Row( children: [ @@ -268,10 +272,8 @@ class _NotificationScreenState extends State { IconButton( icon: const Icon(Symbols.check), padding: EdgeInsets.all(0), - visualDensity: - const VisualDensity(horizontal: -4, vertical: -4), - onPressed: - _isSubmitting ? null : () => _markOneAsRead(nty), + visualDensity: const VisualDensity(horizontal: -4, vertical: -4), + onPressed: _isSubmitting ? null : () => _markOneAsRead(nty), ), ], ).padding(horizontal: 16); diff --git a/lib/types/post.dart b/lib/types/post.dart index aeab920..542ef03 100644 --- a/lib/types/post.dart +++ b/lib/types/post.dart @@ -37,6 +37,8 @@ class SnPost with _$SnPost { required DateTime? publishedUntil, required int totalUpvote, required int totalDownvote, + @Default(0) int totalViews, + @Default(0) int totalAggregatedViews, required int publisherId, required int? pollId, required SnPublisher publisher, diff --git a/lib/types/post.freezed.dart b/lib/types/post.freezed.dart index b16bc44..9666b25 100644 --- a/lib/types/post.freezed.dart +++ b/lib/types/post.freezed.dart @@ -47,6 +47,8 @@ mixin _$SnPost { DateTime? get publishedUntil => throw _privateConstructorUsedError; int get totalUpvote => throw _privateConstructorUsedError; int get totalDownvote => throw _privateConstructorUsedError; + int get totalViews => throw _privateConstructorUsedError; + int get totalAggregatedViews => throw _privateConstructorUsedError; int get publisherId => throw _privateConstructorUsedError; int? get pollId => throw _privateConstructorUsedError; SnPublisher get publisher => throw _privateConstructorUsedError; @@ -95,6 +97,8 @@ abstract class $SnPostCopyWith<$Res> { DateTime? publishedUntil, int totalUpvote, int totalDownvote, + int totalViews, + int totalAggregatedViews, int publisherId, int? pollId, SnPublisher publisher, @@ -150,6 +154,8 @@ class _$SnPostCopyWithImpl<$Res, $Val extends SnPost> Object? publishedUntil = freezed, Object? totalUpvote = null, Object? totalDownvote = null, + Object? totalViews = null, + Object? totalAggregatedViews = null, Object? publisherId = null, Object? pollId = freezed, Object? publisher = null, @@ -265,6 +271,14 @@ class _$SnPostCopyWithImpl<$Res, $Val extends SnPost> ? _value.totalDownvote : totalDownvote // ignore: cast_nullable_to_non_nullable as int, + totalViews: null == totalViews + ? _value.totalViews + : totalViews // ignore: cast_nullable_to_non_nullable + as int, + totalAggregatedViews: null == totalAggregatedViews + ? _value.totalAggregatedViews + : totalAggregatedViews // ignore: cast_nullable_to_non_nullable + as int, publisherId: null == publisherId ? _value.publisherId : publisherId // ignore: cast_nullable_to_non_nullable @@ -386,6 +400,8 @@ abstract class _$$SnPostImplCopyWith<$Res> implements $SnPostCopyWith<$Res> { DateTime? publishedUntil, int totalUpvote, int totalDownvote, + int totalViews, + int totalAggregatedViews, int publisherId, int? pollId, SnPublisher publisher, @@ -444,6 +460,8 @@ class __$$SnPostImplCopyWithImpl<$Res> Object? publishedUntil = freezed, Object? totalUpvote = null, Object? totalDownvote = null, + Object? totalViews = null, + Object? totalAggregatedViews = null, Object? publisherId = null, Object? pollId = freezed, Object? publisher = null, @@ -559,6 +577,14 @@ class __$$SnPostImplCopyWithImpl<$Res> ? _value.totalDownvote : totalDownvote // ignore: cast_nullable_to_non_nullable as int, + totalViews: null == totalViews + ? _value.totalViews + : totalViews // ignore: cast_nullable_to_non_nullable + as int, + totalAggregatedViews: null == totalAggregatedViews + ? _value.totalAggregatedViews + : totalAggregatedViews // ignore: cast_nullable_to_non_nullable + as int, publisherId: null == publisherId ? _value.publisherId : publisherId // ignore: cast_nullable_to_non_nullable @@ -614,6 +640,8 @@ class _$SnPostImpl extends _SnPost { required this.publishedUntil, required this.totalUpvote, required this.totalDownvote, + this.totalViews = 0, + this.totalAggregatedViews = 0, required this.publisherId, required this.pollId, required this.publisher, @@ -731,6 +759,12 @@ class _$SnPostImpl extends _SnPost { @override final int totalDownvote; @override + @JsonKey() + final int totalViews; + @override + @JsonKey() + final int totalAggregatedViews; + @override final int publisherId; @override final int? pollId; @@ -743,7 +777,7 @@ class _$SnPostImpl extends _SnPost { @override String toString() { - return 'SnPost(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, type: $type, body: $body, language: $language, alias: $alias, aliasPrefix: $aliasPrefix, tags: $tags, categories: $categories, replies: $replies, replyId: $replyId, repostId: $repostId, replyTo: $replyTo, repostTo: $repostTo, visibleUsersList: $visibleUsersList, invisibleUsersList: $invisibleUsersList, visibility: $visibility, editedAt: $editedAt, pinnedAt: $pinnedAt, lockedAt: $lockedAt, isDraft: $isDraft, publishedAt: $publishedAt, publishedUntil: $publishedUntil, totalUpvote: $totalUpvote, totalDownvote: $totalDownvote, publisherId: $publisherId, pollId: $pollId, publisher: $publisher, metric: $metric, preload: $preload)'; + return 'SnPost(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, type: $type, body: $body, language: $language, alias: $alias, aliasPrefix: $aliasPrefix, tags: $tags, categories: $categories, replies: $replies, replyId: $replyId, repostId: $repostId, replyTo: $replyTo, repostTo: $repostTo, visibleUsersList: $visibleUsersList, invisibleUsersList: $invisibleUsersList, visibility: $visibility, editedAt: $editedAt, pinnedAt: $pinnedAt, lockedAt: $lockedAt, isDraft: $isDraft, publishedAt: $publishedAt, publishedUntil: $publishedUntil, totalUpvote: $totalUpvote, totalDownvote: $totalDownvote, totalViews: $totalViews, totalAggregatedViews: $totalAggregatedViews, publisherId: $publisherId, pollId: $pollId, publisher: $publisher, metric: $metric, preload: $preload)'; } @override @@ -796,6 +830,10 @@ class _$SnPostImpl extends _SnPost { other.totalUpvote == totalUpvote) && (identical(other.totalDownvote, totalDownvote) || other.totalDownvote == totalDownvote) && + (identical(other.totalViews, totalViews) || + other.totalViews == totalViews) && + (identical(other.totalAggregatedViews, totalAggregatedViews) || + other.totalAggregatedViews == totalAggregatedViews) && (identical(other.publisherId, publisherId) || other.publisherId == publisherId) && (identical(other.pollId, pollId) || other.pollId == pollId) && @@ -836,6 +874,8 @@ class _$SnPostImpl extends _SnPost { publishedUntil, totalUpvote, totalDownvote, + totalViews, + totalAggregatedViews, publisherId, pollId, publisher, @@ -888,6 +928,8 @@ abstract class _SnPost extends SnPost { required final DateTime? publishedUntil, required final int totalUpvote, required final int totalDownvote, + final int totalViews, + final int totalAggregatedViews, required final int publisherId, required final int? pollId, required final SnPublisher publisher, @@ -952,6 +994,10 @@ abstract class _SnPost extends SnPost { @override int get totalDownvote; @override + int get totalViews; + @override + int get totalAggregatedViews; + @override int get publisherId; @override int? get pollId; diff --git a/lib/types/post.g.dart b/lib/types/post.g.dart index ea98319..1a639ce 100644 --- a/lib/types/post.g.dart +++ b/lib/types/post.g.dart @@ -62,6 +62,9 @@ _$SnPostImpl _$$SnPostImplFromJson(Map json) => _$SnPostImpl( : DateTime.parse(json['published_until'] as String), totalUpvote: (json['total_upvote'] as num).toInt(), totalDownvote: (json['total_downvote'] as num).toInt(), + totalViews: (json['total_views'] as num?)?.toInt() ?? 0, + totalAggregatedViews: + (json['total_aggregated_views'] as num?)?.toInt() ?? 0, publisherId: (json['publisher_id'] as num).toInt(), pollId: (json['poll_id'] as num?)?.toInt(), publisher: @@ -101,6 +104,8 @@ Map _$$SnPostImplToJson(_$SnPostImpl instance) => 'published_until': instance.publishedUntil?.toIso8601String(), 'total_upvote': instance.totalUpvote, 'total_downvote': instance.totalDownvote, + 'total_views': instance.totalViews, + 'total_aggregated_views': instance.totalAggregatedViews, 'publisher_id': instance.publisherId, 'poll_id': instance.pollId, 'publisher': instance.publisher.toJson(), diff --git a/lib/widgets/post/post_item.dart b/lib/widgets/post/post_item.dart index 7d90ecd..49a6763 100644 --- a/lib/widgets/post/post_item.dart +++ b/lib/widgets/post/post_item.dart @@ -684,6 +684,15 @@ class _PostBottomAction extends StatelessWidget { ); }, ), + InkWell( + child: Row( + children: [ + Icon(Symbols.play_circle, size: 20, color: iconColor), + const Gap(8), + Text('postViews').plural(data.totalViews), + ], + ), + ), ], ), InkWell( @@ -829,7 +838,6 @@ class _PostContentHeader extends StatelessWidget { await sn.client.delete('/cgi/co/posts/${data.id}', queryParameters: { 'publisherId': data.publisherId, }); - if (!context.mounted) return; context.showSnackbar('postDeleted'.tr(args: ['#${data.id}'])); } catch (err) { @@ -838,6 +846,25 @@ class _PostContentHeader extends StatelessWidget { } } + Future _flagPost(BuildContext context) async { + final confirm = await context.showConfirmDialog( + 'flagPost'.tr(), + 'flagPostDescription'.tr(), + ); + if (!confirm) return; + if (!context.mounted) return; + + try { + final sn = context.read(); + await sn.client.post('/cgi/co/posts/${data.id}/flag'); + if (!context.mounted) return; + context.showSnackbar('postFlagged'.tr()); + } catch (err) { + if (!context.mounted) return; + context.showErrorDialog(err); + } + } + @override Widget build(BuildContext context) { return Row( @@ -1029,6 +1056,18 @@ class _PostContentHeader extends StatelessWidget { children: [ const Icon(Symbols.flag), const Gap(16), + Text('flagPostAction').tr(), + ], + ), + onTap: () { + _flagPost(context); + }, + ), + PopupMenuItem( + child: Row( + children: [ + const Icon(Symbols.report), + const Gap(16), Text('report').tr(), ], ),