Add initial visibility configuration in posts

codemagic-setup
Hank Grabowski 2023-03-20 21:55:47 -04:00
rodzic aafe6dea7c
commit 8617e21d1a
10 zmienionych plików z 221 dodań i 51 usunięć

Wyświetl plik

@ -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();

Wyświetl plik

@ -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': '',
},

Wyświetl plik

@ -1,4 +1,6 @@
class GroupData {
static final followersPseudoGroup = GroupData('~', 'Followers');
final String id;
@override

Wyświetl plik

@ -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 =>

Wyświetl plik

@ -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) +

Wyświetl plik

@ -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(

Wyświetl plik

@ -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('');
}
}

Wyświetl plik

@ -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';
}
}

Wyświetl plik

@ -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);

Wyświetl plik

@ -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();