mirror of
https://gitlab.com/mysocialportal/relatica
synced 2024-10-18 15:53:32 +00:00
Merge branch 'threads-bsky-othernet-updates' into 'main'
Threads Bluesky and Other Network Updates See merge request mysocialportal/relatica!57
This commit is contained in:
commit
8207e0b260
24 changed files with 723 additions and 78 deletions
20
CHANGELOG.md
20
CHANGELOG.md
|
@ -1,5 +1,25 @@
|
||||||
# Relatica Change Log
|
# Relatica Change Log
|
||||||
|
|
||||||
|
## Version 0.11.0 (beta)
|
||||||
|
|
||||||
|
* Changes
|
||||||
|
* Fixes
|
||||||
|
* Fixes Unlisted posts are showing as and sharing as
|
||||||
|
private ([Issue #78](https://gitlab.com/mysocialportal/relatica/-/issues/78))
|
||||||
|
* Fixes trailing CW type ([Issue #90](https://gitlab.com/mysocialportal/relatica/-/issues/90))
|
||||||
|
* Fix Threads open status in browser
|
||||||
|
errors ([Issue #87](https://gitlab.com/mysocialportal/relatica/-/issues/87))
|
||||||
|
* Fix Bluesky open status in browser
|
||||||
|
errors ([Issue #79](https://gitlab.com/mysocialportal/relatica/-/issues/79))
|
||||||
|
* Fix Threads profiles don't open in browser
|
||||||
|
properly ([Issue #89](https://gitlab.com/mysocialportal/relatica/-/issues/89))
|
||||||
|
* Fix Bluesky profiles don't open in browser
|
||||||
|
properly ([Issue #98](https://gitlab.com/mysocialportal/relatica/-/issues/98))
|
||||||
|
* New Features
|
||||||
|
* Shows the network of the post/comment ([Feature #82](https://gitlab.com/mysocialportal/relatica/-/issues/82))
|
||||||
|
* User configurable ability to limit reacting to, commenting on, or resharing posts by network
|
||||||
|
type([Feature #93](https://gitlab.com/mysocialportal/relatica/-/issues/93))
|
||||||
|
|
||||||
## Version 0.10.1 (beta)
|
## Version 0.10.1 (beta)
|
||||||
|
|
||||||
* Changes
|
* Changes
|
||||||
|
|
3
devtools_options.yaml
Normal file
3
devtools_options.yaml
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
description: This file stores settings for Dart & Flutter DevTools.
|
||||||
|
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
|
||||||
|
extensions:
|
BIN
fonts/forkawesome-webfont.ttf
Normal file
BIN
fonts/forkawesome-webfont.ttf
Normal file
Binary file not shown.
|
@ -172,7 +172,7 @@ class _StatusControlState extends State<FlattenedTreeEntryControl> {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
child: Text(
|
child: Text(
|
||||||
'Content Summary: ${entry.spoilerText} (Click to ${showContent ? "Hide" : "Show"}}')),
|
'Content Summary: ${entry.spoilerText} (Click to ${showContent ? "Hide" : "Show"})')),
|
||||||
if (showContent) ...[
|
if (showContent) ...[
|
||||||
buildContentField(context),
|
buildContentField(context),
|
||||||
const VerticalPadding(
|
const VerticalPadding(
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:relatica/models/exec_error.dart';
|
|
||||||
import 'package:result_monad/result_monad.dart';
|
import 'package:result_monad/result_monad.dart';
|
||||||
|
|
||||||
import '../../globals.dart';
|
import '../../globals.dart';
|
||||||
|
import '../../models/exec_error.dart';
|
||||||
import '../../models/timeline_entry.dart';
|
import '../../models/timeline_entry.dart';
|
||||||
import '../../models/visibility.dart' as v;
|
|
||||||
import '../../services/feature_version_checker.dart';
|
import '../../services/feature_version_checker.dart';
|
||||||
import '../../services/fediverse_server_validator.dart';
|
import '../../services/fediverse_server_validator.dart';
|
||||||
import '../../services/timeline_manager.dart';
|
import '../../services/timeline_manager.dart';
|
||||||
import '../../utils/active_profile_selector.dart';
|
import '../../utils/active_profile_selector.dart';
|
||||||
|
import '../../utils/interaction_availability_util.dart';
|
||||||
import '../../utils/snackbar_builder.dart';
|
import '../../utils/snackbar_builder.dart';
|
||||||
|
|
||||||
class InteractionsBarControl extends StatefulWidget {
|
class InteractionsBarControl extends StatefulWidget {
|
||||||
|
@ -74,6 +74,7 @@ class _InteractionsBarControlState extends State<InteractionsBarControl> {
|
||||||
isProcessing = true;
|
isProcessing = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// TODO Add can reshare check
|
||||||
final fvc = getIt<FriendicaVersionChecker>();
|
final fvc = getIt<FriendicaVersionChecker>();
|
||||||
if (!fvc.canUseFeature(RelaticaFeatures.diasporaReshare)) {
|
if (!fvc.canUseFeature(RelaticaFeatures.diasporaReshare)) {
|
||||||
final serverTypeEstimate = await getIt<FediverseServiceValidator>()
|
final serverTypeEstimate = await getIt<FediverseServiceValidator>()
|
||||||
|
@ -160,39 +161,39 @@ class _InteractionsBarControlState extends State<InteractionsBarControl> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget buildLikeButton() {
|
Widget buildLikeButton() {
|
||||||
|
final canReact = widget.entry.getCanReact();
|
||||||
|
final tooltip =
|
||||||
|
canReact.canDo ? 'Press to toggle like/unlike' : canReact.reason;
|
||||||
return buildButton(
|
return buildButton(
|
||||||
isFavorited ? Icons.thumb_up : Icons.thumb_up_outlined,
|
isFavorited ? Icons.thumb_up : Icons.thumb_up_outlined,
|
||||||
likes,
|
likes,
|
||||||
true,
|
true,
|
||||||
'Press to toggle like/unlike',
|
tooltip,
|
||||||
() async => await toggleFavorited(),
|
canReact.canDo ? () async => await toggleFavorited() : null,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget buildCommentButton() {
|
Widget buildCommentButton() {
|
||||||
|
final canComment = widget.entry.getCanComment();
|
||||||
|
final tooltip =
|
||||||
|
canComment.canDo ? 'Press to add a comment' : canComment.reason;
|
||||||
return buildButton(
|
return buildButton(
|
||||||
Icons.comment,
|
Icons.comment,
|
||||||
comments,
|
comments,
|
||||||
true,
|
true,
|
||||||
'Press to add a comment',
|
tooltip,
|
||||||
() async => await addComment(),
|
canComment.canDo ? () async => await addComment() : null,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget buildReshareButton() {
|
Widget buildReshareButton() {
|
||||||
final canReshare = !widget.isMine &&
|
final reshareable = widget.entry.getIsReshareable(widget.isMine);
|
||||||
widget.entry.visibility.type == v.VisibilityType.public;
|
final canReshare = reshareable.canDo;
|
||||||
late final String tooltip;
|
late final String tooltip;
|
||||||
if (canReshare) {
|
if (canReshare) {
|
||||||
tooltip = youReshared ? 'Press to undo reshare' : 'Press to reshare';
|
tooltip = youReshared ? 'Press to undo reshare' : 'Press to reshare';
|
||||||
} else {
|
} else {
|
||||||
if (widget.isMine) {
|
tooltip = reshareable.reason;
|
||||||
tooltip = "Can't reshare your own post";
|
|
||||||
} else if (widget.entry.visibility.type != v.VisibilityType.public) {
|
|
||||||
tooltip = "Can't reshare a private post";
|
|
||||||
} else {
|
|
||||||
tooltip = "Can't reshare at this time";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return buildButton(
|
return buildButton(
|
||||||
youReshared ? Icons.repeat_on_outlined : Icons.repeat,
|
youReshared ? Icons.repeat_on_outlined : Icons.repeat,
|
||||||
|
|
|
@ -15,6 +15,7 @@ import '../../utils/dateutils.dart';
|
||||||
import '../image_control.dart';
|
import '../image_control.dart';
|
||||||
import '../padding.dart';
|
import '../padding.dart';
|
||||||
import '../visibility_dialog.dart';
|
import '../visibility_dialog.dart';
|
||||||
|
import 'timeline_network_info_control.dart';
|
||||||
|
|
||||||
class StatusHeaderControl extends StatelessWidget {
|
class StatusHeaderControl extends StatelessWidget {
|
||||||
static final _logger = Logger('$StatusHeaderControl');
|
static final _logger = Logger('$StatusHeaderControl');
|
||||||
|
@ -124,17 +125,22 @@ class StatusHeaderControl extends StatelessWidget {
|
||||||
style: Theme.of(context).textTheme.bodySmall,
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
|
tooltip:
|
||||||
|
'Visibility: ${entry.visibility.type.toLabel()} (click for details)',
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
await showVisibilityDialog(context, manager, entry.visibility);
|
await showVisibilityDialog(context, manager, entry.visibility);
|
||||||
},
|
},
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
entry.visibility.type == v.VisibilityType.public
|
switch (entry.visibility.type) {
|
||||||
? Icons.public
|
v.VisibilityType.public => Icons.public,
|
||||||
: Icons.lock,
|
v.VisibilityType.private => Icons.lock,
|
||||||
|
v.VisibilityType.unlisted => Icons.not_interested,
|
||||||
|
},
|
||||||
color: Theme.of(context).hintColor,
|
color: Theme.of(context).hintColor,
|
||||||
size: Theme.of(context).textTheme.bodySmall?.fontSize,
|
size: Theme.of(context).textTheme.bodySmall?.fontSize,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
TimelineNetworkInfoControl(info: entry.networkInfo),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
27
lib/controls/timeline/timeline_network_info_control.dart
Normal file
27
lib/controls/timeline/timeline_network_info_control.dart
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import '../../globals.dart';
|
||||||
|
import '../../models/timeline_network_info.dart';
|
||||||
|
import '../../utils/known_network_extensions.dart';
|
||||||
|
|
||||||
|
class TimelineNetworkInfoControl extends StatelessWidget {
|
||||||
|
final TimelineNetworkInfo info;
|
||||||
|
|
||||||
|
const TimelineNetworkInfoControl({super.key, required this.info});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final networkText =
|
||||||
|
info.network == KnownNetworks.unknown ? info.name : info.labelName;
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () async => showConfirmDialog(context, networkText),
|
||||||
|
child: Tooltip(
|
||||||
|
message: networkText,
|
||||||
|
child: Text(
|
||||||
|
info.forkAwesomeUnicode,
|
||||||
|
style: const TextStyle(fontFamily: 'ForkAwesome'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -66,6 +66,9 @@ Future<bool?> showVisibilityDialog(
|
||||||
if (visibility.type == v.VisibilityType.public) ...[
|
if (visibility.type == v.VisibilityType.public) ...[
|
||||||
const Text('Public')
|
const Text('Public')
|
||||||
],
|
],
|
||||||
|
if (visibility.type == v.VisibilityType.unlisted) ...[
|
||||||
|
const Text('Unlisted')
|
||||||
|
],
|
||||||
if (visibility.type != v.VisibilityType.public) ...[
|
if (visibility.type != v.VisibilityType.public) ...[
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
|
121
lib/models/settings/network_capabilities_settings.dart
Normal file
121
lib/models/settings/network_capabilities_settings.dart
Normal file
|
@ -0,0 +1,121 @@
|
||||||
|
import '../timeline_network_info.dart';
|
||||||
|
|
||||||
|
class NetworkCapabilitiesSettings {
|
||||||
|
late final List<NetworkCapabilitiesItem> _capabilities;
|
||||||
|
late final Map<KnownNetworks, NetworkCapabilitiesItem> _capabilitiesKV;
|
||||||
|
|
||||||
|
num get length => _capabilities.length;
|
||||||
|
|
||||||
|
NetworkCapabilitiesSettings(
|
||||||
|
{required List<NetworkCapabilitiesItem> capabilities}) {
|
||||||
|
_capabilities = capabilities;
|
||||||
|
_capabilitiesKV = {for (final c in capabilities) c.network: c};
|
||||||
|
}
|
||||||
|
|
||||||
|
NetworkCapabilitiesItem operator [](int i) => _capabilities[i];
|
||||||
|
|
||||||
|
NetworkCapabilitiesItem getCapabilities(KnownNetworks network) =>
|
||||||
|
_capabilitiesKV[network] ?? NetworkCapabilitiesItem.unknown();
|
||||||
|
|
||||||
|
operator []=(int i, NetworkCapabilitiesItem item) {
|
||||||
|
_capabilities[i] = item;
|
||||||
|
_capabilitiesKV[item.network] = item;
|
||||||
|
}
|
||||||
|
|
||||||
|
factory NetworkCapabilitiesSettings.defaultSettings() {
|
||||||
|
final networks = <KnownNetworks>{};
|
||||||
|
networks.add(KnownNetworks.friendica);
|
||||||
|
networks.add(KnownNetworks.mastodon);
|
||||||
|
networks.add(KnownNetworks.threads);
|
||||||
|
networks.add(KnownNetworks.bluesky);
|
||||||
|
networks.add(KnownNetworks.diaspora);
|
||||||
|
networks.add(KnownNetworks.pixelfed);
|
||||||
|
networks.add(KnownNetworks.peertube);
|
||||||
|
networks.add(KnownNetworks.unknown);
|
||||||
|
networks.addAll(KnownNetworks.values);
|
||||||
|
final capabilities = networks
|
||||||
|
.map((e) => switch (e) {
|
||||||
|
KnownNetworks.activityPub => NetworkCapabilitiesItem(
|
||||||
|
network: e,
|
||||||
|
react: true,
|
||||||
|
reshare: true,
|
||||||
|
comment: true,
|
||||||
|
),
|
||||||
|
KnownNetworks.bluesky => NetworkCapabilitiesItem(
|
||||||
|
network: e,
|
||||||
|
react: true,
|
||||||
|
reshare: false,
|
||||||
|
comment: true,
|
||||||
|
),
|
||||||
|
KnownNetworks.threads => NetworkCapabilitiesItem(
|
||||||
|
network: e,
|
||||||
|
react: true,
|
||||||
|
reshare: false,
|
||||||
|
comment: false,
|
||||||
|
),
|
||||||
|
_ => NetworkCapabilitiesItem(
|
||||||
|
network: e,
|
||||||
|
react: true,
|
||||||
|
reshare: true,
|
||||||
|
comment: true,
|
||||||
|
),
|
||||||
|
})
|
||||||
|
.toList();
|
||||||
|
return NetworkCapabilitiesSettings(capabilities: capabilities);
|
||||||
|
}
|
||||||
|
|
||||||
|
factory NetworkCapabilitiesSettings.fromJson(List<dynamic> json) {
|
||||||
|
final capabilities =
|
||||||
|
json.map((j) => NetworkCapabilitiesItem.fromJson(j)).toList();
|
||||||
|
return NetworkCapabilitiesSettings(capabilities: capabilities);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Map<String, dynamic>> toJson() {
|
||||||
|
return _capabilities.map((c) => c.toJson()).toList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class NetworkCapabilitiesItem {
|
||||||
|
final KnownNetworks network;
|
||||||
|
final bool react;
|
||||||
|
final bool reshare;
|
||||||
|
final bool comment;
|
||||||
|
|
||||||
|
NetworkCapabilitiesItem({
|
||||||
|
required this.network,
|
||||||
|
required this.react,
|
||||||
|
required this.reshare,
|
||||||
|
required this.comment,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory NetworkCapabilitiesItem.fromJson(Map<String, dynamic> json) =>
|
||||||
|
NetworkCapabilitiesItem(
|
||||||
|
network: KnownNetworks.parse(json['network']),
|
||||||
|
react: json['react'] ?? true,
|
||||||
|
reshare: json['reshare'] ?? true,
|
||||||
|
comment: json['comment'] ?? true,
|
||||||
|
);
|
||||||
|
|
||||||
|
factory NetworkCapabilitiesItem.unknown() => NetworkCapabilitiesItem(
|
||||||
|
network: KnownNetworks.unknown,
|
||||||
|
react: true,
|
||||||
|
reshare: true,
|
||||||
|
comment: true,
|
||||||
|
);
|
||||||
|
|
||||||
|
NetworkCapabilitiesItem copyWith(
|
||||||
|
{bool? react, bool? reshare, bool? comment}) =>
|
||||||
|
NetworkCapabilitiesItem(
|
||||||
|
network: network,
|
||||||
|
react: react ?? this.react,
|
||||||
|
reshare: reshare ?? this.reshare,
|
||||||
|
comment: comment ?? this.comment,
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => {
|
||||||
|
'network': network.name,
|
||||||
|
'react': react,
|
||||||
|
'reshare': reshare,
|
||||||
|
'comment': comment,
|
||||||
|
};
|
||||||
|
}
|
|
@ -5,6 +5,7 @@ import 'link_data.dart';
|
||||||
import 'link_preview_data.dart';
|
import 'link_preview_data.dart';
|
||||||
import 'location_data.dart';
|
import 'location_data.dart';
|
||||||
import 'media_attachment.dart';
|
import 'media_attachment.dart';
|
||||||
|
import 'timeline_network_info.dart';
|
||||||
import 'visibility.dart';
|
import 'visibility.dart';
|
||||||
|
|
||||||
class TimelineEntry {
|
class TimelineEntry {
|
||||||
|
@ -52,10 +53,12 @@ class TimelineEntry {
|
||||||
|
|
||||||
final List<MediaAttachment> mediaAttachments;
|
final List<MediaAttachment> mediaAttachments;
|
||||||
|
|
||||||
final EngagementSummary engagementSummary;
|
final TimelineNetworkInfo networkInfo;
|
||||||
|
|
||||||
final LinkPreviewData? linkPreviewData;
|
final LinkPreviewData? linkPreviewData;
|
||||||
|
|
||||||
|
final EngagementSummary engagementSummary;
|
||||||
|
|
||||||
TimelineEntry(
|
TimelineEntry(
|
||||||
{this.id = '',
|
{this.id = '',
|
||||||
this.parentId = '',
|
this.parentId = '',
|
||||||
|
@ -80,6 +83,7 @@ class TimelineEntry {
|
||||||
this.dislikes = const [],
|
this.dislikes = const [],
|
||||||
this.mediaAttachments = const [],
|
this.mediaAttachments = const [],
|
||||||
this.engagementSummary = const EngagementSummary(),
|
this.engagementSummary = const EngagementSummary(),
|
||||||
|
this.networkInfo = TimelineNetworkInfo.empty,
|
||||||
this.linkPreviewData})
|
this.linkPreviewData})
|
||||||
: visibility = visibility ?? Visibility.public();
|
: visibility = visibility ?? Visibility.public();
|
||||||
|
|
||||||
|
@ -108,6 +112,7 @@ class TimelineEntry {
|
||||||
likes = [],
|
likes = [],
|
||||||
dislikes = [],
|
dislikes = [],
|
||||||
mediaAttachments = [],
|
mediaAttachments = [],
|
||||||
|
networkInfo = TimelineNetworkInfo.empty,
|
||||||
engagementSummary = const EngagementSummary(),
|
engagementSummary = const EngagementSummary(),
|
||||||
linkPreviewData = LinkPreviewData(link: 'fake link');
|
linkPreviewData = LinkPreviewData(link: 'fake link');
|
||||||
|
|
||||||
|
@ -138,6 +143,7 @@ class TimelineEntry {
|
||||||
List<Connection>? dislikes,
|
List<Connection>? dislikes,
|
||||||
List<MediaAttachment>? mediaAttachments,
|
List<MediaAttachment>? mediaAttachments,
|
||||||
EngagementSummary? engagementSummary,
|
EngagementSummary? engagementSummary,
|
||||||
|
TimelineNetworkInfo? networkInfo,
|
||||||
LinkPreviewData? linkPreviewData,
|
LinkPreviewData? linkPreviewData,
|
||||||
}) {
|
}) {
|
||||||
return TimelineEntry(
|
return TimelineEntry(
|
||||||
|
@ -165,6 +171,7 @@ class TimelineEntry {
|
||||||
dislikes: dislikes ?? this.dislikes,
|
dislikes: dislikes ?? this.dislikes,
|
||||||
mediaAttachments: mediaAttachments ?? this.mediaAttachments,
|
mediaAttachments: mediaAttachments ?? this.mediaAttachments,
|
||||||
engagementSummary: engagementSummary ?? this.engagementSummary,
|
engagementSummary: engagementSummary ?? this.engagementSummary,
|
||||||
|
networkInfo: networkInfo ?? this.networkInfo,
|
||||||
linkPreviewData: linkPreviewData ?? this.linkPreviewData,
|
linkPreviewData: linkPreviewData ?? this.linkPreviewData,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -205,6 +212,7 @@ class TimelineEntry {
|
||||||
likes == other.likes &&
|
likes == other.likes &&
|
||||||
dislikes == other.dislikes &&
|
dislikes == other.dislikes &&
|
||||||
mediaAttachments == other.mediaAttachments &&
|
mediaAttachments == other.mediaAttachments &&
|
||||||
|
networkInfo == other.networkInfo &&
|
||||||
engagementSummary == other.engagementSummary;
|
engagementSummary == other.engagementSummary;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -231,5 +239,6 @@ class TimelineEntry {
|
||||||
likes.hashCode ^
|
likes.hashCode ^
|
||||||
dislikes.hashCode ^
|
dislikes.hashCode ^
|
||||||
mediaAttachments.hashCode ^
|
mediaAttachments.hashCode ^
|
||||||
|
networkInfo.hashCode ^
|
||||||
engagementSummary.hashCode;
|
engagementSummary.hashCode;
|
||||||
}
|
}
|
||||||
|
|
68
lib/models/timeline_network_info.dart
Normal file
68
lib/models/timeline_network_info.dart
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
enum KnownNetworks {
|
||||||
|
activityPub,
|
||||||
|
bluesky,
|
||||||
|
calckey,
|
||||||
|
diaspora,
|
||||||
|
drupal,
|
||||||
|
firefish,
|
||||||
|
friendica,
|
||||||
|
funkwhale,
|
||||||
|
gnu_social,
|
||||||
|
hubzilla,
|
||||||
|
kbin,
|
||||||
|
lemmy,
|
||||||
|
mastodon,
|
||||||
|
nextcloud,
|
||||||
|
peertube,
|
||||||
|
pixelfed,
|
||||||
|
pleroma,
|
||||||
|
plume,
|
||||||
|
red,
|
||||||
|
redmatrix,
|
||||||
|
socialhome,
|
||||||
|
threads,
|
||||||
|
wordpress,
|
||||||
|
unknown,
|
||||||
|
;
|
||||||
|
|
||||||
|
static KnownNetworks parse(String? text) {
|
||||||
|
if (text == null) {
|
||||||
|
return unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
return values.firstWhere(
|
||||||
|
(e) => e.name == text,
|
||||||
|
orElse: () => unknown,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TimelineNetworkInfo {
|
||||||
|
static const empty = TimelineNetworkInfo(
|
||||||
|
name: 'Unknown',
|
||||||
|
vapidKey: '',
|
||||||
|
network: KnownNetworks.unknown,
|
||||||
|
);
|
||||||
|
|
||||||
|
final String name;
|
||||||
|
final String vapidKey;
|
||||||
|
final KnownNetworks network;
|
||||||
|
|
||||||
|
const TimelineNetworkInfo({
|
||||||
|
required this.name,
|
||||||
|
required this.vapidKey,
|
||||||
|
required this.network,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) =>
|
||||||
|
identical(this, other) ||
|
||||||
|
other is TimelineNetworkInfo &&
|
||||||
|
runtimeType == other.runtimeType &&
|
||||||
|
name == other.name &&
|
||||||
|
vapidKey == other.vapidKey &&
|
||||||
|
network == other.network;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => name.hashCode ^ vapidKey.hashCode ^ network.hashCode;
|
||||||
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
enum VisibilityType {
|
enum VisibilityType {
|
||||||
public,
|
public,
|
||||||
private,
|
private,
|
||||||
|
unlisted,
|
||||||
;
|
;
|
||||||
|
|
||||||
String toLabel() {
|
String toLabel() {
|
||||||
|
@ -9,6 +10,8 @@ enum VisibilityType {
|
||||||
return 'Public';
|
return 'Public';
|
||||||
case VisibilityType.private:
|
case VisibilityType.private:
|
||||||
return 'Private';
|
return 'Private';
|
||||||
|
case VisibilityType.unlisted:
|
||||||
|
return 'Unlisted';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -46,6 +49,10 @@ class Visibility {
|
||||||
type: VisibilityType.private,
|
type: VisibilityType.private,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
factory Visibility.unlisted() => const Visibility(
|
||||||
|
type: VisibilityType.unlisted,
|
||||||
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) =>
|
bool operator ==(Object other) =>
|
||||||
identical(this, other) ||
|
identical(this, other) ||
|
||||||
|
|
|
@ -583,10 +583,11 @@ class _EditorScreenState extends State<EditorScreen> {
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
await showVisibilityDialog(context, cm, visibility);
|
await showVisibilityDialog(context, cm, visibility);
|
||||||
},
|
},
|
||||||
icon: visibility.type == VisibilityType.public
|
icon: Icon(switch (visibility.type) {
|
||||||
? const Icon(Icons.public)
|
VisibilityType.public => Icons.public,
|
||||||
: const Icon(Icons.lock),
|
VisibilityType.private => Icons.lock,
|
||||||
)
|
VisibilityType.unlisted => Icons.not_interested,
|
||||||
|
}))
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -632,6 +633,12 @@ class _EditorScreenState extends State<EditorScreen> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (value == VisibilityType.unlisted &&
|
||||||
|
currentCircle == null) {
|
||||||
|
visibility = Visibility.unlisted();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
visibility = Visibility(
|
visibility = Visibility(
|
||||||
type: VisibilityType.private,
|
type: VisibilityType.private,
|
||||||
allowedCircleIds: [currentCircle!.id],
|
allowedCircleIds: [currentCircle!.id],
|
||||||
|
|
|
@ -12,6 +12,7 @@ import '../di_initialization.dart';
|
||||||
import '../globals.dart';
|
import '../globals.dart';
|
||||||
import '../routes.dart';
|
import '../routes.dart';
|
||||||
import '../services/setting_service.dart';
|
import '../services/setting_service.dart';
|
||||||
|
import '../utils/known_network_extensions.dart';
|
||||||
import '../utils/theme_mode_extensions.dart';
|
import '../utils/theme_mode_extensions.dart';
|
||||||
|
|
||||||
class SettingsScreen extends StatelessWidget {
|
class SettingsScreen extends StatelessWidget {
|
||||||
|
@ -34,6 +35,7 @@ class SettingsScreen extends StatelessWidget {
|
||||||
if (!kReleaseMode) buildColorBlindnessTestSettings(settings),
|
if (!kReleaseMode) buildColorBlindnessTestSettings(settings),
|
||||||
buildClearCaches(context),
|
buildClearCaches(context),
|
||||||
buildLogPanel(context, settings),
|
buildLogPanel(context, settings),
|
||||||
|
const NetworkCapabilitiesWidget(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -138,3 +140,77 @@ class SettingsScreen extends StatelessWidget {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class NetworkCapabilitiesWidget extends StatelessWidget {
|
||||||
|
const NetworkCapabilitiesWidget({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final settings = context.watch<SettingsService>();
|
||||||
|
final nc = settings.networkCapabilities;
|
||||||
|
final rows = <DataRow>[];
|
||||||
|
|
||||||
|
for (int i = 0; i < nc.length; i++) {
|
||||||
|
final e = nc[i];
|
||||||
|
final r = DataRow(
|
||||||
|
cells: [
|
||||||
|
DataCell(
|
||||||
|
Text(
|
||||||
|
'${e.network.forkAwesomeUnicode} ${e.network.labelName}',
|
||||||
|
style: const TextStyle(fontFamily: 'ForkAwesome'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
DataCell(
|
||||||
|
Checkbox(
|
||||||
|
value: e.react,
|
||||||
|
onChanged: (bool? value) {
|
||||||
|
nc[i] = e.copyWith(react: value ?? false);
|
||||||
|
settings.networkCapabilities = nc;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
DataCell(
|
||||||
|
Checkbox(
|
||||||
|
value: e.reshare,
|
||||||
|
onChanged: (bool? value) {
|
||||||
|
nc[i] = e.copyWith(reshare: value ?? false);
|
||||||
|
settings.networkCapabilities = nc;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
DataCell(
|
||||||
|
Checkbox(
|
||||||
|
value: e.comment,
|
||||||
|
onChanged: (bool? value) {
|
||||||
|
nc[i] = e.copyWith(comment: value ?? false);
|
||||||
|
settings.networkCapabilities = nc;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
rows.add(r);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ListTile(
|
||||||
|
title: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text('Network Capabilities'),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: null,
|
||||||
|
child: Text('Reset to Defaults'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
subtitle: DataTable(
|
||||||
|
columns: [
|
||||||
|
DataColumn(label: Text('Network')),
|
||||||
|
DataColumn(label: Text('React')),
|
||||||
|
DataColumn(label: Text('Reshare')),
|
||||||
|
DataColumn(label: Text('Comment')),
|
||||||
|
],
|
||||||
|
rows: rows,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -7,7 +7,7 @@ extension ConnectionMastodonExtensions on Connection {
|
||||||
{String defaultServerName = ''}) {
|
{String defaultServerName = ''}) {
|
||||||
final name = json['display_name'] ?? '';
|
final name = json['display_name'] ?? '';
|
||||||
final id = json['id']?.toString() ?? '';
|
final id = json['id']?.toString() ?? '';
|
||||||
final profileUrl = Uri.parse(json['url'] ?? '');
|
var profileUrl = Uri.parse(json['url'] ?? '');
|
||||||
const network = 'Unknown';
|
const network = 'Unknown';
|
||||||
final avatar = Uri.tryParse(json['avatar_static'] ?? '') ?? Uri();
|
final avatar = Uri.tryParse(json['avatar_static'] ?? '') ?? Uri();
|
||||||
final String handleFromJson = json['acct'];
|
final String handleFromJson = json['acct'];
|
||||||
|
@ -20,11 +20,23 @@ extension ConnectionMastodonExtensions on Connection {
|
||||||
late final String handle;
|
late final String handle;
|
||||||
if (handleFromJson.contains('@')) {
|
if (handleFromJson.contains('@')) {
|
||||||
handle = handleFromJson;
|
handle = handleFromJson;
|
||||||
|
final handleElements = handleFromJson.split('@');
|
||||||
|
if (handleElements.last == 'threads.net') {
|
||||||
|
profileUrl =
|
||||||
|
Uri.parse('https://www.threads.net/@${handleElements.first}');
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
final server = defaultServerName.isNotEmpty
|
final server = defaultServerName.isNotEmpty
|
||||||
? defaultServerName
|
? defaultServerName
|
||||||
: getIt<AccountsService>().currentProfile.serverName;
|
: getIt<AccountsService>().currentProfile.serverName;
|
||||||
handle = '$handleFromJson@$server';
|
handle = '$handleFromJson@$server';
|
||||||
|
if (server == 'threads.net') {
|
||||||
|
profileUrl = Uri.parse('https://www.threads.net/@$handleFromJson');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (profileUrl.scheme == 'did') {
|
||||||
|
profileUrl = Uri.parse('https://bsky.app/profile/$handleFromJson');
|
||||||
}
|
}
|
||||||
|
|
||||||
return Connection(
|
return Connection(
|
||||||
|
|
|
@ -5,6 +5,7 @@ import '../../models/engagement_summary.dart';
|
||||||
import '../../models/link_data.dart';
|
import '../../models/link_data.dart';
|
||||||
import '../../models/location_data.dart';
|
import '../../models/location_data.dart';
|
||||||
import '../../models/timeline_entry.dart';
|
import '../../models/timeline_entry.dart';
|
||||||
|
import '../../models/timeline_network_info.dart';
|
||||||
import '../../models/visibility.dart';
|
import '../../models/visibility.dart';
|
||||||
import '../../services/auth_service.dart';
|
import '../../services/auth_service.dart';
|
||||||
import '../../services/connections_manager.dart';
|
import '../../services/connections_manager.dart';
|
||||||
|
@ -17,6 +18,7 @@ import 'connection_mastodon_extensions.dart';
|
||||||
import 'hashtag_mastodon_extensions.dart';
|
import 'hashtag_mastodon_extensions.dart';
|
||||||
import 'link_preview_mastodon_extensions.dart';
|
import 'link_preview_mastodon_extensions.dart';
|
||||||
import 'media_attachment_mastodon_extension.dart';
|
import 'media_attachment_mastodon_extension.dart';
|
||||||
|
import 'timeline_network_info_mastodon_extensions.dart';
|
||||||
|
|
||||||
final _logger = Logger('TimelineEntryMastodonExtensions');
|
final _logger = Logger('TimelineEntryMastodonExtensions');
|
||||||
|
|
||||||
|
@ -32,6 +34,10 @@ extension TimelineEntryMastodonExtensions on TimelineEntry {
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
final networkInfo = json['application'] != null
|
||||||
|
? TimelineNetworkInfoMastodonExtensions.fromJson(json['application'])
|
||||||
|
: TimelineNetworkInfo.empty;
|
||||||
|
|
||||||
final connectionManager = getIt<ActiveProfileSelector<ConnectionsManager>>()
|
final connectionManager = getIt<ActiveProfileSelector<ConnectionsManager>>()
|
||||||
.getForProfile(activeProfile)
|
.getForProfile(activeProfile)
|
||||||
.fold(
|
.fold(
|
||||||
|
@ -76,6 +82,8 @@ extension TimelineEntryMastodonExtensions on TimelineEntry {
|
||||||
final visibilityString = json['visibility'];
|
final visibilityString = json['visibility'];
|
||||||
if (visibilityString == 'public') {
|
if (visibilityString == 'public') {
|
||||||
visibility = Visibility.public();
|
visibility = Visibility.public();
|
||||||
|
} else if (visibilityString == 'unlisted') {
|
||||||
|
visibility = Visibility.unlisted();
|
||||||
} else if (visibilityString == 'private') {
|
} else if (visibilityString == 'private') {
|
||||||
final allowedUserIds =
|
final allowedUserIds =
|
||||||
json['friendica']?['visibility']?['allow_cid'] as List<dynamic>? ??
|
json['friendica']?['visibility']?['allow_cid'] as List<dynamic>? ??
|
||||||
|
@ -94,15 +102,16 @@ extension TimelineEntryMastodonExtensions on TimelineEntry {
|
||||||
allowedCircleIds: allowedCircleIds.map((e) => e.toString()).toList(),
|
allowedCircleIds: allowedCircleIds.map((e) => e.toString()).toList(),
|
||||||
excludedCircleIds: excludedCircleIds.map((e) => e.toString()).toList(),
|
excludedCircleIds: excludedCircleIds.map((e) => e.toString()).toList(),
|
||||||
);
|
);
|
||||||
} else if (visibilityString == 'unlisted') {
|
|
||||||
visibility = Visibility.private();
|
|
||||||
} else {
|
} else {
|
||||||
visibility = Visibility.private();
|
visibility = Visibility.private();
|
||||||
}
|
}
|
||||||
|
|
||||||
const title = '';
|
const title = '';
|
||||||
final spoilerText = json['spoiler_text'] ?? '';
|
final spoilerText = json['spoiler_text'] ?? '';
|
||||||
final externalLink = json['uri'] ?? '';
|
final externalLink = switch (networkInfo.network) {
|
||||||
|
KnownNetworks.bluesky || KnownNetworks.threads => json['url'] ?? '',
|
||||||
|
_ => json['uri'] ?? '',
|
||||||
|
};
|
||||||
const actualLocationData = LocationData();
|
const actualLocationData = LocationData();
|
||||||
|
|
||||||
final modificationTimestamp = timestamp;
|
final modificationTimestamp = timestamp;
|
||||||
|
@ -165,6 +174,7 @@ extension TimelineEntryMastodonExtensions on TimelineEntry {
|
||||||
tags: tags,
|
tags: tags,
|
||||||
mediaAttachments: mediaAttachments,
|
mediaAttachments: mediaAttachments,
|
||||||
engagementSummary: engagementSummary,
|
engagementSummary: engagementSummary,
|
||||||
|
networkInfo: networkInfo,
|
||||||
linkPreviewData: linkPreviewData,
|
linkPreviewData: linkPreviewData,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,74 @@
|
||||||
|
import '../../models/timeline_network_info.dart';
|
||||||
|
|
||||||
|
extension TimelineNetworkInfoMastodonExtensions on TimelineNetworkInfo {
|
||||||
|
static TimelineNetworkInfo fromJson(Map<String, dynamic> json) {
|
||||||
|
final String name = json['name'] ?? 'Unknown';
|
||||||
|
final vapidKey = json['vapid_key'] ?? '';
|
||||||
|
final nameMainPart = name.split('(').first.trim();
|
||||||
|
final KnownNetworks network = switch (nameMainPart.toLowerCase()) {
|
||||||
|
'api' => KnownNetworks.friendica,
|
||||||
|
'activitypub' => KnownNetworks.activityPub,
|
||||||
|
'akkoma' => KnownNetworks.pleroma,
|
||||||
|
'bluesky' => KnownNetworks.bluesky,
|
||||||
|
'diaspora' => KnownNetworks.diaspora,
|
||||||
|
'friendica' => KnownNetworks.friendica,
|
||||||
|
'friendika' => KnownNetworks.friendica,
|
||||||
|
'gnu social' => KnownNetworks.gnu_social,
|
||||||
|
'gnusocial' => KnownNetworks.gnu_social,
|
||||||
|
'hubzilla' => KnownNetworks.hubzilla,
|
||||||
|
'mastodon' => KnownNetworks.mastodon,
|
||||||
|
'peertube' => KnownNetworks.peertube,
|
||||||
|
'pixelfed' => KnownNetworks.pixelfed,
|
||||||
|
'pleroma' => KnownNetworks.pleroma,
|
||||||
|
'red' => KnownNetworks.hubzilla,
|
||||||
|
'redmatrix' => KnownNetworks.hubzilla,
|
||||||
|
'socialhome' => KnownNetworks.socialhome,
|
||||||
|
'wordpress' => KnownNetworks.wordpress,
|
||||||
|
'lemmy' => KnownNetworks.lemmy,
|
||||||
|
'plume' => KnownNetworks.plume,
|
||||||
|
'funkwhale' => KnownNetworks.funkwhale,
|
||||||
|
'nextcloud' => KnownNetworks.nextcloud,
|
||||||
|
'drupal' => KnownNetworks.drupal,
|
||||||
|
'firefish' => KnownNetworks.firefish,
|
||||||
|
'calckey' => KnownNetworks.calckey,
|
||||||
|
'kbin' => KnownNetworks.kbin,
|
||||||
|
'threads' => KnownNetworks.threads,
|
||||||
|
|
||||||
|
// Clients which are assumed to be interfacing with Friendica if got set as application
|
||||||
|
// list from blockbot's blockbot_is_fediverse_client
|
||||||
|
'mastodonandroid' => KnownNetworks.friendica,
|
||||||
|
'tootdeck-worker' => KnownNetworks.friendica,
|
||||||
|
'piefed' => KnownNetworks.friendica,
|
||||||
|
'brighteon' => KnownNetworks.friendica,
|
||||||
|
'pachli' => KnownNetworks.friendica,
|
||||||
|
'tusky' => KnownNetworks.friendica,
|
||||||
|
'mona' => KnownNetworks.friendica,
|
||||||
|
'mitra' => KnownNetworks.friendica,
|
||||||
|
'megalodonandroid' => KnownNetworks.friendica,
|
||||||
|
'fedilab' => KnownNetworks.friendica,
|
||||||
|
'mastodonapp' => KnownNetworks.friendica,
|
||||||
|
'toot!' => KnownNetworks.friendica,
|
||||||
|
'intravnews' => KnownNetworks.friendica,
|
||||||
|
'pixeldroid' => KnownNetworks.friendica,
|
||||||
|
'greatnews' => KnownNetworks.friendica,
|
||||||
|
'protopage' => KnownNetworks.friendica,
|
||||||
|
'newsfox' => KnownNetworks.friendica,
|
||||||
|
'vienna' => KnownNetworks.friendica,
|
||||||
|
'wp-urldetails' => KnownNetworks.friendica,
|
||||||
|
'husky' => KnownNetworks.friendica,
|
||||||
|
'activitypub-go-http-client' => KnownNetworks.friendica,
|
||||||
|
'mobilesafari' => KnownNetworks.friendica,
|
||||||
|
'mastodon-ios' => KnownNetworks.friendica,
|
||||||
|
'mastodonpy' => KnownNetworks.friendica,
|
||||||
|
'techniverse' => KnownNetworks.friendica,
|
||||||
|
'relatica' => KnownNetworks.friendica,
|
||||||
|
_ => KnownNetworks.unknown,
|
||||||
|
};
|
||||||
|
|
||||||
|
return TimelineNetworkInfo(
|
||||||
|
name: name,
|
||||||
|
vapidKey: vapidKey,
|
||||||
|
network: network,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,6 +8,10 @@ extension VisibilityMastodonExtensions on Visibility {
|
||||||
return 'public';
|
return 'public';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (type == VisibilityType.unlisted) {
|
||||||
|
return 'unlisted';
|
||||||
|
}
|
||||||
|
|
||||||
if (!onComment && hasDetails) {
|
if (!onComment && hasDetails) {
|
||||||
final circleId =
|
final circleId =
|
||||||
allowedCircleIds.firstOrNull ?? allowedUserIds.firstOrNull;
|
allowedCircleIds.firstOrNull ?? allowedUserIds.firstOrNull;
|
||||||
|
|
|
@ -6,10 +6,31 @@ import '../models/exec_error.dart';
|
||||||
import '../models/server_data.dart';
|
import '../models/server_data.dart';
|
||||||
import '../utils/network_utils.dart';
|
import '../utils/network_utils.dart';
|
||||||
|
|
||||||
|
final threadsData = ServerData(
|
||||||
|
domainName: 'threads.net',
|
||||||
|
isFediverse: true,
|
||||||
|
protocols: [
|
||||||
|
'activitypub',
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
final threadsWwwData = ServerData(
|
||||||
|
domainName: 'www.threads.net',
|
||||||
|
isFediverse: true,
|
||||||
|
protocols: [
|
||||||
|
'activitypub',
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
const threadsDomain = 'threads.net';
|
||||||
|
const threadsWwwDomain = 'www.threads.net';
|
||||||
|
|
||||||
class FediverseServiceValidator {
|
class FediverseServiceValidator {
|
||||||
static const softwareTypeDiaspora = 'diaspora';
|
static const softwareTypeDiaspora = 'diaspora';
|
||||||
|
final knownServers = <String, ServerData>{
|
||||||
final knownServers = <String, ServerData>{};
|
threadsDomain: threadsData,
|
||||||
|
threadsWwwDomain: threadsData,
|
||||||
|
};
|
||||||
|
|
||||||
FutureResult<ServerData, ExecError> getServerData(String url) async {
|
FutureResult<ServerData, ExecError> getServerData(String url) async {
|
||||||
final uri = Uri.tryParse(url);
|
final uri = Uri.tryParse(url);
|
||||||
|
@ -37,6 +58,14 @@ class FediverseServiceValidator {
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<ServerData> refreshServerData(String domainName) async {
|
static Future<ServerData> refreshServerData(String domainName) async {
|
||||||
|
if (domainName == threadsDomain) {
|
||||||
|
return threadsData;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (domainName == threadsWwwDomain) {
|
||||||
|
return threadsWwwData;
|
||||||
|
}
|
||||||
|
|
||||||
final uri = Uri.https(
|
final uri = Uri.https(
|
||||||
domainName,
|
domainName,
|
||||||
'/.well-known/nodeinfo',
|
'/.well-known/nodeinfo',
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:color_blindness/color_blindness.dart';
|
import 'package:color_blindness/color_blindness.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
|
import '../models/settings/network_capabilities_settings.dart';
|
||||||
import '../utils/theme_mode_extensions.dart';
|
import '../utils/theme_mode_extensions.dart';
|
||||||
|
|
||||||
class SettingsService extends ChangeNotifier {
|
class SettingsService extends ChangeNotifier {
|
||||||
|
@ -51,6 +54,18 @@ class SettingsService extends ChangeNotifier {
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
NetworkCapabilitiesSettings _networkCapabilities =
|
||||||
|
NetworkCapabilitiesSettings.defaultSettings();
|
||||||
|
|
||||||
|
NetworkCapabilitiesSettings get networkCapabilities => _networkCapabilities;
|
||||||
|
|
||||||
|
set networkCapabilities(NetworkCapabilitiesSettings updatedCapabilities) {
|
||||||
|
_networkCapabilities = updatedCapabilities;
|
||||||
|
final jsonString = jsonEncode(_networkCapabilities.toJson());
|
||||||
|
_prefs.setString(_networkCapabilitiesKey, jsonString);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> initialize() async {
|
Future<void> initialize() async {
|
||||||
if (_initialized) {
|
if (_initialized) {
|
||||||
return;
|
return;
|
||||||
|
@ -60,6 +75,7 @@ class SettingsService extends ChangeNotifier {
|
||||||
_themeMode = ThemeModeExtensions.parse(_prefs.getString(_themeModeKey));
|
_themeMode = ThemeModeExtensions.parse(_prefs.getString(_themeModeKey));
|
||||||
_colorBlindnessType = _colorBlindnessTypeFromPrefs(_prefs);
|
_colorBlindnessType = _colorBlindnessTypeFromPrefs(_prefs);
|
||||||
_logLevel = _levelFromPrefs(_prefs);
|
_logLevel = _levelFromPrefs(_prefs);
|
||||||
|
_networkCapabilities = _networkCapabilitiesFromPrefs(_prefs);
|
||||||
_initialized = true;
|
_initialized = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -68,6 +84,7 @@ const _lowBandwidthModeKey = 'LowBandwidthMode';
|
||||||
const _themeModeKey = 'ThemeMode';
|
const _themeModeKey = 'ThemeMode';
|
||||||
const _colorBlindnessTestingModeKey = 'ColorBlindnessTestingMode';
|
const _colorBlindnessTestingModeKey = 'ColorBlindnessTestingMode';
|
||||||
const _logLevelKey = 'LogLevel';
|
const _logLevelKey = 'LogLevel';
|
||||||
|
const _networkCapabilitiesKey = 'NetworkCapabilities';
|
||||||
|
|
||||||
ColorBlindnessType _colorBlindnessTypeFromPrefs(SharedPreferences prefs) {
|
ColorBlindnessType _colorBlindnessTypeFromPrefs(SharedPreferences prefs) {
|
||||||
final cbString = prefs.getString(_colorBlindnessTestingModeKey);
|
final cbString = prefs.getString(_colorBlindnessTestingModeKey);
|
||||||
|
@ -80,6 +97,18 @@ ColorBlindnessType _colorBlindnessTypeFromPrefs(SharedPreferences prefs) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
NetworkCapabilitiesSettings _networkCapabilitiesFromPrefs(
|
||||||
|
SharedPreferences prefs) {
|
||||||
|
final ncString = prefs.getString(_networkCapabilitiesKey);
|
||||||
|
if (ncString?.isEmpty ?? true) {
|
||||||
|
return NetworkCapabilitiesSettings.defaultSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
final List<dynamic> json = jsonDecode(ncString!);
|
||||||
|
final nc = NetworkCapabilitiesSettings.fromJson(json);
|
||||||
|
return nc;
|
||||||
|
}
|
||||||
|
|
||||||
Level _levelFromPrefs(SharedPreferences prefs) {
|
Level _levelFromPrefs(SharedPreferences prefs) {
|
||||||
final levelString = prefs.getString(_logLevelKey);
|
final levelString = prefs.getString(_logLevelKey);
|
||||||
return switch (levelString) {
|
return switch (levelString) {
|
||||||
|
|
96
lib/utils/interaction_availability_util.dart
Normal file
96
lib/utils/interaction_availability_util.dart
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
import '../globals.dart';
|
||||||
|
import '../models/timeline_entry.dart';
|
||||||
|
import '../models/timeline_network_info.dart';
|
||||||
|
import '../models/visibility.dart';
|
||||||
|
import '../services/setting_service.dart';
|
||||||
|
import 'known_network_extensions.dart';
|
||||||
|
|
||||||
|
class InteractionCapabilityResult {
|
||||||
|
final bool canDo;
|
||||||
|
final String reason;
|
||||||
|
|
||||||
|
const InteractionCapabilityResult(
|
||||||
|
{required this.canDo, required this.reason});
|
||||||
|
}
|
||||||
|
|
||||||
|
extension InteractionAvailabilityExtension on TimelineEntry {
|
||||||
|
InteractionCapabilityResult getCanComment() {
|
||||||
|
final settingsService = getIt<SettingsService>();
|
||||||
|
final nc = settingsService.networkCapabilities
|
||||||
|
.getCapabilities(networkInfo.network);
|
||||||
|
|
||||||
|
if (!nc.comment) {
|
||||||
|
return InteractionCapabilityResult(
|
||||||
|
canDo: false,
|
||||||
|
reason:
|
||||||
|
"User disabled commenting on ${networkInfo.network.labelName} items. Go into settings to change.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return const InteractionCapabilityResult(
|
||||||
|
canDo: true,
|
||||||
|
reason: "Can comment on item",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
InteractionCapabilityResult getCanReact() {
|
||||||
|
final settingsService = getIt<SettingsService>();
|
||||||
|
final nc = settingsService.networkCapabilities
|
||||||
|
.getCapabilities(networkInfo.network);
|
||||||
|
|
||||||
|
if (!nc.react) {
|
||||||
|
return InteractionCapabilityResult(
|
||||||
|
canDo: false,
|
||||||
|
reason:
|
||||||
|
"User disabled reacting on ${networkInfo.network.labelName} items. Go into settings to change.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return const InteractionCapabilityResult(
|
||||||
|
canDo: true,
|
||||||
|
reason: "Can react on item",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
InteractionCapabilityResult getIsReshareable(bool isMine) {
|
||||||
|
if (isMine) {
|
||||||
|
return const InteractionCapabilityResult(
|
||||||
|
canDo: false,
|
||||||
|
reason: "Can't reshare your own post",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (networkInfo.network == KnownNetworks.bluesky) {
|
||||||
|
return const InteractionCapabilityResult(
|
||||||
|
canDo: false,
|
||||||
|
reason:
|
||||||
|
"Resharing of Bluesky posts through the API isn't supported by Friendica.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final settingsService = getIt<SettingsService>();
|
||||||
|
final nc = settingsService.networkCapabilities
|
||||||
|
.getCapabilities(networkInfo.network);
|
||||||
|
|
||||||
|
if (!nc.reshare) {
|
||||||
|
return InteractionCapabilityResult(
|
||||||
|
canDo: false,
|
||||||
|
reason:
|
||||||
|
"User disabled resharing ${networkInfo.network.labelName} items. Go into settings to change.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (visibility.type == VisibilityType.public ||
|
||||||
|
visibility.type == VisibilityType.unlisted) {
|
||||||
|
return const InteractionCapabilityResult(
|
||||||
|
canDo: true,
|
||||||
|
reason: "Can reshare item",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return const InteractionCapabilityResult(
|
||||||
|
canDo: false,
|
||||||
|
reason: "Can't reshare private items",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
63
lib/utils/known_network_extensions.dart
Normal file
63
lib/utils/known_network_extensions.dart
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
import '../models/timeline_network_info.dart';
|
||||||
|
|
||||||
|
extension KnownNetworkExtensions on KnownNetworks {
|
||||||
|
String get labelName => switch (this) {
|
||||||
|
KnownNetworks.activityPub => 'ActivityPub',
|
||||||
|
KnownNetworks.bluesky => 'Bluesky',
|
||||||
|
KnownNetworks.calckey => 'Calckey',
|
||||||
|
KnownNetworks.diaspora => 'Diaspora',
|
||||||
|
KnownNetworks.drupal => 'Drupal',
|
||||||
|
KnownNetworks.firefish => 'Firefish',
|
||||||
|
KnownNetworks.friendica => 'Friendica',
|
||||||
|
KnownNetworks.funkwhale => 'Funkwhale',
|
||||||
|
KnownNetworks.gnu_social => 'GNU Social',
|
||||||
|
KnownNetworks.hubzilla => 'Hubzilla',
|
||||||
|
KnownNetworks.kbin => 'Kbin',
|
||||||
|
KnownNetworks.lemmy => 'Lemmy',
|
||||||
|
KnownNetworks.mastodon => 'Mastodon',
|
||||||
|
KnownNetworks.nextcloud => 'Nextcloud',
|
||||||
|
KnownNetworks.peertube => 'PeerTube',
|
||||||
|
KnownNetworks.pixelfed => 'Pixelfed',
|
||||||
|
KnownNetworks.pleroma => 'Pleroma',
|
||||||
|
KnownNetworks.plume => 'Plume',
|
||||||
|
KnownNetworks.red => 'Red',
|
||||||
|
KnownNetworks.redmatrix => 'RedMatrix',
|
||||||
|
KnownNetworks.socialhome => 'Socialhome',
|
||||||
|
KnownNetworks.threads => 'Threads',
|
||||||
|
KnownNetworks.wordpress => 'WordPress',
|
||||||
|
KnownNetworks.unknown => 'Unknown',
|
||||||
|
};
|
||||||
|
|
||||||
|
String get forkAwesomeUnicode => switch (this) {
|
||||||
|
KnownNetworks.activityPub => '\uf2f2',
|
||||||
|
KnownNetworks.bluesky => '\uf111',
|
||||||
|
KnownNetworks.calckey => '\uf1ec',
|
||||||
|
KnownNetworks.diaspora => '\uf2e5',
|
||||||
|
KnownNetworks.drupal => '\uf1a9',
|
||||||
|
KnownNetworks.firefish => '\uf06d',
|
||||||
|
KnownNetworks.friendica => '\uf2e6',
|
||||||
|
KnownNetworks.funkwhale => '\uf339',
|
||||||
|
KnownNetworks.gnu_social => '\uf2e7',
|
||||||
|
KnownNetworks.hubzilla => '\uf2eb',
|
||||||
|
KnownNetworks.kbin => '\uf058',
|
||||||
|
KnownNetworks.lemmy => '\uf0c0',
|
||||||
|
KnownNetworks.mastodon => '\uf2e1',
|
||||||
|
KnownNetworks.nextcloud => '\uf307',
|
||||||
|
KnownNetworks.peertube => '\uf2e4',
|
||||||
|
KnownNetworks.pixelfed => '\uf314',
|
||||||
|
KnownNetworks.pleroma => '\uf324',
|
||||||
|
KnownNetworks.plume => '\uf356',
|
||||||
|
KnownNetworks.red => '\uf2eb',
|
||||||
|
KnownNetworks.redmatrix => '\uf2eb',
|
||||||
|
KnownNetworks.socialhome => '\uf2ec',
|
||||||
|
KnownNetworks.threads => '\uf16d',
|
||||||
|
KnownNetworks.wordpress => '\uf19a',
|
||||||
|
KnownNetworks.unknown => '\uf059',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
extension TimelineNetworkInfoExtensions on TimelineNetworkInfo {
|
||||||
|
String get labelName => network.labelName;
|
||||||
|
|
||||||
|
String get forkAwesomeUnicode => network.forkAwesomeUnicode;
|
||||||
|
}
|
60
pubspec.lock
60
pubspec.lock
|
@ -733,30 +733,6 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.8.1"
|
version: "4.8.1"
|
||||||
leak_tracker:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: leak_tracker
|
|
||||||
sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "10.0.0"
|
|
||||||
leak_tracker_flutter_testing:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: leak_tracker_flutter_testing
|
|
||||||
sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "2.0.1"
|
|
||||||
leak_tracker_testing:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: leak_tracker_testing
|
|
||||||
sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "2.0.1"
|
|
||||||
lints:
|
lints:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -785,18 +761,18 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: matcher
|
name: matcher
|
||||||
sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb
|
sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.12.16+1"
|
version: "0.12.16"
|
||||||
material_color_utilities:
|
material_color_utilities:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: material_color_utilities
|
name: material_color_utilities
|
||||||
sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a"
|
sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.8.0"
|
version: "0.5.0"
|
||||||
media_kit:
|
media_kit:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
@ -873,10 +849,10 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: meta
|
name: meta
|
||||||
sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04
|
sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.11.0"
|
version: "1.10.0"
|
||||||
mime:
|
mime:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -950,7 +926,7 @@ packages:
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.0"
|
version: "2.1.0"
|
||||||
package_info_plus:
|
package_info_plus:
|
||||||
dependency: "direct main"
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: package_info_plus
|
name: package_info_plus
|
||||||
sha256: "7e76fad405b3e4016cd39d08f455a4eb5199723cf594cd1b8916d47140d93017"
|
sha256: "7e76fad405b3e4016cd39d08f455a4eb5199723cf594cd1b8916d47140d93017"
|
||||||
|
@ -969,10 +945,10 @@ packages:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: path
|
name: path
|
||||||
sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af"
|
sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.9.0"
|
version: "1.8.3"
|
||||||
path_parsing:
|
path_parsing:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -1547,14 +1523,6 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.17"
|
version: "2.0.17"
|
||||||
vm_service:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: vm_service
|
|
||||||
sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "13.0.0"
|
|
||||||
volume_controller:
|
volume_controller:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -1587,6 +1555,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.0"
|
version: "1.1.0"
|
||||||
|
web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: web
|
||||||
|
sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.3.0"
|
||||||
web_socket_channel:
|
web_socket_channel:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -1644,5 +1620,5 @@ packages:
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.2"
|
version: "3.1.2"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.2.0-0 <4.0.0"
|
dart: ">=3.2.0-194.0.dev <4.0.0"
|
||||||
flutter: ">=3.10.0"
|
flutter: ">=3.10.0"
|
||||||
|
|
|
@ -2,7 +2,7 @@ name: relatica
|
||||||
description: A mobile and desktop client for interacting with the Friendica social network
|
description: A mobile and desktop client for interacting with the Friendica social network
|
||||||
|
|
||||||
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
||||||
version: 0.10.1
|
version: 0.11.0
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=3.0.0 <4.0.0'
|
sdk: '>=3.0.0 <4.0.0'
|
||||||
|
@ -38,7 +38,6 @@ dependencies:
|
||||||
network_to_file_image: ^4.0.1
|
network_to_file_image: ^4.0.1
|
||||||
objectbox: ^2.3.1
|
objectbox: ^2.3.1
|
||||||
objectbox_flutter_libs: ^2.3.1
|
objectbox_flutter_libs: ^2.3.1
|
||||||
package_info_plus: ^4.2.0
|
|
||||||
path: ^1.8.2
|
path: ^1.8.2
|
||||||
path_provider: ^2.0.11
|
path_provider: ^2.0.11
|
||||||
provider: ^6.0.4
|
provider: ^6.0.4
|
||||||
|
@ -69,6 +68,10 @@ flutter:
|
||||||
uses-material-design: true
|
uses-material-design: true
|
||||||
assets:
|
assets:
|
||||||
- icon/relatica_logo.svg
|
- icon/relatica_logo.svg
|
||||||
|
fonts:
|
||||||
|
- family: ForkAwesome
|
||||||
|
fonts:
|
||||||
|
- asset: fonts/forkawesome-webfont.ttf
|
||||||
|
|
||||||
parts:
|
parts:
|
||||||
uet-lms:
|
uet-lms:
|
||||||
|
@ -81,6 +84,7 @@ parts:
|
||||||
stage-packages:
|
stage-packages:
|
||||||
- libsecret-1-0
|
- libsecret-1-0
|
||||||
- libjsoncpp1
|
- libjsoncpp1
|
||||||
|
|
||||||
# To add assets to your application, add an assets section, like this:
|
# To add assets to your application, add an assets section, like this:
|
||||||
# assets:
|
# assets:
|
||||||
# - images/a_dot_burr.jpeg
|
# - images/a_dot_burr.jpeg
|
||||||
|
|
Loading…
Reference in a new issue