kopia lustrzana https://gitlab.com/mysocialportal/relatica
Add initial visibility configuration in posts
rodzic
aafe6dea7c
commit
8617e21d1a
|
@ -1,7 +1,8 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/material.dart' hide Visibility;
|
||||
|
||||
import '../../globals.dart';
|
||||
import '../../models/image_entry.dart';
|
||||
import '../../models/visibility.dart';
|
||||
import '../../screens/existing_image_selector_screen.dart';
|
||||
import '../../screens/image_viewer_screen.dart';
|
||||
import '../../serializers/friendica/image_entry_friendica_extensions.dart';
|
||||
|
@ -11,7 +12,13 @@ import '../padding.dart';
|
|||
class GallerySelectorControl extends StatefulWidget {
|
||||
final List<ImageEntry> entries;
|
||||
|
||||
const GallerySelectorControl({super.key, required this.entries});
|
||||
final Visibility? visibilityFilter;
|
||||
|
||||
const GallerySelectorControl({
|
||||
super.key,
|
||||
required this.entries,
|
||||
this.visibilityFilter,
|
||||
});
|
||||
|
||||
@override
|
||||
State<GallerySelectorControl> createState() => _GallerySelectorControlState();
|
||||
|
@ -31,7 +38,8 @@ class _GallerySelectorControlState extends State<GallerySelectorControl> {
|
|||
onPressed: () async {
|
||||
final result = await Navigator.of(context).push<List<ImageEntry>>(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => ExistingImageSelectorScreen()));
|
||||
builder: (context) => ExistingImageSelectorScreen(
|
||||
visibilityFilter: widget.visibilityFilter)));
|
||||
if (result != null) {
|
||||
setState(() {
|
||||
widget.entries.clear();
|
||||
|
|
|
@ -20,14 +20,17 @@ import '../models/instance_info.dart';
|
|||
import '../models/media_attachment_uploads/image_types_enum.dart';
|
||||
import '../models/timeline_entry.dart';
|
||||
import '../models/user_notification.dart';
|
||||
import '../models/visibility.dart';
|
||||
import '../serializers/friendica/direct_message_friendica_extensions.dart';
|
||||
import '../serializers/friendica/gallery_data_friendica_extensions.dart';
|
||||
import '../serializers/friendica/image_entry_friendica_extensions.dart';
|
||||
import '../serializers/friendica/visibility_friendica_extensions.dart';
|
||||
import '../serializers/mastodon/connection_mastodon_extensions.dart';
|
||||
import '../serializers/mastodon/group_data_mastodon_extensions.dart';
|
||||
import '../serializers/mastodon/instance_info_mastodon_extensions.dart';
|
||||
import '../serializers/mastodon/notification_mastodon_extension.dart';
|
||||
import '../serializers/mastodon/timeline_entry_mastodon_extensions.dart';
|
||||
import '../serializers/mastodon/visibility_mastodon_extensions.dart';
|
||||
import '../services/network_status_service.dart';
|
||||
import 'paging_data.dart';
|
||||
|
||||
|
@ -548,6 +551,7 @@ class RemoteFileClient extends FriendicaClient {
|
|||
String description = '',
|
||||
String album = '',
|
||||
String fileName = '',
|
||||
required Visibility visibility,
|
||||
}) async {
|
||||
final postUri = Uri.parse('https://$serverName/api/friendica/photo/create');
|
||||
final request = http.MultipartRequest('POST', postUri);
|
||||
|
@ -557,7 +561,8 @@ class RemoteFileClient extends FriendicaClient {
|
|||
}
|
||||
request.fields['desc'] = description;
|
||||
request.fields['album'] = album;
|
||||
request.files.add(await http.MultipartFile.fromBytes(
|
||||
request.fields.addAll(visibility.toMapEntries());
|
||||
request.files.add(http.MultipartFile.fromBytes(
|
||||
'media',
|
||||
filename: fileName,
|
||||
contentType:
|
||||
|
@ -642,6 +647,7 @@ class StatusesClient extends FriendicaClient {
|
|||
String spoilerText = '',
|
||||
String inReplyToId = '',
|
||||
List<String> mediaIds = const [],
|
||||
required Visibility visibility,
|
||||
}) async {
|
||||
_logger.finest(() =>
|
||||
'Creating status ${inReplyToId.isNotEmpty ? "In Reply to: " : ""} $inReplyToId, with media: $mediaIds');
|
||||
|
@ -651,7 +657,7 @@ class StatusesClient extends FriendicaClient {
|
|||
if (spoilerText.isNotEmpty) 'spoiler_text': spoilerText,
|
||||
if (inReplyToId.isNotEmpty) 'in_reply_to_id': inReplyToId,
|
||||
if (mediaIds.isNotEmpty) 'media_ids': mediaIds,
|
||||
'visibility': 'unlisted',
|
||||
'visibility': visibility.toCreateStatusValue(),
|
||||
'friendica': {
|
||||
'title': '',
|
||||
},
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
class GroupData {
|
||||
static final followersPseudoGroup = GroupData('~', 'Followers');
|
||||
|
||||
final String id;
|
||||
|
||||
@override
|
||||
|
|
|
@ -1,6 +1,16 @@
|
|||
enum VisibilityType {
|
||||
public,
|
||||
private,
|
||||
;
|
||||
|
||||
String toLabel() {
|
||||
switch (this) {
|
||||
case VisibilityType.public:
|
||||
return 'Public';
|
||||
case VisibilityType.private:
|
||||
return 'Private';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Visibility {
|
||||
|
@ -16,9 +26,9 @@ class Visibility {
|
|||
|
||||
bool get hasDetails =>
|
||||
allowedUserIds.isNotEmpty ||
|
||||
excludedUserIds.isNotEmpty ||
|
||||
allowedGroupIds.isNotEmpty ||
|
||||
excludedGroupIds.isNotEmpty;
|
||||
excludedUserIds.isNotEmpty ||
|
||||
allowedGroupIds.isNotEmpty ||
|
||||
excludedGroupIds.isNotEmpty;
|
||||
|
||||
const Visibility({
|
||||
required this.type,
|
||||
|
@ -28,24 +38,26 @@ class Visibility {
|
|||
this.excludedGroupIds = const [],
|
||||
});
|
||||
|
||||
factory Visibility.public() => const Visibility(
|
||||
factory Visibility.public() =>
|
||||
const Visibility(
|
||||
type: VisibilityType.public,
|
||||
);
|
||||
|
||||
factory Visibility.private() => const Visibility(
|
||||
factory Visibility.private() =>
|
||||
const Visibility(
|
||||
type: VisibilityType.private,
|
||||
);
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is Visibility &&
|
||||
runtimeType == other.runtimeType &&
|
||||
type == other.type &&
|
||||
allowedUserIds == other.allowedUserIds &&
|
||||
excludedUserIds == other.excludedUserIds &&
|
||||
allowedGroupIds == other.allowedGroupIds &&
|
||||
excludedGroupIds == other.excludedGroupIds;
|
||||
other is Visibility &&
|
||||
runtimeType == other.runtimeType &&
|
||||
type == other.type &&
|
||||
allowedUserIds == other.allowedUserIds &&
|
||||
excludedUserIds == other.excludedUserIds &&
|
||||
allowedGroupIds == other.allowedGroupIds &&
|
||||
excludedGroupIds == other.excludedGroupIds;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/material.dart' hide Visibility;
|
||||
import 'package:flutter_widget_from_html_core/flutter_widget_from_html_core.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
@ -15,10 +15,12 @@ import '../controls/padding.dart';
|
|||
import '../controls/standard_appbar.dart';
|
||||
import '../controls/timeline/status_header_control.dart';
|
||||
import '../globals.dart';
|
||||
import '../models/group_data.dart';
|
||||
import '../models/image_entry.dart';
|
||||
import '../models/link_preview_data.dart';
|
||||
import '../models/media_attachment_uploads/new_entry_media_items.dart';
|
||||
import '../models/timeline_entry.dart';
|
||||
import '../models/visibility.dart';
|
||||
import '../serializers/friendica/link_preview_friendica_extensions.dart';
|
||||
import '../services/feature_version_checker.dart';
|
||||
import '../services/timeline_manager.dart';
|
||||
|
@ -51,6 +53,8 @@ class _EditorScreenState extends State<EditorScreen> {
|
|||
final newMediaItems = NewEntryMediaItems();
|
||||
final existingMediaItems = <ImageEntry>[];
|
||||
final focusNode = FocusNode();
|
||||
Visibility visibility = Visibility.public();
|
||||
GroupData? currentGroup;
|
||||
|
||||
var isSubmitting = false;
|
||||
|
||||
|
@ -100,6 +104,7 @@ class _EditorScreenState extends State<EditorScreen> {
|
|||
if (entry.linkPreviewData?.link.isNotEmpty ?? false) {
|
||||
restoreLinkPreviewData(entry.linkPreviewData!);
|
||||
}
|
||||
visibility = entry.visibility;
|
||||
setState(() {
|
||||
loaded = true;
|
||||
});
|
||||
|
@ -152,6 +157,7 @@ class _EditorScreenState extends State<EditorScreen> {
|
|||
inReplyToId: widget.parentId,
|
||||
newMediaItems: newMediaItems,
|
||||
existingMediaItems: existingMediaItems,
|
||||
visibility: visibility,
|
||||
);
|
||||
setState(() {
|
||||
isSubmitting = false;
|
||||
|
@ -184,6 +190,7 @@ class _EditorScreenState extends State<EditorScreen> {
|
|||
inReplyToId: widget.parentId,
|
||||
newMediaItems: newMediaItems,
|
||||
existingMediaItems: existingMediaItems,
|
||||
newMediaItemVisibility: visibility,
|
||||
);
|
||||
setState(() {
|
||||
isSubmitting = false;
|
||||
|
@ -251,11 +258,16 @@ class _EditorScreenState extends State<EditorScreen> {
|
|||
),
|
||||
),
|
||||
const VerticalPadding(),
|
||||
buildVisibilitySelector(context),
|
||||
const VerticalPadding(),
|
||||
buildContentField(context),
|
||||
const VerticalPadding(),
|
||||
buildLinkWithPreview(context),
|
||||
const VerticalPadding(),
|
||||
GallerySelectorControl(entries: existingMediaItems),
|
||||
GallerySelectorControl(
|
||||
entries: existingMediaItems,
|
||||
visibilityFilter: visibility,
|
||||
),
|
||||
const VerticalPadding(),
|
||||
MediaUploadsControl(
|
||||
entryMediaItems: newMediaItems,
|
||||
|
@ -532,6 +544,80 @@ class _EditorScreenState extends State<EditorScreen> {
|
|||
);
|
||||
}
|
||||
|
||||
Widget buildVisibilitySelector(BuildContext context) {
|
||||
final groups = context
|
||||
.watch<ActiveProfileSelector<TimelineManager>>()
|
||||
.activeEntry
|
||||
.andThen((tm) => tm.getGroups())
|
||||
.getValueOrElse(() => []);
|
||||
groups.sort((g1, g2) => g1.name.compareTo(g2.name));
|
||||
|
||||
final groupMenuItems = <DropdownMenuEntry<GroupData>>[];
|
||||
groupMenuItems.add(DropdownMenuEntry(
|
||||
value: GroupData.followersPseudoGroup,
|
||||
label: GroupData.followersPseudoGroup.name));
|
||||
groupMenuItems.add(DropdownMenuEntry(
|
||||
value: GroupData('', ''), label: '-', enabled: false));
|
||||
groupMenuItems.addAll(groups.map((g) => DropdownMenuEntry(
|
||||
value: g,
|
||||
label: g.name,
|
||||
)));
|
||||
if (!groups.contains(currentGroup)) {
|
||||
currentGroup = null;
|
||||
}
|
||||
return Row(
|
||||
children: [
|
||||
const Text('Visibility:'),
|
||||
const HorizontalPadding(),
|
||||
DropdownMenu<VisibilityType>(
|
||||
initialSelection: visibility.type,
|
||||
enabled: !widget.forEditing,
|
||||
onSelected: (value) {
|
||||
setState(() {
|
||||
if (value == VisibilityType.public) {
|
||||
visibility = Visibility.public();
|
||||
return;
|
||||
}
|
||||
|
||||
if (value == VisibilityType.private && currentGroup == null) {
|
||||
visibility = Visibility.private();
|
||||
return;
|
||||
}
|
||||
|
||||
visibility = Visibility(
|
||||
type: VisibilityType.private,
|
||||
allowedGroupIds: [currentGroup!.id],
|
||||
);
|
||||
});
|
||||
},
|
||||
dropdownMenuEntries: VisibilityType.values
|
||||
.map((v) => DropdownMenuEntry(
|
||||
value: v,
|
||||
label: v.toLabel(),
|
||||
))
|
||||
.toList(),
|
||||
),
|
||||
const HorizontalPadding(),
|
||||
if (visibility.type == VisibilityType.private)
|
||||
DropdownMenu<GroupData>(
|
||||
enabled: !widget.forEditing,
|
||||
initialSelection: currentGroup,
|
||||
onSelected: (value) {
|
||||
setState(() {
|
||||
currentGroup = value;
|
||||
visibility = Visibility(
|
||||
type: VisibilityType.private,
|
||||
allowedGroupIds:
|
||||
currentGroup == null ? [] : [currentGroup!.id],
|
||||
);
|
||||
});
|
||||
},
|
||||
dropdownMenuEntries: groupMenuItems,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void updateLinkPreviewThumbnail(LinkPreviewData preview, int increment) {
|
||||
var currentIndex =
|
||||
preview.availableImageUrls.indexOf(preview.selectedImageUrl) +
|
||||
|
|
|
@ -1,15 +1,23 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/material.dart' hide Visibility;
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../controls/login_aware_cached_network_image.dart';
|
||||
import '../models/gallery_data.dart';
|
||||
import '../models/image_entry.dart';
|
||||
import '../models/visibility.dart';
|
||||
import '../serializers/friendica/image_entry_friendica_extensions.dart';
|
||||
import '../services/gallery_service.dart';
|
||||
import '../utils/active_profile_selector.dart';
|
||||
import 'image_viewer_screen.dart';
|
||||
|
||||
class ExistingImageSelectorScreen extends StatefulWidget {
|
||||
final Visibility? visibilityFilter;
|
||||
|
||||
const ExistingImageSelectorScreen({
|
||||
super.key,
|
||||
this.visibilityFilter,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ExistingImageSelectorScreen> createState() =>
|
||||
_ExistingImageSelectorScreenState();
|
||||
|
@ -102,7 +110,13 @@ class _ExistingImageSelectorScreenState
|
|||
);
|
||||
final List<ImageEntry> images = selectedGallery == null
|
||||
? []
|
||||
: service.getGalleryImageList(galleryName).getValueOrElse(() => []);
|
||||
: service
|
||||
.getGalleryImageList(galleryName)
|
||||
.getValueOrElse(() => [])
|
||||
.where((i) => widget.visibilityFilter == null
|
||||
? true
|
||||
: i.visibility.type == widget.visibilityFilter!.type)
|
||||
.toList();
|
||||
return GridView.builder(
|
||||
itemCount: images.length,
|
||||
padding: const EdgeInsets.all(5.0),
|
||||
|
@ -118,23 +132,30 @@ class _ExistingImageSelectorScreenState
|
|||
nextPageOnly: true,
|
||||
);
|
||||
}
|
||||
final imageWidget = LoginAwareCachedNetworkImage(
|
||||
imageUrl: image.thumbnailUrl,
|
||||
);
|
||||
final selected = selectedImages.contains(image);
|
||||
|
||||
final tileWidget = selected
|
||||
? Stack(
|
||||
children: [
|
||||
imageWidget,
|
||||
Positioned(
|
||||
child: Icon(
|
||||
Icons.check_circle,
|
||||
color: Colors.green,
|
||||
))
|
||||
],
|
||||
)
|
||||
: imageWidget;
|
||||
final tileWidget = Card(
|
||||
child: Stack(
|
||||
children: [
|
||||
LoginAwareCachedNetworkImage(
|
||||
imageUrl: image.thumbnailUrl,
|
||||
),
|
||||
if (selected)
|
||||
const Positioned(
|
||||
child: Icon(
|
||||
Icons.check_circle,
|
||||
color: Colors.green,
|
||||
)),
|
||||
Positioned(
|
||||
bottom: 5.0,
|
||||
right: 5.0,
|
||||
child: Icon(image.visibility.type == VisibilityType.public
|
||||
? Icons.public
|
||||
: Icons.lock),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(2.0),
|
||||
child: GestureDetector(
|
||||
|
|
|
@ -28,6 +28,19 @@ extension VisibilityFriendicaExtensions on Visibility {
|
|||
);
|
||||
}
|
||||
|
||||
Map<String, String> toMapEntries() {
|
||||
return {
|
||||
'allow_cid': _idsListToAclString(allowedUserIds),
|
||||
'deny_cid': _idsListToAclString(excludedUserIds),
|
||||
'allow_gid': _idsListToAclString(allowedGroupIds),
|
||||
'deny_gid': _idsListToAclString(excludedGroupIds),
|
||||
};
|
||||
}
|
||||
|
||||
static List<String> _parseAcl(String? acl) =>
|
||||
acl?.split(RegExp('[><]')).where((e) => e.isNotEmpty).toList() ?? [];
|
||||
|
||||
String _idsListToAclString(List<String> ids) {
|
||||
return ids.map((id) => '<$id>').join('');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
import '../../models/visibility.dart';
|
||||
|
||||
extension VisibilityMastodonExtensions on Visibility {
|
||||
String toCreateStatusValue() {
|
||||
if (type == VisibilityType.public) {
|
||||
return 'public';
|
||||
}
|
||||
|
||||
if (hasDetails) {
|
||||
return allowedGroupIds.first;
|
||||
}
|
||||
|
||||
return 'private';
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/material.dart' hide Visibility;
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:result_monad/result_monad.dart';
|
||||
|
@ -12,6 +12,7 @@ import '../models/exec_error.dart';
|
|||
import '../models/image_entry.dart';
|
||||
import '../models/media_attachment_uploads/new_entry_media_items.dart';
|
||||
import '../models/timeline_entry.dart';
|
||||
import '../models/visibility.dart';
|
||||
import 'auth_service.dart';
|
||||
import 'media_upload_attachment_helper.dart';
|
||||
|
||||
|
@ -85,6 +86,7 @@ class EntryManagerService extends ChangeNotifier {
|
|||
String inReplyToId = '',
|
||||
required NewEntryMediaItems mediaItems,
|
||||
required List<ImageEntry> existingMediaItems,
|
||||
required Visibility visibility,
|
||||
}) async {
|
||||
_logger.finest('Creating new post: $text');
|
||||
final mediaIds = existingMediaItems.map((m) => m.scales.first.id).toList();
|
||||
|
@ -118,6 +120,7 @@ class EntryManagerService extends ChangeNotifier {
|
|||
album: mediaItems.albumName,
|
||||
description: item.description,
|
||||
fileName: filename,
|
||||
visibility: visibility,
|
||||
),
|
||||
);
|
||||
if (uploadResult.isSuccess) {
|
||||
|
@ -134,7 +137,8 @@ class EntryManagerService extends ChangeNotifier {
|
|||
text: text,
|
||||
spoilerText: spoilerText,
|
||||
inReplyToId: inReplyToId,
|
||||
mediaIds: mediaIds)
|
||||
mediaIds: mediaIds,
|
||||
visibility: visibility)
|
||||
.andThenSuccessAsync((item) async {
|
||||
await processNewItems(
|
||||
[item], getIt<AccountsService>().currentProfile.username, null);
|
||||
|
@ -172,6 +176,7 @@ class EntryManagerService extends ChangeNotifier {
|
|||
String spoilerText = '',
|
||||
required NewEntryMediaItems mediaItems,
|
||||
required List<ImageEntry> existingMediaItems,
|
||||
required Visibility newMediaItemVisibility,
|
||||
}) async {
|
||||
_logger.finest('Editing post: $text');
|
||||
final mediaIds = existingMediaItems
|
||||
|
@ -203,11 +208,11 @@ class EntryManagerService extends ChangeNotifier {
|
|||
(imageBytes) async =>
|
||||
await RemoteFileClient(getIt<AccountsService>().currentProfile)
|
||||
.uploadFileAsAttachment(
|
||||
bytes: imageBytes,
|
||||
album: mediaItems.albumName,
|
||||
description: item.description,
|
||||
fileName: filename,
|
||||
),
|
||||
bytes: imageBytes,
|
||||
album: mediaItems.albumName,
|
||||
description: item.description,
|
||||
fileName: filename,
|
||||
visibility: newMediaItemVisibility),
|
||||
);
|
||||
if (uploadResult.isSuccess) {
|
||||
mediaIds.add(uploadResult.value.scales.first.id);
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/material.dart' hide Visibility;
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:result_monad/result_monad.dart';
|
||||
|
||||
|
@ -13,6 +13,7 @@ import '../models/image_entry.dart';
|
|||
import '../models/media_attachment_uploads/new_entry_media_items.dart';
|
||||
import '../models/timeline.dart';
|
||||
import '../models/timeline_entry.dart';
|
||||
import '../models/visibility.dart';
|
||||
import 'auth_service.dart';
|
||||
import 'entry_manager_service.dart';
|
||||
|
||||
|
@ -69,6 +70,7 @@ class TimelineManager extends ChangeNotifier {
|
|||
String inReplyToId = '',
|
||||
required NewEntryMediaItems newMediaItems,
|
||||
required List<ImageEntry> existingMediaItems,
|
||||
required Visibility visibility,
|
||||
}) async {
|
||||
final result = await entryManagerService.createNewStatus(
|
||||
text,
|
||||
|
@ -76,6 +78,7 @@ class TimelineManager extends ChangeNotifier {
|
|||
inReplyToId: inReplyToId,
|
||||
mediaItems: newMediaItems,
|
||||
existingMediaItems: existingMediaItems,
|
||||
visibility: visibility,
|
||||
);
|
||||
if (result.isSuccess) {
|
||||
_logger.finest('Notifying listeners of new status created');
|
||||
|
@ -91,14 +94,13 @@ class TimelineManager extends ChangeNotifier {
|
|||
String inReplyToId = '',
|
||||
required NewEntryMediaItems newMediaItems,
|
||||
required List<ImageEntry> existingMediaItems,
|
||||
required Visibility newMediaItemVisibility,
|
||||
}) async {
|
||||
final result = await entryManagerService.editStatus(
|
||||
id,
|
||||
text,
|
||||
spoilerText: spoilerText,
|
||||
mediaItems: newMediaItems,
|
||||
existingMediaItems: existingMediaItems,
|
||||
);
|
||||
final result = await entryManagerService.editStatus(id, text,
|
||||
spoilerText: spoilerText,
|
||||
mediaItems: newMediaItems,
|
||||
existingMediaItems: existingMediaItems,
|
||||
newMediaItemVisibility: newMediaItemVisibility);
|
||||
if (result.isSuccess) {
|
||||
_logger.finest('Notifying listeners of updated status');
|
||||
notifyListeners();
|
||||
|
|
Ładowanie…
Reference in New Issue