Compare commits

..

3 Commits

Author SHA1 Message Date
efddaf50f2 Abuse report 2024-12-08 01:01:20 +08:00
d4aaf61091 Report post 2024-12-08 00:53:22 +08:00
fa346b528e 🐛 Bug fixes on profile page 2024-12-08 00:40:44 +08:00
6 changed files with 263 additions and 28 deletions

View File

@ -387,5 +387,14 @@
"articleWrittenAt": "Written at {}", "articleWrittenAt": "Written at {}",
"articleEditedAt": "Edited at {}", "articleEditedAt": "Edited at {}",
"attachmentSaved": "Saved to album", "attachmentSaved": "Saved to album",
"openInAlbum": "Open in album" "openInAlbum": "Open in album",
"postAbuseReport": "Report Post",
"postAbuseReportDescription": "Report posts that violate our user agreement and community guidelines to help us improve the content on Solar Network. Please describe how this post violates the relevant rules. Do not include any sensitive information. We will process your report within 24 hours.",
"abuseReport": "Abuse Report",
"abuseReportDescription": "Report any resources that violate our user agreement and community guidelines to help us improve the content on Solar Network. Please describe the location of the resource (provide resource ID as best as possible) and how this violates the relevant rules. Do not include any sensitive information. We will process your report within 24 hours.",
"abuseReportActionDescription": "Report abuse usage behavior.",
"abuseReportResource": "Resource Location / ID",
"abuseReportReason": "Reason",
"abuseReportSubmitted": "Report submitted, thank you for your contribution.",
"submit": "Submit"
} }

View File

@ -377,7 +377,7 @@
"accountJoinedAt": "加入于 {}", "accountJoinedAt": "加入于 {}",
"accountBirthday": "出生于 {}", "accountBirthday": "出生于 {}",
"accountBadge": "徽章", "accountBadge": "徽章",
"badgeCompanyStaff": "索尔辛茨士大夫 · Staff", "badgeCompanyStaff": "索尔辛茨士大夫 · 员工",
"badgeSiteMigration": "Solar Network 原住民", "badgeSiteMigration": "Solar Network 原住民",
"accountStatus": "状态", "accountStatus": "状态",
"accountStatusOnline": "在线", "accountStatusOnline": "在线",
@ -387,5 +387,14 @@
"articleWrittenAt": "发表于 {}", "articleWrittenAt": "发表于 {}",
"articleEditedAt": "编辑于 {}", "articleEditedAt": "编辑于 {}",
"attachmentSaved": "已保存到相册", "attachmentSaved": "已保存到相册",
"openInAlbum": "在相册中打开" "openInAlbum": "在相册中打开",
"postAbuseReport": "检举帖子",
"postAbuseReportDescription": "检举不符合我们用户协议以及社区准则的帖子,来帮助我们更好的维护 Solar Network 上的内容。请在下面描述该帖子如何违反我么的相关规定。请勿填写任何敏感信息。我们将会在 24 小时内处理您的检举。",
"abuseReport": "检举",
"abuseReportDescription": "检举不符合我们用户协议以及社区准则的任何资源,来帮助我们更好的维护 Solar Network 上的内容。请在下面描述资源的位置(提供资源 ID 为佳)以及如何违反我么的相关规定。请勿填写任何敏感信息。我们将会在 24 小时内处理您的检举。",
"abuseReportActionDescription": "检举不合规行为。",
"abuseReportResource": "资源位置 / ID",
"abuseReportReason": "检举原因",
"abuseReportSubmitted": "检举已提交,感谢你的贡献。",
"submit": "提交"
} }

View File

