Merge branch 'status-privacy-overhaul' into 'main'

Status privacy overhaul

See merge request mysocialportal/relatica!48
codemagic-setup
HankG 2023-11-18 04:20:49 +00:00
commit e3967b89bc
6 zmienionych plików z 287 dodań i 80 usunięć

Wyświetl plik

@ -5,7 +5,7 @@ import 'package:logging/logging.dart';
import '../../globals.dart';
import '../../models/connection.dart';
import '../../models/timeline_entry.dart';
import '../../models/visibility.dart';
import '../../models/visibility.dart' as v;
import '../../routes.dart';
import '../../services/auth_service.dart';
import '../../services/connections_manager.dart';
@ -14,6 +14,7 @@ import '../../utils/active_profile_selector.dart';
import '../../utils/dateutils.dart';
import '../image_control.dart';
import '../padding.dart';
import '../visibility_dialog.dart';
class StatusHeaderControl extends StatelessWidget {
static final _logger = Logger('$StatusHeaderControl');
@ -39,23 +40,23 @@ class StatusHeaderControl extends StatelessWidget {
.getForProfile(activeProfile)
.transform((s) => s.getForPost(entry.id)?.resharers.firstOrNull)
.getValueOrElse(() => null);
getIt<ActiveProfileSelector<ConnectionsManager>>()
final manager = getIt<ActiveProfileSelector<ConnectionsManager>>()
.getForProfile(activeProfile)
.match(
onSuccess: (manager) {
author =
manager.getById(entry.authorId).getValueOrElse(() => Connection());
reshareAuthor = reshareId == null
? Connection()
: manager.getById(reshareId).getValueOrElse(() => Connection());
},
onError: (error) {
_logger.severe('Error getting connections manageR: $error');
author = Connection();
reshareAuthor = Connection();
},
);
.fold(
onSuccess: (m) => m,
onError: (error) {
_logger.severe('Error getting connection manager: $error');
return null;
});
author = manager!.getById(entry.authorId).getValueOrElse(
() => Connection(),
);
reshareAuthor = reshareId == null
? Connection()
: manager.getById(reshareId).getValueOrElse(
() => Connection(),
);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
@ -122,13 +123,17 @@ class StatusHeaderControl extends StatelessWidget {
ElapsedDateUtils.epochSecondsToString(entry.backdatedTimestamp),
style: Theme.of(context).textTheme.bodySmall,
),
const HorizontalPadding(),
Icon(
entry.visibility.type == VisibilityType.public
? Icons.public
: Icons.lock,
color: Theme.of(context).hintColor,
size: Theme.of(context).textTheme.bodySmall?.fontSize,
IconButton(
onPressed: () async {
await showVisibilityDialog(context, manager, entry.visibility);
},
icon: Icon(
entry.visibility.type == v.VisibilityType.public
? Icons.public
: Icons.lock,
color: Theme.of(context).hintColor,
size: Theme.of(context).textTheme.bodySmall?.fontSize,
),
),
],
),

Wyświetl plik

@ -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
},
),
],
);
},
);
}

Wyświetl plik

@ -843,7 +843,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': visibility.toCreateStatusValue(),
'visibility': visibility.toCreateStatusValue(inReplyToId.isNotEmpty),
'friendica': {
'title': '',
},

Wyświetl plik

