✨ Pin post
This commit is contained in:
@@ -18,6 +18,7 @@ import 'package:island/screens/posts/compose.dart';
|
||||
import 'package:island/widgets/alert.dart';
|
||||
import 'package:island/widgets/content/markdown.dart';
|
||||
import 'package:island/widgets/post/post_item_screenshot.dart';
|
||||
import 'package:island/widgets/post/post_pin_sheet.dart';
|
||||
import 'package:island/widgets/post/post_shared.dart';
|
||||
import 'package:island/widgets/safety/abuse_report_helper.dart';
|
||||
import 'package:island/widgets/share/share_sheet.dart';
|
||||
@@ -202,6 +203,45 @@ class PostActionableItem extends HookConsumerWidget {
|
||||
);
|
||||
},
|
||||
),
|
||||
if (isAuthor && item.pinMode == null)
|
||||
MenuAction(
|
||||
title: 'pinPost'.tr(),
|
||||
image: MenuImage.icon(Symbols.keep),
|
||||
callback: () {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (context) => PostPinSheet(post: item),
|
||||
).then((value) {
|
||||
if (value is int) {
|
||||
onUpdate?.call(item.copyWith(pinMode: value));
|
||||
}
|
||||
});
|
||||
},
|
||||
)
|
||||
else if (isAuthor && item.pinMode != null)
|
||||
MenuAction(
|
||||
title: 'unpinPost'.tr(),
|
||||
image: MenuImage.icon(Symbols.keep_off),
|
||||
callback: () {
|
||||
showConfirmAlert('unpinPostHint'.tr(), 'unpinPost'.tr()).then(
|
||||
(confirm) async {
|
||||
if (confirm) {
|
||||
final client = ref.watch(apiClientProvider);
|
||||
try {
|
||||
if (context.mounted) showLoadingModal(context);
|
||||
await client.delete('/sphere/posts/${item.id}/pin');
|
||||
onUpdate?.call(item.copyWith(pinMode: null));
|
||||
} catch (err) {
|
||||
showErrorAlert(err);
|
||||
} finally {
|
||||
if (context.mounted) hideLoadingModal(context);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
MenuSeparator(),
|
||||
MenuAction(
|
||||
title: 'share'.tr(),
|
||||
|
@@ -22,6 +22,7 @@ class PostListNotifier extends _$PostListNotifier
|
||||
List<String>? categories,
|
||||
List<String>? tags,
|
||||
bool shuffle = false,
|
||||
bool pinned = false,
|
||||
}) {
|
||||
return fetch(cursor: null);
|
||||
}
|
||||
@@ -40,6 +41,7 @@ class PostListNotifier extends _$PostListNotifier
|
||||
if (tags != null) 'tags': tags,
|
||||
if (categories != null) 'categories': categories,
|
||||
if (shuffle) 'shuffle': true,
|
||||
if (pinned) 'pinned': true,
|
||||
};
|
||||
|
||||
final response = await client.get(
|
||||
@@ -77,6 +79,7 @@ class SliverPostList extends HookConsumerWidget {
|
||||
final List<String>? categories;
|
||||
final List<String>? tags;
|
||||
final bool shuffle;
|
||||
final bool pinned;
|
||||
final PostItemType itemType;
|
||||
final Color? backgroundColor;
|
||||
final EdgeInsets? padding;
|
||||
@@ -93,6 +96,7 @@ class SliverPostList extends HookConsumerWidget {
|
||||
this.categories,
|
||||
this.tags,
|
||||
this.shuffle = false,
|
||||
this.pinned = false,
|
||||
this.itemType = PostItemType.regular,
|
||||
this.backgroundColor,
|
||||
this.padding,
|
||||
@@ -104,33 +108,19 @@ class SliverPostList extends HookConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final provider = postListNotifierProvider(
|
||||
pubName: pubName,
|
||||
realm: realm,
|
||||
type: type,
|
||||
categories: categories,
|
||||
tags: tags,
|
||||
shuffle: shuffle,
|
||||
pinned: pinned,
|
||||
);
|
||||
return PagingHelperSliverView(
|
||||
provider: postListNotifierProvider(
|
||||
pubName: pubName,
|
||||
realm: realm,
|
||||
type: type,
|
||||
categories: categories,
|
||||
tags: tags,
|
||||
shuffle: shuffle,
|
||||
),
|
||||
futureRefreshable:
|
||||
postListNotifierProvider(
|
||||
pubName: pubName,
|
||||
realm: realm,
|
||||
type: type,
|
||||
categories: categories,
|
||||
tags: tags,
|
||||
shuffle: shuffle,
|
||||
).future,
|
||||
notifierRefreshable:
|
||||
postListNotifierProvider(
|
||||
pubName: pubName,
|
||||
realm: realm,
|
||||
type: type,
|
||||
categories: categories,
|
||||
tags: tags,
|
||||
shuffle: shuffle,
|
||||
).notifier,
|
||||
provider: provider,
|
||||
futureRefreshable: provider.future,
|
||||
notifierRefreshable: provider.notifier,
|
||||
contentBuilder:
|
||||
(data, widgetCount, endItemView) => SliverList.builder(
|
||||
itemCount: widgetCount,
|
||||
|
@@ -6,7 +6,7 @@ part of 'post_list.dart';
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$postListNotifierHash() => r'faa0b939fae56367ff120ce63d9deb17b1995c9c';
|
||||
String _$postListNotifierHash() => r'7be076e6cee1c52c258d0fad2cd9fe9ac5e100ac';
|
||||
|
||||
/// Copied from Dart SDK
|
||||
class _SystemHash {
|
||||
@@ -37,6 +37,7 @@ abstract class _$PostListNotifier
|
||||
late final List<String>? categories;
|
||||
late final List<String>? tags;
|
||||
late final bool shuffle;
|
||||
late final bool pinned;
|
||||
|
||||
FutureOr<CursorPagingData<SnPost>> build({
|
||||
String? pubName,
|
||||
@@ -45,6 +46,7 @@ abstract class _$PostListNotifier
|
||||
List<String>? categories,
|
||||
List<String>? tags,
|
||||
bool shuffle = false,
|
||||
bool pinned = false,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -66,6 +68,7 @@ class PostListNotifierFamily
|
||||
List<String>? categories,
|
||||
List<String>? tags,
|
||||
bool shuffle = false,
|
||||
bool pinned = false,
|
||||
}) {
|
||||
return PostListNotifierProvider(
|
||||
pubName: pubName,
|
||||
@@ -74,6 +77,7 @@ class PostListNotifierFamily
|
||||
categories: categories,
|
||||
tags: tags,
|
||||
shuffle: shuffle,
|
||||
pinned: pinned,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -88,6 +92,7 @@ class PostListNotifierFamily
|
||||
categories: provider.categories,
|
||||
tags: provider.tags,
|
||||
shuffle: provider.shuffle,
|
||||
pinned: provider.pinned,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -121,6 +126,7 @@ class PostListNotifierProvider
|
||||
List<String>? categories,
|
||||
List<String>? tags,
|
||||
bool shuffle = false,
|
||||
bool pinned = false,
|
||||
}) : this._internal(
|
||||
() =>
|
||||
PostListNotifier()
|
||||
@@ -129,7 +135,8 @@ class PostListNotifierProvider
|
||||
..type = type
|
||||
..categories = categories
|
||||
..tags = tags
|
||||
..shuffle = shuffle,
|
||||
..shuffle = shuffle
|
||||
..pinned = pinned,
|
||||
from: postListNotifierProvider,
|
||||
name: r'postListNotifierProvider',
|
||||
debugGetCreateSourceHash:
|
||||
@@ -145,6 +152,7 @@ class PostListNotifierProvider
|
||||
categories: categories,
|
||||
tags: tags,
|
||||
shuffle: shuffle,
|
||||
pinned: pinned,
|
||||
);
|
||||
|
||||
PostListNotifierProvider._internal(
|
||||
@@ -160,6 +168,7 @@ class PostListNotifierProvider
|
||||
required this.categories,
|
||||
required this.tags,
|
||||
required this.shuffle,
|
||||
required this.pinned,
|
||||
}) : super.internal();
|
||||
|
||||
final String? pubName;
|
||||
@@ -168,6 +177,7 @@ class PostListNotifierProvider
|
||||
final List<String>? categories;
|
||||
final List<String>? tags;
|
||||
final bool shuffle;
|
||||
final bool pinned;
|
||||
|
||||
@override
|
||||
FutureOr<CursorPagingData<SnPost>> runNotifierBuild(
|
||||
@@ -180,6 +190,7 @@ class PostListNotifierProvider
|
||||
categories: categories,
|
||||
tags: tags,
|
||||
shuffle: shuffle,
|
||||
pinned: pinned,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -195,7 +206,8 @@ class PostListNotifierProvider
|
||||
..type = type
|
||||
..categories = categories
|
||||
..tags = tags
|
||||
..shuffle = shuffle,
|
||||
..shuffle = shuffle
|
||||
..pinned = pinned,
|
||||
from: from,
|
||||
name: null,
|
||||
dependencies: null,
|
||||
@@ -207,6 +219,7 @@ class PostListNotifierProvider
|
||||
categories: categories,
|
||||
tags: tags,
|
||||
shuffle: shuffle,
|
||||
pinned: pinned,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -228,7 +241,8 @@ class PostListNotifierProvider
|
||||
other.type == type &&
|
||||
other.categories == categories &&
|
||||
other.tags == tags &&
|
||||
other.shuffle == shuffle;
|
||||
other.shuffle == shuffle &&
|
||||
other.pinned == pinned;
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -240,6 +254,7 @@ class PostListNotifierProvider
|
||||
hash = _SystemHash.combine(hash, categories.hashCode);
|
||||
hash = _SystemHash.combine(hash, tags.hashCode);
|
||||
hash = _SystemHash.combine(hash, shuffle.hashCode);
|
||||
hash = _SystemHash.combine(hash, pinned.hashCode);
|
||||
|
||||
return _SystemHash.finish(hash);
|
||||
}
|
||||
@@ -266,6 +281,9 @@ mixin PostListNotifierRef
|
||||
|
||||
/// The parameter `shuffle` of this provider.
|
||||
bool get shuffle;
|
||||
|
||||
/// The parameter `pinned` of this provider.
|
||||
bool get pinned;
|
||||
}
|
||||
|
||||
class _PostListNotifierProviderElement
|
||||
@@ -290,6 +308,8 @@ class _PostListNotifierProviderElement
|
||||
List<String>? get tags => (origin as PostListNotifierProvider).tags;
|
||||
@override
|
||||
bool get shuffle => (origin as PostListNotifierProvider).shuffle;
|
||||
@override
|
||||
bool get pinned => (origin as PostListNotifierProvider).pinned;
|
||||
}
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
|
124
lib/widgets/post/post_pin_sheet.dart
Normal file
124
lib/widgets/post/post_pin_sheet.dart
Normal file
@@ -0,0 +1,124 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/models/post.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
import 'package:island/widgets/alert.dart';
|
||||
import 'package:island/widgets/content/sheet.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
|
||||
class PostPinSheet extends HookConsumerWidget {
|
||||
final SnPost post;
|
||||
const PostPinSheet({super.key, required this.post});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final mode = useState(0);
|
||||
|
||||
Future<void> pinPost() async {
|
||||
try {
|
||||
showLoadingModal(context);
|
||||
final client = ref.watch(apiClientProvider);
|
||||
await client.post(
|
||||
'/sphere/posts/${post.id}/pin',
|
||||
data: {'mode': mode.value},
|
||||
);
|
||||
|
||||
if (context.mounted) Navigator.of(context).pop(mode.value);
|
||||
} catch (e) {
|
||||
showErrorAlert(e);
|
||||
} finally {
|
||||
if (context.mounted) hideLoadingModal(context);
|
||||
}
|
||||
}
|
||||
|
||||
return SheetScaffold(
|
||||
titleText: 'pinPost'.tr(),
|
||||
heightFactor: 0.6,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// Publisher page pin option (always available)
|
||||
ListTile(
|
||||
leading: Radio<int>(
|
||||
value: 0,
|
||||
groupValue: mode.value,
|
||||
onChanged: (value) {
|
||||
mode.value = value!;
|
||||
},
|
||||
),
|
||||
title: Text('publisherPage'.tr()),
|
||||
subtitle: Text('pinPostPublisherHint'.tr()),
|
||||
onTap: () {
|
||||
mode.value = 0;
|
||||
},
|
||||
),
|
||||
|
||||
// Realm page pin option (show always, but disabled when not available)
|
||||
ListTile(
|
||||
leading: Radio<int>(
|
||||
value: 1,
|
||||
groupValue: mode.value,
|
||||
onChanged:
|
||||
post.realmId != null && post.realmId!.isNotEmpty
|
||||
? (value) {
|
||||
mode.value = value!;
|
||||
}
|
||||
: null,
|
||||
),
|
||||
title: Text('realmPage'.tr()),
|
||||
subtitle:
|
||||
post.realmId != null && post.realmId!.isNotEmpty
|
||||
? Text('pinPostRealmHint'.tr())
|
||||
: Text('pinPostRealmDisabledHint'.tr()),
|
||||
onTap:
|
||||
post.realmId != null && post.realmId!.isNotEmpty
|
||||
? () {
|
||||
mode.value = 1;
|
||||
}
|
||||
: null,
|
||||
enabled: post.realmId != null && post.realmId!.isNotEmpty,
|
||||
),
|
||||
|
||||
// Reply page pin option (show always, but disabled when not available)
|
||||
ListTile(
|
||||
leading: Radio<int>(
|
||||
value: 2,
|
||||
groupValue: mode.value,
|
||||
onChanged:
|
||||
post.repliedPostId != null && post.repliedPostId!.isNotEmpty
|
||||
? (value) {
|
||||
mode.value = value!;
|
||||
}
|
||||
: null,
|
||||
),
|
||||
title: Text('replyPage'.tr()),
|
||||
subtitle:
|
||||
post.repliedPostId != null && post.repliedPostId!.isNotEmpty
|
||||
? Text('pinPostReplyHint'.tr())
|
||||
: Text('pinPostReplyDisabledHint'.tr()),
|
||||
onTap:
|
||||
post.repliedPostId != null && post.repliedPostId!.isNotEmpty
|
||||
? () {
|
||||
mode.value = 2;
|
||||
}
|
||||
: null,
|
||||
enabled:
|
||||
post.repliedPostId != null && post.repliedPostId!.isNotEmpty,
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Pin button
|
||||
FilledButton.icon(
|
||||
onPressed: pinPost,
|
||||
icon: const Icon(Symbols.keep),
|
||||
label: Text('pin'.tr()),
|
||||
).padding(horizontal: 24),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@@ -545,107 +545,119 @@ class PostHeader extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
spacing: 12,
|
||||
return Column(
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap:
|
||||
isInteractive
|
||||
? () {
|
||||
context.pushNamed(
|
||||
'publisherProfile',
|
||||
pathParameters: {'name': item.publisher.name},
|
||||
);
|
||||
}
|
||||
: null,
|
||||
child: ProfilePictureWidget(
|
||||
file: item.publisher.picture,
|
||||
radius: 16,
|
||||
borderRadius: item.publisher.type == 0 ? null : 6,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
if (item.pinMode != null)
|
||||
Row(
|
||||
spacing: 4,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
spacing: 4,
|
||||
const Icon(Symbols.keep, size: 15, fill: 1),
|
||||
Text('pinnedPost').tr().fontSize(13),
|
||||
],
|
||||
).opacity(0.8).padding(horizontal: 8, bottom: 4),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
spacing: 12,
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap:
|
||||
isInteractive
|
||||
? () {
|
||||
context.pushNamed(
|
||||
'publisherProfile',
|
||||
pathParameters: {'name': item.publisher.name},
|
||||
);
|
||||
}
|
||||
: null,
|
||||
child: ProfilePictureWidget(
|
||||
file: item.publisher.picture,
|
||||
radius: 16,
|
||||
borderRadius: item.publisher.type == 0 ? null : 6,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(item.publisher.nick).bold(),
|
||||
if (item.publisher.verification != null)
|
||||
VerificationMark(mark: item.publisher.verification!),
|
||||
if (item.realm == null)
|
||||
Text('@${item.publisher.name}').fontSize(11)
|
||||
else
|
||||
...([
|
||||
const Icon(Symbols.arrow_right, size: 14),
|
||||
Flexible(
|
||||
child: InkWell(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
spacing: 5,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
item.realm!.name,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
ProfilePictureWidget(
|
||||
file: item.realm!.picture,
|
||||
fallbackIcon: Symbols.group,
|
||||
radius: 9,
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
spacing: 4,
|
||||
children: [
|
||||
Text(item.publisher.nick).bold(),
|
||||
if (item.publisher.verification != null)
|
||||
VerificationMark(mark: item.publisher.verification!),
|
||||
if (item.realm == null)
|
||||
Text('@${item.publisher.name}').fontSize(11)
|
||||
else
|
||||
...([
|
||||
const Icon(Symbols.arrow_right, size: 14),
|
||||
Flexible(
|
||||
child: InkWell(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
spacing: 5,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
item.realm!.name,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
ProfilePictureWidget(
|
||||
file: item.realm!.picture,
|
||||
fallbackIcon: Symbols.group,
|
||||
radius: 9,
|
||||
),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed(
|
||||
'realmDetail',
|
||||
pathParameters: {'slug': item.realm!.slug},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
]),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
spacing: 6,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
!isFullPost && isRelativeTime
|
||||
? (item.publishedAt ?? item.createdAt)!
|
||||
.formatRelative(context)
|
||||
: (item.publishedAt ?? item.createdAt)!
|
||||
.formatSystem(),
|
||||
).fontSize(10),
|
||||
if (item.editedAt != null)
|
||||
Text(
|
||||
'editedAt'.tr(
|
||||
args: [
|
||||
!isFullPost && isRelativeTime
|
||||
? item.editedAt!.formatRelative(context)
|
||||
: item.editedAt!.formatSystem(),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed(
|
||||
'realmDetail',
|
||||
pathParameters: {'slug': item.realm!.slug},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
]),
|
||||
).fontSize(10),
|
||||
if (item.visibility != 0)
|
||||
Text(
|
||||
PostVisibilityHelpers.getVisibilityText(
|
||||
item.visibility,
|
||||
).tr(),
|
||||
).fontSize(10),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
spacing: 6,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
!isFullPost && isRelativeTime
|
||||
? (item.publishedAt ?? item.createdAt)!.formatRelative(
|
||||
context,
|
||||
)
|
||||
: (item.publishedAt ?? item.createdAt)!.formatSystem(),
|
||||
).fontSize(10),
|
||||
if (item.editedAt != null)
|
||||
Text(
|
||||
'editedAt'.tr(
|
||||
args: [
|
||||
!isFullPost && isRelativeTime
|
||||
? item.editedAt!.formatRelative(context)
|
||||
: item.editedAt!.formatSystem(),
|
||||
],
|
||||
),
|
||||
).fontSize(10),
|
||||
if (item.visibility != 0)
|
||||
Text(
|
||||
PostVisibilityHelpers.getVisibilityText(
|
||||
item.visibility,
|
||||
).tr(),
|
||||
).fontSize(10),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (trailing != null) trailing!,
|
||||
],
|
||||
),
|
||||
if (trailing != null) trailing!,
|
||||
],
|
||||
).padding(horizontal: renderingPadding.horizontal, bottom: 4);
|
||||
}
|
||||
|
Reference in New Issue
Block a user