@ -5,6 +5,7 @@ import 'package:go_router/go_router.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/sn_network.dart';
import 'package:surface/providers/userinfo.dart'; import 'package:surface/providers/userinfo.dart';
import 'package:surface/widgets/account/account_image.dart'; import 'package:surface/widgets/account/account_image.dart';
import 'package:surface/widgets/app_bar_leading.dart'; import 'package:surface/widgets/app_bar_leading.dart';
@ -104,6 +105,23 @@ class _AuthorizedAccountScreen extends StatelessWidget {
GoRouter.of(context).pushNamed('accountPublishers'); GoRouter.of(context).pushNamed('accountPublishers');
}, },
), ),
ListTile(
title: Text('abuseReport').tr(),
subtitle: Text('abuseReportActionDescription').tr(),
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
leading: const Icon(Symbols.flag),
trailing: const Icon(Symbols.chevron_right),
onTap: () {
showDialog(
context: context,
builder: (context) => _AbuseReportDialog(),
).then((value) {
if (value == true && context.mounted) {
context.showSnackbar('abuseReportSubmitted'.tr());
}
});
},
),
ListTile( ListTile(
title: Text('accountLogout').tr(), title: Text('accountLogout').tr(),
subtitle: Text('accountLogoutSubtitle').tr(), subtitle: Text('accountLogoutSubtitle').tr(),
@ -183,3 +201,89 @@ class _UnauthorizedAccountScreen extends StatelessWidget {
); );
} }
} }
class _AbuseReportDialog extends StatefulWidget {
const _AbuseReportDialog({super.key});
@override
State<_AbuseReportDialog> createState() => _AbuseReportDialogState();
}
class _AbuseReportDialogState extends State<_AbuseReportDialog> {
bool _isBusy = false;
final _resourceController = TextEditingController();
final _reasonController = TextEditingController();
@override
dispose() {
_resourceController.dispose();
_reasonController.dispose();
super.dispose();
}
Future<void> _performAction() async {
setState(() => _isBusy = true);
try {
final sn = context.read<SnNetworkProvider>();
await sn.client.request(
'/cgi/id/reports/abuse',
data: {
'resource': _resourceController.text,
'reason': _reasonController.text,
},
);
if (!mounted) return;
Navigator.pop(context, true);
} catch (err) {
if (!mounted) return;
context.showErrorDialog(err);
} finally {
setState(() => _isBusy = false);
}
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text('abuseReport'.tr()),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('abuseReportDescription'.tr()),
const Gap(12),
TextField(
controller: _resourceController,
maxLength: null,
decoration: InputDecoration(
border: const UnderlineInputBorder(),
labelText: 'abuseReportResource'.tr(),
),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
),
const Gap(4),
TextField(
controller: _reasonController,
maxLength: null,
decoration: InputDecoration(
border: const UnderlineInputBorder(),
labelText: 'abuseReportReason'.tr(),
),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
),
],
),
actions: [
TextButton(
onPressed: _isBusy ? null : () => Navigator.pop(context),
child: Text('dialogDismiss').tr(),
),
TextButton(
onPressed: _isBusy ? null : _performAction,
child: Text('submit').tr(),
),
],
);
}
}

View File