@ -15,6 +15,7 @@ import '../controls/login_aware_cached_network_image.dart';
import '../controls/padding.dart';
import '../controls/standard_appbar.dart';
import '../controls/timeline/status_header_control.dart';
import '../controls/visibility_dialog.dart';
import '../globals.dart';
import '../models/circle_data.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/visibility.dart';
import '../serializers/friendica/link_preview_friendica_extensions.dart';
import '../services/connections_manager.dart';
import '../services/feature_version_checker.dart';
import '../services/timeline_manager.dart';
import '../utils/active_profile_selector.dart';
@ -566,13 +568,22 @@ class _EditorScreenState extends State<EditorScreen> {
Widget buildVisibilitySelector(BuildContext context) {
if (widget.forEditing || widget.parentId.isNotEmpty) {
final cm = context
.read<ActiveProfileSelector<ConnectionsManager>>()
.activeEntry
.value;
return Row(
children: [
const Text('Visibility:'),
const HorizontalPadding(),
visibility.type == VisibilityType.public
? const Icon(Icons.public)
: const Icon(Icons.lock),
IconButton(
onPressed: () async {
await showVisibilityDialog(context, cm, visibility);
},
icon: visibility.type == VisibilityType.public
? const Icon(Icons.public)
: const Icon(Icons.lock),
)
],
);
}
@ -583,67 +594,74 @@ class _EditorScreenState extends State<EditorScreen> {
.getValueOrElse(() => []);
circles.sort((g1, g2) => g1.name.compareTo(g2.name));
final circleMenuItems = <DropdownMenuEntry<CircleData>>[];
circleMenuItems.add(DropdownMenuEntry(
final circleMenuItems = <DropdownMenuItem<CircleData>>[];
circleMenuItems.add(DropdownMenuItem(
value: CircleData.followersPseudoCircle,
label: CircleData.followersPseudoCircle.name));
circleMenuItems.add(DropdownMenuEntry(
value: CircleData('', ''), label: '-', enabled: false));
circleMenuItems.addAll(circles.map((g) => DropdownMenuEntry(
child: Text(CircleData.followersPseudoCircle.name)));
circleMenuItems.add(DropdownMenuItem(
value: CircleData('', ''), child: const Divider(), enabled: false));
circleMenuItems.addAll(circles.map((g) => DropdownMenuItem(
value: g,
label: g.name,
child: Text(g.name),
)));
if (!circles.contains(currentCircle)) {
if (currentCircle != CircleData.followersPseudoCircle &&
!circles.contains(currentCircle)) {
currentCircle = 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;
}
DropdownButton<VisibilityType>(
value: visibility.type,
onChanged: widget.forEditing
? null
: (value) {
setState(() {
if (value == VisibilityType.public) {
visibility = Visibility.public();
return;
}
if (value == VisibilityType.private && currentCircle == null) {
visibility = Visibility.private();
return;
}
if (value == VisibilityType.private &&
currentCircle == null) {
visibility = Visibility.private();
return;
}
visibility = Visibility(
type: VisibilityType.private,
allowedCircleIds: [currentCircle!.id],
);
});
},
dropdownMenuEntries: VisibilityType.values
.map((v) => DropdownMenuEntry(
visibility = Visibility(
type: VisibilityType.private,
allowedCircleIds: [currentCircle!.id],
);
});
},
items: VisibilityType.values
.map((v) => DropdownMenuItem(
value: v,
label: v.toLabel(),
child: Text(v.toLabel()),
))
.toList(),
),
const HorizontalPadding(),
if (visibility.type == VisibilityType.private)
DropdownMenu<CircleData>(
enabled: !widget.forEditing,
initialSelection: currentCircle,
onSelected: (value) {
setState(() {
currentCircle = value;
visibility = Visibility(
type: VisibilityType.private,
allowedCircleIds:
currentCircle == null ? [] : [currentCircle!.id],
);
});
},
dropdownMenuEntries: circleMenuItems,
Expanded(
child: DropdownButton<CircleData>(
value: currentCircle,
isExpanded: true,
onChanged: widget.forEditing
? null
: (value) {
setState(() {
currentCircle = value;
visibility = Visibility(
type: VisibilityType.private,
allowedCircleIds:
currentCircle == null ? [] : [currentCircle!.id],
);
});
},
items: circleMenuItems,
),
),
],
);

Wyświetl plik

@ -71,9 +71,33 @@ extension TimelineEntryMastodonExtensions on TimelineEntry {
: 0;
final youReshared = json['reblogged'] ?? false;
final visibility = ['public', 'unlisted'].contains(json['visibility'])
? Visibility.public()
: Visibility.private();
late final Visibility visibility;
final visibilityString = json['visibility'];
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 = '';
final body = json['content'] ?? '';

Wyświetl plik

@ -3,20 +3,34 @@ import 'package:relatica/models/circle_data.dart';
import '../../models/visibility.dart';
extension VisibilityMastodonExtensions on Visibility {
String toCreateStatusValue() {
String toCreateStatusValue(bool onComment) {
if (type == VisibilityType.public) {
return 'public';
}
if (hasDetails) {
final circleId = allowedCircleIds.first;
if (!onComment && hasDetails) {
final circleId =
allowedCircleIds.firstOrNull ?? allowedUserIds.firstOrNull;
if (circleId == CircleData.followersPseudoCircle.id) {
return 'private';
}
return circleId;
return circleId ?? 'private';
}
if (onComment && !hasDetails && type == VisibilityType.private) {
return 'direct';
}
return 'private';
}
Map<String, dynamic> friendicaExtensionVisibilityJson() {
return {
"allow_cid": allowedUserIds,
"deny_cid": excludedUserIds,
"allow_gid": allowedCircleIds,
"deny_gid": excludedCircleIds,
};
}
}