kopia lustrzana https://gitlab.com/mysocialportal/relatica
Merge branch 'status-privacy-overhaul' into 'main'
Status privacy overhaul See merge request mysocialportal/relatica!48main
commit
e3967b89bc
|
@ -5,7 +5,7 @@ import 'package:logging/logging.dart';
|
||||||
import '../../globals.dart';
|
import '../../globals.dart';
|
||||||
import '../../models/connection.dart';
|
import '../../models/connection.dart';
|
||||||
import '../../models/timeline_entry.dart';
|
import '../../models/timeline_entry.dart';
|
||||||
import '../../models/visibility.dart';
|
import '../../models/visibility.dart' as v;
|
||||||
import '../../routes.dart';
|
import '../../routes.dart';
|
||||||
import '../../services/auth_service.dart';
|
import '../../services/auth_service.dart';
|
||||||
import '../../services/connections_manager.dart';
|
import '../../services/connections_manager.dart';
|
||||||
|
@ -14,6 +14,7 @@ import '../../utils/active_profile_selector.dart';
|
||||||
import '../../utils/dateutils.dart';
|
import '../../utils/dateutils.dart';
|
||||||
import '../image_control.dart';
|
import '../image_control.dart';
|
||||||
import '../padding.dart';
|
import '../padding.dart';
|
||||||
|
import '../visibility_dialog.dart';
|
||||||
|
|
||||||
class StatusHeaderControl extends StatelessWidget {
|
class StatusHeaderControl extends StatelessWidget {
|
||||||
static final _logger = Logger('$StatusHeaderControl');
|
static final _logger = Logger('$StatusHeaderControl');
|
||||||
|
@ -39,22 +40,22 @@ class StatusHeaderControl extends StatelessWidget {
|
||||||
.getForProfile(activeProfile)
|
.getForProfile(activeProfile)
|
||||||
.transform((s) => s.getForPost(entry.id)?.resharers.firstOrNull)
|
.transform((s) => s.getForPost(entry.id)?.resharers.firstOrNull)
|
||||||
.getValueOrElse(() => null);
|
.getValueOrElse(() => null);
|
||||||
|
final manager = getIt<ActiveProfileSelector<ConnectionsManager>>()
|
||||||
getIt<ActiveProfileSelector<ConnectionsManager>>()
|
|
||||||
.getForProfile(activeProfile)
|
.getForProfile(activeProfile)
|
||||||
.match(
|
.fold(
|
||||||
onSuccess: (manager) {
|
onSuccess: (m) => m,
|
||||||
author =
|
onError: (error) {
|
||||||
manager.getById(entry.authorId).getValueOrElse(() => Connection());
|
_logger.severe('Error getting connection manager: $error');
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
author = manager!.getById(entry.authorId).getValueOrElse(
|
||||||
|
() => Connection(),
|
||||||
|
);
|
||||||
reshareAuthor = reshareId == null
|
reshareAuthor = reshareId == null
|
||||||
? Connection()
|
? Connection()
|
||||||
: manager.getById(reshareId).getValueOrElse(() => Connection());
|
: manager.getById(reshareId).getValueOrElse(
|
||||||
},
|
() => Connection(),
|
||||||
onError: (error) {
|
|
||||||
_logger.severe('Error getting connections manageR: $error');
|
|
||||||
author = Connection();
|
|
||||||
reshareAuthor = Connection();
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
|
@ -122,14 +123,18 @@ class StatusHeaderControl extends StatelessWidget {
|
||||||
ElapsedDateUtils.epochSecondsToString(entry.backdatedTimestamp),
|
ElapsedDateUtils.epochSecondsToString(entry.backdatedTimestamp),
|
||||||
style: Theme.of(context).textTheme.bodySmall,
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
),
|
),
|
||||||
const HorizontalPadding(),
|
IconButton(
|
||||||
Icon(
|
onPressed: () async {
|
||||||
entry.visibility.type == VisibilityType.public
|
await showVisibilityDialog(context, manager, entry.visibility);
|
||||||
|
},
|
||||||
|
icon: Icon(
|
||||||
|
entry.visibility.type == v.VisibilityType.public
|
||||||
? Icons.public
|
? Icons.public
|
||||||
: Icons.lock,
|
: Icons.lock,
|
||||||
color: Theme.of(context).hintColor,
|
color: Theme.of(context).hintColor,
|
||||||
size: Theme.of(context).textTheme.bodySmall?.fontSize,
|
size: Theme.of(context).textTheme.bodySmall?.fontSize,
|
||||||
),
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
@ -0,0 +1,146 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import '../../models/visibility.dart' as v;
|
||||||
|
import '../services/connections_manager.dart';
|
||||||
|
|
||||||
|
Future<bool?> showVisibilityDialog(
|
||||||
|
BuildContext context,
|
||||||
|
ConnectionsManager cm,
|
||||||
|
v.Visibility visibility,
|
||||||
|
) async {
|
||||||
|
final circlesMap = {for (var item in cm.getMyCircles()) item.id: item};
|
||||||
|
|
||||||
|
final allowedCircles = visibility.allowedCircleIds.map((c) {
|
||||||
|
if (c == '~') {
|
||||||
|
return 'Followers';
|
||||||
|
}
|
||||||
|
|
||||||
|
return circlesMap[c]?.name ?? 'Circle #$c';
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
final excludedCircles = visibility.excludedCircleIds.map((c) {
|
||||||
|
if (c == '~') {
|
||||||
|
return 'Followers';
|
||||||
|
}
|
||||||
|
|
||||||
|
return circlesMap[c]?.name ?? 'Circle #$c';
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
final allowedUsers = visibility.allowedUserIds
|
||||||
|
.map(
|
||||||
|
(u) => cm.getById(u).fold(
|
||||||
|
onSuccess: (connection) => connection.handle,
|
||||||
|
onError: (_) => 'User $u',
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
final excludedUsers = visibility.excludedUserIds
|
||||||
|
.map(
|
||||||
|
(u) => cm.getById(u).fold(
|
||||||
|
onSuccess: (connection) => connection.handle,
|
||||||
|
onError: (_) => 'User $u',
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
return showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
barrierDismissible: true,
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
return AlertDialog(
|
||||||
|
content: SizedBox(
|
||||||
|
width: MediaQuery.of(context).size.width * 0.8,
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Visibility Details',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.bodyLarge
|
||||||
|
?.copyWith(decoration: TextDecoration.underline),
|
||||||
|
),
|
||||||
|
if (visibility.type == v.VisibilityType.public) ...[
|
||||||
|
const Text('Public')
|
||||||
|
],
|
||||||
|
if (visibility.type != v.VisibilityType.public) ...[
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
'Allowed Users: ',
|
||||||
|
style: TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
allowedUsers.isEmpty
|
||||||
|
? 'Empty'
|
||||||
|
: allowedUsers.join(', '),
|
||||||
|
softWrap: true,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
'Allowed Circles: ',
|
||||||
|
style: TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
allowedCircles.isEmpty
|
||||||
|
? 'Empty'
|
||||||
|
: allowedCircles.join(','),
|
||||||
|
softWrap: true,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
'Excluded Users: ',
|
||||||
|
style: TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
excludedUsers.isEmpty ? 'Empty' : excludedUsers.join(','),
|
||||||
|
softWrap: true,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
'Excluded Circles: ',
|
||||||
|
style: TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
excludedCircles.isEmpty
|
||||||
|
? 'Empty'
|
||||||
|
: excludedCircles.join(','),
|
||||||
|
softWrap: true,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: <Widget>[
|
||||||
|
ElevatedButton(
|
||||||
|
child: const Text('Dismiss'),
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.pop(context, true); // showDialog() returns true
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
|
@ -843,7 +843,7 @@ class StatusesClient extends FriendicaClient {
|
||||||
if (spoilerText.isNotEmpty) 'spoiler_text': spoilerText,
|
if (spoilerText.isNotEmpty) 'spoiler_text': spoilerText,
|
||||||
if (inReplyToId.isNotEmpty) 'in_reply_to_id': inReplyToId,
|
if (inReplyToId.isNotEmpty) 'in_reply_to_id': inReplyToId,
|
||||||
if (mediaIds.isNotEmpty) 'media_ids': mediaIds,
|
if (mediaIds.isNotEmpty) 'media_ids': mediaIds,
|
||||||
'visibility': visibility.toCreateStatusValue(),
|
'visibility': visibility.toCreateStatusValue(inReplyToId.isNotEmpty),
|
||||||
'friendica': {
|
'friendica': {
|
||||||
'title': '',
|
'title': '',
|
||||||
},
|
},
|
||||||
|
|
|
@ -15,6 +15,7 @@ import '../controls/login_aware_cached_network_image.dart';
|
||||||
import '../controls/padding.dart';
|
import '../controls/padding.dart';
|
||||||
import '../controls/standard_appbar.dart';
|
import '../controls/standard_appbar.dart';
|
||||||
import '../controls/timeline/status_header_control.dart';
|
import '../controls/timeline/status_header_control.dart';
|
||||||
|
import '../controls/visibility_dialog.dart';
|
||||||
import '../globals.dart';
|
import '../globals.dart';
|
||||||
import '../models/circle_data.dart';
|
import '../models/circle_data.dart';
|
||||||
import '../models/exec_error.dart';
|
import '../models/exec_error.dart';
|
||||||
|
@ -24,6 +25,7 @@ import '../models/media_attachment_uploads/new_entry_media_items.dart';
|
||||||
import '../models/timeline_entry.dart';
|
import '../models/timeline_entry.dart';
|
||||||
import '../models/visibility.dart';
|
import '../models/visibility.dart';
|
||||||
import '../serializers/friendica/link_preview_friendica_extensions.dart';
|
import '../serializers/friendica/link_preview_friendica_extensions.dart';
|
||||||
|
import '../services/connections_manager.dart';
|
||||||
import '../services/feature_version_checker.dart';
|
import '../services/feature_version_checker.dart';
|
||||||
import '../services/timeline_manager.dart';
|
import '../services/timeline_manager.dart';
|
||||||
import '../utils/active_profile_selector.dart';
|
import '../utils/active_profile_selector.dart';
|
||||||
|
@ -566,13 +568,22 @@ class _EditorScreenState extends State<EditorScreen> {
|
||||||
|
|
||||||
Widget buildVisibilitySelector(BuildContext context) {
|
Widget buildVisibilitySelector(BuildContext context) {
|
||||||
if (widget.forEditing || widget.parentId.isNotEmpty) {
|
if (widget.forEditing || widget.parentId.isNotEmpty) {
|
||||||
|
final cm = context
|
||||||
|
.read<ActiveProfileSelector<ConnectionsManager>>()
|
||||||
|
.activeEntry
|
||||||
|
.value;
|
||||||
|
|
||||||
return Row(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
const Text('Visibility:'),
|
const Text('Visibility:'),
|
||||||
const HorizontalPadding(),
|
IconButton(
|
||||||
visibility.type == VisibilityType.public
|
onPressed: () async {
|
||||||
|
await showVisibilityDialog(context, cm, visibility);
|
||||||
|
},
|
||||||
|
icon: visibility.type == VisibilityType.public
|
||||||
? const Icon(Icons.public)
|
? const Icon(Icons.public)
|
||||||
: const Icon(Icons.lock),
|
: const Icon(Icons.lock),
|
||||||
|
)
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -583,34 +594,37 @@ class _EditorScreenState extends State<EditorScreen> {
|
||||||
.getValueOrElse(() => []);
|
.getValueOrElse(() => []);
|
||||||
circles.sort((g1, g2) => g1.name.compareTo(g2.name));
|
circles.sort((g1, g2) => g1.name.compareTo(g2.name));
|
||||||
|
|
||||||
final circleMenuItems = <DropdownMenuEntry<CircleData>>[];
|
final circleMenuItems = <DropdownMenuItem<CircleData>>[];
|
||||||
circleMenuItems.add(DropdownMenuEntry(
|
circleMenuItems.add(DropdownMenuItem(
|
||||||
value: CircleData.followersPseudoCircle,
|
value: CircleData.followersPseudoCircle,
|
||||||
label: CircleData.followersPseudoCircle.name));
|
child: Text(CircleData.followersPseudoCircle.name)));
|
||||||
circleMenuItems.add(DropdownMenuEntry(
|
circleMenuItems.add(DropdownMenuItem(
|
||||||
value: CircleData('', ''), label: '-', enabled: false));
|
value: CircleData('', ''), child: const Divider(), enabled: false));
|
||||||
circleMenuItems.addAll(circles.map((g) => DropdownMenuEntry(
|
circleMenuItems.addAll(circles.map((g) => DropdownMenuItem(
|
||||||
value: g,
|
value: g,
|
||||||
label: g.name,
|
child: Text(g.name),
|
||||||
)));
|
)));
|
||||||
if (!circles.contains(currentCircle)) {
|
if (currentCircle != CircleData.followersPseudoCircle &&
|
||||||
|
!circles.contains(currentCircle)) {
|
||||||
currentCircle = null;
|
currentCircle = null;
|
||||||
}
|
}
|
||||||
return Row(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
const Text('Visibility:'),
|
const Text('Visibility:'),
|
||||||
const HorizontalPadding(),
|
const HorizontalPadding(),
|
||||||
DropdownMenu<VisibilityType>(
|
DropdownButton<VisibilityType>(
|
||||||
initialSelection: visibility.type,
|
value: visibility.type,
|
||||||
enabled: !widget.forEditing,
|
onChanged: widget.forEditing
|
||||||
onSelected: (value) {
|
? null
|
||||||
|
: (value) {
|
||||||
setState(() {
|
setState(() {
|
||||||
if (value == VisibilityType.public) {
|
if (value == VisibilityType.public) {
|
||||||
visibility = Visibility.public();
|
visibility = Visibility.public();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (value == VisibilityType.private && currentCircle == null) {
|
if (value == VisibilityType.private &&
|
||||||
|
currentCircle == null) {
|
||||||
visibility = Visibility.private();
|
visibility = Visibility.private();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -621,19 +635,22 @@ class _EditorScreenState extends State<EditorScreen> {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
dropdownMenuEntries: VisibilityType.values
|
items: VisibilityType.values
|
||||||
.map((v) => DropdownMenuEntry(
|
.map((v) => DropdownMenuItem(
|
||||||
value: v,
|
value: v,
|
||||||
label: v.toLabel(),
|
child: Text(v.toLabel()),
|
||||||
))
|
))
|
||||||
.toList(),
|
.toList(),
|
||||||
),
|
),
|
||||||
const HorizontalPadding(),
|
const HorizontalPadding(),
|
||||||
if (visibility.type == VisibilityType.private)
|
if (visibility.type == VisibilityType.private)
|
||||||
DropdownMenu<CircleData>(
|
Expanded(
|
||||||
enabled: !widget.forEditing,
|
child: DropdownButton<CircleData>(
|
||||||
initialSelection: currentCircle,
|
value: currentCircle,
|
||||||
onSelected: (value) {
|
isExpanded: true,
|
||||||
|
onChanged: widget.forEditing
|
||||||
|
? null
|
||||||
|
: (value) {
|
||||||
setState(() {
|
setState(() {
|
||||||
currentCircle = value;
|
currentCircle = value;
|
||||||
visibility = Visibility(
|
visibility = Visibility(
|
||||||
|
@ -643,7 +660,8 @@ class _EditorScreenState extends State<EditorScreen> {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
dropdownMenuEntries: circleMenuItems,
|
items: circleMenuItems,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
|
@ -71,9 +71,33 @@ extension TimelineEntryMastodonExtensions on TimelineEntry {
|
||||||
: 0;
|
: 0;
|
||||||
|
|
||||||
final youReshared = json['reblogged'] ?? false;
|
final youReshared = json['reblogged'] ?? false;
|
||||||
final visibility = ['public', 'unlisted'].contains(json['visibility'])
|
late final Visibility visibility;
|
||||||
? Visibility.public()
|
final visibilityString = json['visibility'];
|
||||||
: Visibility.private();
|
if (visibilityString == 'public') {
|
||||||
|
visibility = Visibility.public();
|
||||||
|
} else if (visibilityString == 'private') {
|
||||||
|
final allowedUserIds =
|
||||||
|
json['friendica']?['visibility']?['allow_cid'] as List<dynamic>? ??
|
||||||
|
[];
|
||||||
|
final excludedUserIds =
|
||||||
|
json['friendica']?['visibility']?['deny_cid'] as List<dynamic>? ?? [];
|
||||||
|
final allowedCircleIds =
|
||||||
|
json['friendica']?['visibility']?['allow_gid'] as List<dynamic>? ??
|
||||||
|
[];
|
||||||
|
final excludedCircleIds =
|
||||||
|
json['friendica']?['visibility']?['deny_gid'] as List<dynamic>? ?? [];
|
||||||
|
visibility = Visibility(
|
||||||
|
type: VisibilityType.private,
|
||||||
|
allowedUserIds: allowedUserIds.map((e) => e.toString()).toList(),
|
||||||
|
excludedUserIds: excludedUserIds.map((e) => e.toString()).toList(),
|
||||||
|
allowedCircleIds: allowedCircleIds.map((e) => e.toString()).toList(),
|
||||||
|
excludedCircleIds: excludedCircleIds.map((e) => e.toString()).toList(),
|
||||||
|
);
|
||||||
|
} else if (visibilityString == 'unlisted') {
|
||||||
|
visibility = Visibility.private();
|
||||||
|
} else {
|
||||||
|
visibility = Visibility.private();
|
||||||
|
}
|
||||||
|
|
||||||
const title = '';
|
const title = '';
|
||||||
final body = json['content'] ?? '';
|
final body = json['content'] ?? '';
|
||||||
|
|
|
@ -3,20 +3,34 @@ import 'package:relatica/models/circle_data.dart';
|
||||||
import '../../models/visibility.dart';
|
import '../../models/visibility.dart';
|
||||||
|
|
||||||
extension VisibilityMastodonExtensions on Visibility {
|
extension VisibilityMastodonExtensions on Visibility {
|
||||||
String toCreateStatusValue() {
|
String toCreateStatusValue(bool onComment) {
|
||||||
if (type == VisibilityType.public) {
|
if (type == VisibilityType.public) {
|
||||||
return 'public';
|
return 'public';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasDetails) {
|
if (!onComment && hasDetails) {
|
||||||
final circleId = allowedCircleIds.first;
|
final circleId =
|
||||||
|
allowedCircleIds.firstOrNull ?? allowedUserIds.firstOrNull;
|
||||||
if (circleId == CircleData.followersPseudoCircle.id) {
|
if (circleId == CircleData.followersPseudoCircle.id) {
|
||||||
return 'private';
|
return 'private';
|
||||||
}
|
}
|
||||||
|
|
||||||
return circleId;
|
return circleId ?? 'private';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (onComment && !hasDetails && type == VisibilityType.private) {
|
||||||
|
return 'direct';
|
||||||
}
|
}
|
||||||
|
|
||||||
return 'private';
|
return 'private';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> friendicaExtensionVisibilityJson() {
|
||||||
|
return {
|
||||||
|
"allow_cid": allowedUserIds,
|
||||||
|
"deny_cid": excludedUserIds,
|
||||||
|
"allow_gid": allowedCircleIds,
|
||||||
|
"deny_gid": excludedCircleIds,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Ładowanie…
Reference in New Issue