@ -6,6 +6,7 @@ import 'package:gap/gap.dart';
import 'package:google_fonts/google_fonts.dart'; import 'package:google_fonts/google_fonts.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:relative_time/relative_time.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/sn_network.dart'; import 'package:surface/providers/sn_network.dart';
import 'package:surface/types/account.dart'; import 'package:surface/types/account.dart';
@ -28,14 +29,14 @@ const Map<String, (String, IconData, Color)> kBadgesMeta = {
class UserScreen extends StatefulWidget { class UserScreen extends StatefulWidget {
final String name; final String name;
const UserScreen({super.key, required this.name}); const UserScreen({super.key, required this.name});
@override @override
State<UserScreen> createState() => _UserScreenState(); State<UserScreen> createState() => _UserScreenState();
} }
class _UserScreenState extends State<UserScreen> class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateMixin {
with SingleTickerProviderStateMixin {
late final ScrollController _scrollController = ScrollController(); late final ScrollController _scrollController = ScrollController();
SnAccount? _account; SnAccount? _account;
@ -75,14 +76,12 @@ class _UserScreenState extends State<UserScreen>
double _appBarBlur = 0.0; double _appBarBlur = 0.0;
late final _appBarWidth = MediaQuery.of(context).size.width; late final _appBarWidth = MediaQuery.of(context).size.width;
late final _appBarHeight = late final _appBarHeight = (_appBarWidth * kBannerAspectRatio).roundToDouble();
(_appBarWidth * kBannerAspectRatio).roundToDouble();
void _updateAppBarBlur() { void _updateAppBarBlur() {
if (_scrollController.offset > _appBarHeight) return; if (_scrollController.offset > _appBarHeight) return;
setState(() { setState(() {
_appBarBlur = _appBarBlur = (_scrollController.offset / _appBarHeight * 10).clamp(0.0, 10.0);
(_scrollController.offset / _appBarHeight * 10).clamp(0.0, 10.0);
}); });
} }
@ -218,9 +217,7 @@ class _UserScreenState extends State<UserScreen>
Symbols.circle, Symbols.circle,
fill: 1, fill: 1,
size: 16, size: 16,
color: (_status?.isOnline ?? false) color: (_status?.isOnline ?? false) ? Colors.green : Colors.grey,
? Colors.green
: Colors.grey,
).padding(all: 4), ).padding(all: 4),
const Gap(8), const Gap(8),
Text( Text(
@ -230,19 +227,55 @@ class _UserScreenState extends State<UserScreen>
: 'accountStatusOffline'.tr() : 'accountStatusOffline'.tr()
: 'loading'.tr(), : 'loading'.tr(),
), ),
if (_status != null && !_status!.isOnline && _status!.lastSeenAt != null)
Text(
'accountStatusLastSeen'.tr(args: [
_status!.lastSeenAt != null
? RelativeTime(context).format(
_status!.lastSeenAt!.toLocal(),
)
: 'unknown',
]),
).padding(left: 6).opacity(0.75),
], ],
).padding(vertical: 8, horizontal: 12), ).padding(vertical: 8, horizontal: 12),
), ),
const Gap(8), const Gap(8),
Wrap(
children: _account!.badges
.map(
(ele) => Tooltip(
richMessage: TextSpan(
children: [
TextSpan(text: kBadgesMeta[ele.type]?.$1.tr() ?? 'unknown'.tr()),
if (ele.metadata['title'] != null)
TextSpan(
text: '\n${ele.metadata['title']}',
style: const TextStyle(fontWeight: FontWeight.bold),
),
TextSpan(text: '\n'),
TextSpan(
text: DateFormat.yMEd().format(ele.createdAt),
),
],
),
child: Icon(
kBadgesMeta[ele.type]?.$2 ?? Symbols.question_mark,
color: kBadgesMeta[ele.type]?.$3,
fill: 1,
),
),
)
.toList(),
).padding(horizontal: 8),
const Gap(8),
Column( Column(
children: [ children: [
Row( Row(
children: [ children: [
const Icon(Symbols.calendar_add_on), const Icon(Symbols.calendar_add_on),
const Gap(8), const Gap(8),
Text('publisherJoinedAt').tr(args: [ Text('publisherJoinedAt').tr(args: [DateFormat('y/M/d').format(_account!.createdAt)]),
DateFormat('y/M/d').format(_account!.createdAt)
]),
], ],
), ),
Row( Row(
@ -279,11 +312,7 @@ class _UserScreenState extends State<UserScreen>
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text('accountBadge') Text('accountBadge').bold().fontSize(17).tr().padding(horizontal: 20, bottom: 4),
.bold()
.fontSize(17)
.tr()
.padding(horizontal: 20, bottom: 4),
SizedBox( SizedBox(
height: 80, height: 80,
width: double.infinity, width: double.infinity,
@ -297,8 +326,7 @@ class _UserScreenState extends State<UserScreen>
child: Card( child: Card(
child: ListTile( child: ListTile(
leading: Icon( leading: Icon(
kBadgesMeta[badge.type]?.$2 ?? kBadgesMeta[badge.type]?.$2 ?? Symbols.question_mark,
Symbols.question_mark,
color: kBadgesMeta[badge.type]?.$3, color: kBadgesMeta[badge.type]?.$3,
fill: 1, fill: 1,
), ),
@ -308,8 +336,7 @@ class _UserScreenState extends State<UserScreen>
subtitle: badge.metadata['title'] != null subtitle: badge.metadata['title'] != null
? Text(badge.metadata['title']) ? Text(badge.metadata['title'])
: Text( : Text(
DateFormat('y/M/d') DateFormat('y/M/d').format(badge.createdAt),
.format(badge.createdAt),
), ),
), ),
), ),

View File

@ -73,7 +73,6 @@ class MarkdownTextContent extends StatelessWidget {
extensionSet: markdown.ExtensionSet( extensionSet: markdown.ExtensionSet(
<markdown.BlockSyntax>[ <markdown.BlockSyntax>[
markdown.CodeBlockSyntax(), markdown.CodeBlockSyntax(),
...markdown.ExtensionSet.commonMark.blockSyntaxes,
...markdown.ExtensionSet.gitHubFlavored.blockSyntaxes, ...markdown.ExtensionSet.gitHubFlavored.blockSyntaxes,
], ],
<markdown.InlineSyntax>[ <markdown.InlineSyntax>[
@ -82,7 +81,6 @@ class MarkdownTextContent extends StatelessWidget {
markdown.AutolinkSyntax(), markdown.AutolinkSyntax(),
markdown.AutolinkExtensionSyntax(), markdown.AutolinkExtensionSyntax(),
markdown.CodeSyntax(), markdown.CodeSyntax(),
...markdown.ExtensionSet.commonMark.inlineSyntaxes,
...markdown.ExtensionSet.gitHubFlavored.inlineSyntaxes ...markdown.ExtensionSet.gitHubFlavored.inlineSyntaxes
], ],
), ),
@ -93,7 +91,7 @@ class MarkdownTextContent extends StatelessWidget {
final segments = uri.split('/'); final segments = uri.split('/');
switch (segments[0]) { switch (segments[0]) {
default: default:
GoRouter.of(context).push(uri); GoRouter.of(context).push('/$uri');
} }
return; return;
} }
@ -188,7 +186,7 @@ class _UserNameCardInlineSyntax extends markdown.InlineSyntax {
final alias = match[0]!; final alias = match[0]!;
final anchor = markdown.Element.text('a', alias) final anchor = markdown.Element.text('a', alias)
..attributes['href'] = Uri.encodeFull( ..attributes['href'] = Uri.encodeFull(
'solink://users/${alias.substring(1)}', 'solink://account/${alias.substring(1)}',
); );
parser.addNode(anchor); parser.addNode(anchor);

View File

@ -569,6 +569,18 @@ class _PostContentHeader extends StatelessWidget {
Text('report').tr(), Text('report').tr(),
], ],
), ),
onTap: () {
showDialog(
context: context,
builder: (context) => _PostAbuseReportDialog(
data: data,
),
).then((value) {
if (value == true && context.mounted) {
context.showSnackbar('abuseReportSubmitted'.tr());
}
});
},
), ),
], ],
), ),
@ -724,3 +736,79 @@ class _PostTruncatedHint extends StatelessWidget {
).opacity(0.75); ).opacity(0.75);
} }
} }
class _PostAbuseReportDialog extends StatefulWidget {
final SnPost data;
const _PostAbuseReportDialog({super.key, required this.data});
@override
State<_PostAbuseReportDialog> createState() => _PostAbuseReportDialogState();
}
class _PostAbuseReportDialogState extends State<_PostAbuseReportDialog> {
bool _isBusy = false;
final _reasonController = TextEditingController();
@override
dispose() {
_reasonController.dispose();
super.dispose();
}
Future<void> _performAction() async {
setState(() => _isBusy = true);
try {
final sn = context.read<SnNetworkProvider>();
await sn.client.request(
'/cgi/id/reports/abuse',
data: {
'resource': 'post:${widget.data.id}',
'reason': _reasonController.text,
},
);
if (!mounted) return;
Navigator.pop(context, true);
} catch (err) {
if (!mounted) return;
context.showErrorDialog(err);
} finally {
setState(() => _isBusy = false);
}
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text('postAbuseReport'.tr()),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('postAbuseReportDescription'.tr()),
const Gap(12),
TextField(
controller: _reasonController,
maxLength: null,
decoration: InputDecoration(
border: const UnderlineInputBorder(),
labelText: 'abuseReportReason'.tr(),
),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
),
],
),
actions: [
TextButton(
onPressed: _isBusy ? null : () => Navigator.pop(context),
child: Text('dialogDismiss').tr(),
),
TextButton(
onPressed: _isBusy ? null : _performAction,
child: Text('submit').tr(),
),
],
);
}
}