mirror of
https://gitlab.com/mysocialportal/relatica
synced 2024-10-18 11:13:31 +00:00
Refactor follow requests to use actual follow request system if available
This commit is contained in:
parent
1801780dcf
commit
bd02a01d08
19 changed files with 438 additions and 133 deletions
|
@ -16,6 +16,7 @@ import 'services/connections_manager.dart';
|
|||
import 'services/direct_message_service.dart';
|
||||
import 'services/entry_manager_service.dart';
|
||||
import 'services/feature_version_checker.dart';
|
||||
import 'services/follow_requests_manager.dart';
|
||||
import 'services/gallery_service.dart';
|
||||
import 'services/hashtag_service.dart';
|
||||
import 'services/interactions_manager.dart';
|
||||
|
@ -83,6 +84,8 @@ Future<void> dependencyInjectionInitialization() async {
|
|||
)));
|
||||
getIt.registerSingleton<ActiveProfileSelector<NotificationsManager>>(
|
||||
ActiveProfileSelector((_) => NotificationsManager()));
|
||||
getIt.registerSingleton<ActiveProfileSelector<FollowRequestsManager>>(
|
||||
ActiveProfileSelector((_) => FollowRequestsManager()));
|
||||
getIt.registerSingleton<ActiveProfileSelector<DirectMessageService>>(
|
||||
ActiveProfileSelector((p) => DirectMessageService()));
|
||||
getIt.registerSingleton<ActiveProfileSelector<InteractionsManager>>(
|
||||
|
|
|
@ -13,6 +13,7 @@ import '../models/auth/profile.dart';
|
|||
import '../models/connection.dart';
|
||||
import '../models/direct_message.dart';
|
||||
import '../models/exec_error.dart';
|
||||
import '../models/follow_request.dart';
|
||||
import '../models/gallery_data.dart';
|
||||
import '../models/group_data.dart';
|
||||
import '../models/image_entry.dart';
|
||||
|
@ -26,6 +27,7 @@ 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/follow_request_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';
|
||||
|
@ -361,6 +363,22 @@ class RelationshipsClient extends FriendicaClient {
|
|||
.execErrorCast();
|
||||
}
|
||||
|
||||
FutureResult<PagedResponse<List<FollowRequest>>, ExecError> getFollowRequests(
|
||||
PagingData page) async {
|
||||
_logger.finest(() => 'Getting follow requests with paging data $page');
|
||||
_networkStatusService.startConnectionUpdateStatus();
|
||||
final baseUrl = 'https://$serverName/api/v1/follow_requests';
|
||||
final result = await _getApiListRequest(
|
||||
Uri.parse('$baseUrl?${page.toQueryParameters()}'),
|
||||
);
|
||||
_networkStatusService.finishConnectionUpdateStatus();
|
||||
return result
|
||||
.andThenSuccess((response) => response.map((jsonArray) => jsonArray
|
||||
.map((json) => FollowRequestMastodonExtension.fromJson(json))
|
||||
.toList()))
|
||||
.execErrorCast();
|
||||
}
|
||||
|
||||
FutureResult<PagedResponse<List<Connection>>, ExecError> getMyFollowers(
|
||||
PagingData page) async {
|
||||
_logger.finest(() => 'Getting followers data with page data $page');
|
||||
|
|
|
@ -70,11 +70,17 @@ class PagesManager<TResult, TID> {
|
|||
}
|
||||
|
||||
FutureResult<PagedResponse<TResult>, ExecError> nextFromEnd() async {
|
||||
if (_pages.isEmpty) {
|
||||
return buildErrorResult(type: ErrorType.rangeError);
|
||||
}
|
||||
return _previousOrNext(_pages.last.id, false);
|
||||
}
|
||||
|
||||
FutureResult<PagedResponse<TResult>, ExecError>
|
||||
previousFromBeginning() async {
|
||||
if (_pages.isEmpty) {
|
||||
return buildErrorResult(type: ErrorType.rangeError);
|
||||
}
|
||||
return _previousOrNext(_pages.first.id, true);
|
||||
}
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ import 'services/auth_service.dart';
|
|||
import 'services/connections_manager.dart';
|
||||
import 'services/direct_message_service.dart';
|
||||
import 'services/entry_manager_service.dart';
|
||||
import 'services/follow_requests_manager.dart';
|
||||
import 'services/gallery_service.dart';
|
||||
import 'services/hashtag_service.dart';
|
||||
import 'services/interactions_manager.dart';
|
||||
|
@ -84,6 +85,11 @@ class App extends StatelessWidget {
|
|||
create: (_) =>
|
||||
getIt<ActiveProfileSelector<NotificationsManager>>(),
|
||||
),
|
||||
ChangeNotifierProvider<
|
||||
ActiveProfileSelector<FollowRequestsManager>>(
|
||||
create: (_) =>
|
||||
getIt<ActiveProfileSelector<FollowRequestsManager>>(),
|
||||
),
|
||||
ChangeNotifierProvider<
|
||||
ActiveProfileSelector<DirectMessageService>>(
|
||||
create: (_) =>
|
||||
|
|
21
lib/models/follow_request.dart
Normal file
21
lib/models/follow_request.dart
Normal file
|
@ -0,0 +1,21 @@
|
|||
import 'connection.dart';
|
||||
|
||||
class FollowRequest {
|
||||
final Connection connection;
|
||||
final DateTime createdAt;
|
||||
|
||||
const FollowRequest({
|
||||
required this.connection,
|
||||
required this.createdAt,
|
||||
});
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is FollowRequest &&
|
||||
runtimeType == other.runtimeType &&
|
||||
connection == other.connection;
|
||||
|
||||
@override
|
||||
int get hashCode => connection.hashCode;
|
||||
}
|
|
@ -50,7 +50,7 @@ enum NotificationType {
|
|||
}
|
||||
}
|
||||
|
||||
class UserNotification {
|
||||
class UserNotification implements Comparable<UserNotification> {
|
||||
final String id;
|
||||
final NotificationType type;
|
||||
final String fromId;
|
||||
|
@ -79,4 +79,17 @@ class UserNotification {
|
|||
String toString() {
|
||||
return 'UserNotification{id: $id, seen: $dismissed, fromName: $fromName, content: $content}';
|
||||
}
|
||||
|
||||
@override
|
||||
int compareTo(UserNotification other) {
|
||||
if (dismissed == other.dismissed) {
|
||||
return -timestamp.compareTo(other.timestamp);
|
||||
}
|
||||
|
||||
if (dismissed && !other.dismissed) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -98,7 +98,7 @@ class _EditorScreenState extends State<EditorScreen> {
|
|||
.andThenAsync((manager) async => await manager.getEntryById(widget.id));
|
||||
result.match(onSuccess: (entry) {
|
||||
_logger.fine('Loading status ${widget.id} information into fields');
|
||||
contentController.text = toEditTextField(entry.body);
|
||||
contentController.text = htmlToSimpleText(entry.body);
|
||||
spoilerController.text = entry.spoilerText;
|
||||
existingMediaItems
|
||||
.addAll(entry.mediaAttachments.map((e) => e.toImageEntry()));
|
||||
|
|
|
@ -1,12 +1,21 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_widget_from_html_core/flutter_widget_from_html_core.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:relatica/utils/active_profile_selector.dart';
|
||||
import 'package:result_monad/result_monad.dart';
|
||||
|
||||
import '../controls/login_aware_cached_network_image.dart';
|
||||
import '../controls/padding.dart';
|
||||
import '../globals.dart';
|
||||
import '../models/connection.dart';
|
||||
import '../models/exec_error.dart';
|
||||
import '../routes.dart';
|
||||
import '../services/connections_manager.dart';
|
||||
import '../services/feature_version_checker.dart';
|
||||
import '../services/follow_requests_manager.dart';
|
||||
import '../services/notifications_manager.dart';
|
||||
import '../utils/active_profile_selector.dart';
|
||||
import '../utils/url_opening_utils.dart';
|
||||
|
||||
class FollowRequestAdjudicationScreen extends StatefulWidget {
|
||||
final String userId;
|
||||
|
@ -24,30 +33,47 @@ class _FollowRequestAdjudicationScreenState
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final manager = context
|
||||
final fm =
|
||||
getIt<ActiveProfileSelector<FollowRequestsManager>>().activeEntry.value;
|
||||
final cm = context
|
||||
.watch<ActiveProfileSelector<ConnectionsManager>>()
|
||||
.activeEntry
|
||||
.value;
|
||||
final connResult = manager.getById(widget.userId);
|
||||
late final Widget body;
|
||||
if (connResult.isFailure) {
|
||||
body = Text('Error getting contact information: ${connResult.error}');
|
||||
|
||||
late final Result<Connection, ExecError> result;
|
||||
if (getIt<FriendicaVersionChecker>()
|
||||
.canUseFeature(RelaticaFeatures.usingActualFollowRequests)) {
|
||||
result = fm
|
||||
.getByUserId(widget.userId)
|
||||
.mapValue((request) => request.connection);
|
||||
} else {
|
||||
result = cm.getById(widget.userId);
|
||||
}
|
||||
|
||||
final contact = connResult.value;
|
||||
switch (contact.status) {
|
||||
case ConnectionStatus.theyFollowYou:
|
||||
case ConnectionStatus.youFollowThem:
|
||||
case ConnectionStatus.none:
|
||||
body = _buildMainPanel(context, manager, contact);
|
||||
break;
|
||||
case ConnectionStatus.mutual:
|
||||
body = const Text('Already allowed them to connect');
|
||||
break;
|
||||
case ConnectionStatus.you:
|
||||
case ConnectionStatus.unknown:
|
||||
body = Text('Invalid state, nothing to do here: ${contact.status}');
|
||||
break;
|
||||
late final Widget body;
|
||||
if (result.isFailure) {
|
||||
body = Text('Error getting request info: ${result.error}');
|
||||
} else {
|
||||
final contact = result.value;
|
||||
final contactStatus = cm
|
||||
.getById(widget.userId)
|
||||
.getValueOrElse(() => Connection(status: ConnectionStatus.none))
|
||||
.status;
|
||||
|
||||
switch (contactStatus) {
|
||||
case ConnectionStatus.theyFollowYou:
|
||||
case ConnectionStatus.youFollowThem:
|
||||
case ConnectionStatus.none:
|
||||
body = _buildMainPanel(context, contact, cm, fm);
|
||||
break;
|
||||
case ConnectionStatus.mutual:
|
||||
body = const Text('Already allowed them to connect');
|
||||
break;
|
||||
case ConnectionStatus.you:
|
||||
case ConnectionStatus.unknown:
|
||||
body = Text('Invalid state, nothing to do here: ${contact.status}');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
|
@ -63,7 +89,11 @@ class _FollowRequestAdjudicationScreenState
|
|||
}
|
||||
|
||||
Widget _buildMainPanel(
|
||||
BuildContext context, ConnectionsManager manager, Connection contact) {
|
||||
BuildContext context,
|
||||
Connection contact,
|
||||
ConnectionsManager connectionsManager,
|
||||
FollowRequestsManager followRequestsManager,
|
||||
) {
|
||||
// Options are:
|
||||
// Accept and follow back
|
||||
// Accept and don't follow back
|
||||
|
@ -71,50 +101,102 @@ class _FollowRequestAdjudicationScreenState
|
|||
// Back with no action
|
||||
// Calling method should check if completed (true) or not (false) to decide if updating their view of that item
|
||||
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
LoginAwareCachedNetworkImage(imageUrl: contact.avatarUrl.toString()),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
contact.name,
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
const HorizontalPadding(),
|
||||
],
|
||||
),
|
||||
const VerticalPadding(),
|
||||
ElevatedButton(
|
||||
onPressed: processing
|
||||
? null
|
||||
: () async => await accept(manager, contact, true),
|
||||
child: const Text('Accept and follow back'),
|
||||
),
|
||||
const VerticalPadding(),
|
||||
ElevatedButton(
|
||||
onPressed:
|
||||
processing ? null : () async => accept(manager, contact, false),
|
||||
child: const Text("Accept but don't follow back"),
|
||||
),
|
||||
const VerticalPadding(),
|
||||
ElevatedButton(
|
||||
onPressed: processing ? null : () async => reject(manager, contact),
|
||||
child: const Text('Reject'),
|
||||
),
|
||||
const VerticalPadding(),
|
||||
ElevatedButton(
|
||||
onPressed: processing ? null : () async => ignore(manager, contact),
|
||||
child: const Text('Ignore (Rejects but user cannot ask again)'),
|
||||
),
|
||||
],
|
||||
return SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
LoginAwareCachedNetworkImage(imageUrl: contact.avatarUrl.toString()),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
'${contact.name}(${contact.handle})',
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
const HorizontalPadding(),
|
||||
],
|
||||
),
|
||||
const VerticalPadding(),
|
||||
Wrap(
|
||||
runSpacing: 5.0,
|
||||
spacing: 5.0,
|
||||
alignment: WrapAlignment.center,
|
||||
children: [
|
||||
ElevatedButton(
|
||||
onPressed: processing
|
||||
? null
|
||||
: () async => await accept(connectionsManager,
|
||||
followRequestsManager, contact, true),
|
||||
child: const Text('Accept and follow back'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: processing
|
||||
? null
|
||||
: () async => await accept(connectionsManager,
|
||||
followRequestsManager, contact, true),
|
||||
child: const Text('Accept and follow back'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: processing
|
||||
? null
|
||||
: () async => accept(connectionsManager,
|
||||
followRequestsManager, contact, false),
|
||||
child: const Text("Accept but don't follow back"),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: processing
|
||||
? null
|
||||
: () async => reject(
|
||||
connectionsManager, followRequestsManager, contact),
|
||||
child: const Text('Reject'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: processing
|
||||
? null
|
||||
: () async => ignore(
|
||||
connectionsManager, followRequestsManager, contact),
|
||||
child: const Text('Ignore (Rejects but user cannot ask again)'),
|
||||
),
|
||||
],
|
||||
),
|
||||
const VerticalPadding(),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
ElevatedButton(
|
||||
onPressed: () => context.pushNamed(
|
||||
ScreenPaths.userPosts,
|
||||
params: {'id': contact.id},
|
||||
),
|
||||
child: const Text('Posts')),
|
||||
ElevatedButton(
|
||||
onPressed: () async =>
|
||||
await openProfileExternal(context, contact),
|
||||
child: const Text('Open In Browser'),
|
||||
),
|
||||
],
|
||||
),
|
||||
const VerticalPadding(),
|
||||
HtmlWidget(
|
||||
contact.note,
|
||||
onTapUrl: (url) async {
|
||||
return await openUrlStringInSystembrowser(context, url, 'link');
|
||||
},
|
||||
),
|
||||
const VerticalPadding(),
|
||||
Text(
|
||||
'#Followers: ${contact.followerCount} followers, #Following, ${contact.followingCount}, #Statuses: ${contact.statusesCount}'),
|
||||
const VerticalPadding(),
|
||||
Text('Last Status: ${contact.lastStatus ?? "Unknown"}'),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> accept(
|
||||
ConnectionsManager manager,
|
||||
FollowRequestsManager followRequestsManager,
|
||||
Connection contact,
|
||||
bool followBack,
|
||||
) async {
|
||||
|
@ -127,6 +209,8 @@ class _FollowRequestAdjudicationScreenState
|
|||
await manager.follow(contact);
|
||||
}
|
||||
|
||||
_performUpdates(followRequestsManager);
|
||||
|
||||
setState(() {
|
||||
processing = false;
|
||||
});
|
||||
|
@ -136,13 +220,15 @@ class _FollowRequestAdjudicationScreenState
|
|||
}
|
||||
}
|
||||
|
||||
Future<void> reject(ConnectionsManager manager, Connection contact) async {
|
||||
Future<void> reject(ConnectionsManager manager,
|
||||
FollowRequestsManager followRequestsManager, Connection contact) async {
|
||||
setState(() {
|
||||
processing = true;
|
||||
});
|
||||
|
||||
await manager.rejectFollowRequest(contact);
|
||||
|
||||
_performUpdates(followRequestsManager);
|
||||
setState(() {
|
||||
processing = false;
|
||||
});
|
||||
|
@ -152,12 +238,14 @@ class _FollowRequestAdjudicationScreenState
|
|||
}
|
||||
}
|
||||
|
||||
Future<void> ignore(ConnectionsManager manager, Connection contact) async {
|
||||
Future<void> ignore(ConnectionsManager manager,
|
||||
FollowRequestsManager followRequestsManager, Connection contact) async {
|
||||
setState(() {
|
||||
processing = true;
|
||||
});
|
||||
|
||||
await manager.ignoreFollowRequest(contact);
|
||||
_performUpdates(followRequestsManager);
|
||||
|
||||
setState(() {
|
||||
processing = false;
|
||||
|
@ -167,4 +255,11 @@ class _FollowRequestAdjudicationScreenState
|
|||
context.pop();
|
||||
}
|
||||
}
|
||||
|
||||
void _performUpdates(FollowRequestsManager followRequestsManager) {
|
||||
followRequestsManager.update();
|
||||
getIt<ActiveProfileSelector<NotificationsManager>>()
|
||||
.activeEntry
|
||||
.andThenSuccess((m) => m.updateNotifications());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,18 +25,6 @@ class UserProfileScreen extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _UserProfileScreenState extends State<UserProfileScreen> {
|
||||
Future<void> openProfileExternal(
|
||||
BuildContext context,
|
||||
Connection connection,
|
||||
) async {
|
||||
final openInBrowser =
|
||||
await showYesNoDialog(context, 'Open profile in browser?');
|
||||
if (openInBrowser == true) {
|
||||
await openUrlStringInSystembrowser(
|
||||
context, connection.profileUrl.toString(), 'Post');
|
||||
}
|
||||
}
|
||||
|
||||
var isUpdating = false;
|
||||
|
||||
@override
|
||||
|
@ -107,7 +95,7 @@ class _UserProfileScreenState extends State<UserProfileScreen> {
|
|||
Text(
|
||||
'#Followers: ${profile.followerCount} followers, #Following, ${profile.followingCount}, #Statuses: ${profile.statusesCount}'),
|
||||
const VerticalPadding(),
|
||||
Text('Last Status: ${profile.lastStatus}'),
|
||||
Text('Last Status: ${profile.lastStatus ?? "Unknown"}'),
|
||||
const VerticalPadding(),
|
||||
if (profile.status == ConnectionStatus.mutual ||
|
||||
profile.status == ConnectionStatus.youFollowThem)
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import 'package:relatica/utils/html_to_edit_text_helper.dart';
|
||||
import 'package:relatica/utils/string_utils.dart';
|
||||
|
||||
import '../../models/link_preview_data.dart';
|
||||
import '../../utils/html_to_edit_text_helper.dart';
|
||||
import '../../utils/string_utils.dart';
|
||||
|
||||
extension LinkPreviewExtension on LinkPreviewData {
|
||||
String toBodyAttachment() {
|
||||
|
@ -9,8 +8,9 @@ extension LinkPreviewExtension on LinkPreviewData {
|
|||
return "[attachment type='link' url='$link' title='$title']$description[/attachment]";
|
||||
}
|
||||
|
||||
final sanitizedTitle = toEditTextField(title).stripHyperlinks();
|
||||
final sanitizedDescription = toEditTextField(description).stripHyperlinks();
|
||||
final sanitizedTitle = htmlToSimpleText(title).stripHyperlinks();
|
||||
final sanitizedDescription =
|
||||
htmlToSimpleText(description).stripHyperlinks();
|
||||
|
||||
return "[attachment type='link' url='$link' title='$sanitizedTitle' image='$selectedImageUrl']$sanitizedDescription[/attachment]";
|
||||
}
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
import 'package:uuid/uuid.dart';
|
||||
|
||||
import '../../models/follow_request.dart';
|
||||
import '../../models/user_notification.dart';
|
||||
import 'connection_mastodon_extensions.dart';
|
||||
|
||||
extension FollowRequestMastodonExtension on FollowRequest {
|
||||
static FollowRequest fromJson(Map<String, dynamic> json) {
|
||||
final connection = ConnectionMastodonExtensions.fromJson(json);
|
||||
final createdAt =
|
||||
DateTime.tryParse(json['created_at'] ?? '') ?? DateTime.now();
|
||||
return FollowRequest(connection: connection, createdAt: createdAt);
|
||||
}
|
||||
|
||||
UserNotification toUserNotification() {
|
||||
return UserNotification(
|
||||
id: Uuid().v4(),
|
||||
type: NotificationType.follow_request,
|
||||
fromId: connection.id,
|
||||
fromName: connection.name,
|
||||
fromUrl: connection.profileUrl,
|
||||
timestamp: createdAt.millisecondsSinceEpoch,
|
||||
iid: '',
|
||||
dismissed: false,
|
||||
content:
|
||||
'${connection.name}(${connection.handle}) submitted a follow request ',
|
||||
link: '',
|
||||
);
|
||||
}
|
||||
}
|
|
@ -5,6 +5,8 @@ import '../../models/user_notification.dart';
|
|||
import '../../services/connections_manager.dart';
|
||||
import '../../utils/active_profile_selector.dart';
|
||||
import '../../utils/dateutils.dart';
|
||||
import '../../utils/html_to_edit_text_helper.dart';
|
||||
import '../../utils/string_utils.dart';
|
||||
import 'connection_mastodon_extensions.dart';
|
||||
import 'timeline_entry_mastodon_extensions.dart';
|
||||
|
||||
|
@ -32,10 +34,10 @@ extension NotificationMastodonExtension on UserNotification {
|
|||
var content = '';
|
||||
switch (type) {
|
||||
case NotificationType.follow:
|
||||
content = '${from.name} is now following you';
|
||||
content = '${from.name}(${from.handle}) is now following you';
|
||||
break;
|
||||
case NotificationType.follow_request:
|
||||
content = '${from.name} submitted a follow request ';
|
||||
content = '${from.name}(${from.handle}) submitted a follow request ';
|
||||
break;
|
||||
case NotificationType.unknown:
|
||||
content = '${from.name} has unknown interaction notification';
|
||||
|
@ -59,7 +61,8 @@ extension NotificationMastodonExtension on UserNotification {
|
|||
final shareInfo = status.reshareAuthorId.isNotEmpty
|
||||
? "reshare of ${status.reshareAuthor}'s"
|
||||
: '';
|
||||
content = "$baseContent $shareInfo $referenceType: ${status.body}";
|
||||
final bodyText = htmlToSimpleText(status.body).truncate(length: 100);
|
||||
content = "$baseContent $shareInfo $referenceType: $bodyText";
|
||||
break;
|
||||
case NotificationType.direct_message:
|
||||
// this is a Relatica internal type so nothing to do here
|
||||
|
|
|
@ -8,6 +8,7 @@ import '../models/friendica_version.dart';
|
|||
enum RelaticaFeatures {
|
||||
postSpoilerText,
|
||||
statusEditing,
|
||||
usingActualFollowRequests,
|
||||
}
|
||||
|
||||
class FriendicaVersionChecker {
|
||||
|
@ -46,5 +47,6 @@ class FriendicaVersionChecker {
|
|||
static final featureVersionRequirement = <RelaticaFeatures, FriendicaVersion>{
|
||||
RelaticaFeatures.postSpoilerText: v2023_03,
|
||||
RelaticaFeatures.statusEditing: v2023_03,
|
||||
RelaticaFeatures.usingActualFollowRequests: v2023_03,
|
||||
};
|
||||
}
|
||||
|
|
61
lib/services/follow_requests_manager.dart
Normal file
61
lib/services/follow_requests_manager.dart
Normal file
|
@ -0,0 +1,61 @@
|
|||
import 'dart:collection';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:result_monad/result_monad.dart';
|
||||
|
||||
import '../friendica_client/friendica_client.dart';
|
||||
import '../friendica_client/paged_response.dart';
|
||||
import '../friendica_client/paging_data.dart';
|
||||
import '../globals.dart';
|
||||
import '../models/exec_error.dart';
|
||||
import '../models/follow_request.dart';
|
||||
import 'auth_service.dart';
|
||||
|
||||
class FollowRequestsManager extends ChangeNotifier {
|
||||
static const maxIterations = 20;
|
||||
final _requests = <String, FollowRequest>{};
|
||||
|
||||
List<FollowRequest> get requests => UnmodifiableListView(_requests.values);
|
||||
|
||||
Result<FollowRequest, ExecError> getByUserId(String id) {
|
||||
final request = _requests[id];
|
||||
return request != null
|
||||
? Result.ok(request)
|
||||
: buildErrorResult(
|
||||
type: ErrorType.rangeError,
|
||||
message: 'Request for $id not found',
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> update() async {
|
||||
var result = await _processPage(PagingData());
|
||||
var count = 0;
|
||||
final updatedRequests = <FollowRequest>{};
|
||||
while (result.isSuccess &&
|
||||
result.value.hasMorePages &&
|
||||
count < maxIterations) {
|
||||
result
|
||||
.andThenSuccess((requests) => updatedRequests.addAll(requests.data));
|
||||
result = await _processPage(result.value.next);
|
||||
count++;
|
||||
}
|
||||
|
||||
_requests.clear();
|
||||
for (final r in updatedRequests) {
|
||||
_requests[r.connection.id] = r;
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
FutureResult<PagedResponse<List<FollowRequest>>, ExecError> _processPage(
|
||||
PagingData? page) async {
|
||||
if (page == null) {
|
||||
return buildErrorResult(type: ErrorType.rangeError);
|
||||
}
|
||||
final result =
|
||||
await RelationshipsClient(getIt<AccountsService>().currentProfile)
|
||||
.getFollowRequests(page);
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
|
@ -10,9 +10,12 @@ import '../friendica_client/paging_data.dart';
|
|||
import '../globals.dart';
|
||||
import '../models/exec_error.dart';
|
||||
import '../models/user_notification.dart';
|
||||
import '../serializers/mastodon/follow_request_mastodon_extensions.dart';
|
||||
import '../utils/active_profile_selector.dart';
|
||||
import 'auth_service.dart';
|
||||
import 'direct_message_service.dart';
|
||||
import 'feature_version_checker.dart';
|
||||
import 'follow_requests_manager.dart';
|
||||
import 'network_status_service.dart';
|
||||
|
||||
class NotificationsManager extends ChangeNotifier {
|
||||
|
@ -29,20 +32,34 @@ class NotificationsManager extends ChangeNotifier {
|
|||
updateNotifications();
|
||||
_firstLoad = false;
|
||||
}
|
||||
final result = List<UserNotification>.from(_notifications.values);
|
||||
result.sort((n1, n2) {
|
||||
if (n1.dismissed == n2.dismissed) {
|
||||
return n2.timestamp.compareTo(n1.timestamp);
|
||||
final dms = <UserNotification>[];
|
||||
final connectionRequests = <UserNotification>[];
|
||||
final unread = <UserNotification>[];
|
||||
final read = <UserNotification>[];
|
||||
for (final n in _notifications.values) {
|
||||
if (n.dismissed) {
|
||||
read.add(n);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (n1.dismissed && !n2.dismissed) {
|
||||
return 1;
|
||||
switch (n.type) {
|
||||
case NotificationType.direct_message:
|
||||
dms.add(n);
|
||||
break;
|
||||
case NotificationType.follow:
|
||||
case NotificationType.follow_request:
|
||||
connectionRequests.add(n);
|
||||
break;
|
||||
default:
|
||||
unread.add(n);
|
||||
}
|
||||
}
|
||||
dms.sort();
|
||||
connectionRequests.sort();
|
||||
unread.sort();
|
||||
read.sort();
|
||||
|
||||
return -1;
|
||||
});
|
||||
|
||||
return result;
|
||||
return [...connectionRequests, ...dms, ...unread, ...read];
|
||||
}
|
||||
|
||||
void clear() {
|
||||
|
@ -52,10 +69,11 @@ class NotificationsManager extends ChangeNotifier {
|
|||
|
||||
FutureResult<List<UserNotification>, ExecError> updateNotifications() async {
|
||||
const initialPull = 100;
|
||||
final nn = <UserNotification>[];
|
||||
final notificationsFromRefresh = <UserNotification>[];
|
||||
if (_pm.pages.isEmpty) {
|
||||
final result = await _pm.initialize(initialPull);
|
||||
result.andThenSuccess((response) => nn.addAll(response.data));
|
||||
result.andThenSuccess(
|
||||
(response) => notificationsFromRefresh.addAll(response.data));
|
||||
} else {
|
||||
for (var i = 0; i < _pm.pages.length; i++) {
|
||||
if (i > 0 && i == _pm.pages.length - 1) {
|
||||
|
@ -78,8 +96,7 @@ class NotificationsManager extends ChangeNotifier {
|
|||
_logger.severe(
|
||||
'Next page returned no results and no previous page so need to re-initalize');
|
||||
} else {
|
||||
final response =
|
||||
await _clientGetNotificationsRequest(page.previous!);
|
||||
final response = await _clientGetNotificationsRequest(pd!);
|
||||
response.match(
|
||||
onSuccess: (response) => pd = response.next,
|
||||
onError: (error) =>
|
||||
|
@ -91,7 +108,8 @@ class NotificationsManager extends ChangeNotifier {
|
|||
'Previous and next page both returned nulls so need to reinitialize');
|
||||
_pm.clear();
|
||||
final result = await _pm.initialize(initialPull);
|
||||
result.andThenSuccess((response) => nn.addAll(response.data));
|
||||
result.andThenSuccess(
|
||||
(response) => notificationsFromRefresh.addAll(response.data));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -105,25 +123,38 @@ class NotificationsManager extends ChangeNotifier {
|
|||
|
||||
final response = await _clientGetNotificationsRequest(page.next!);
|
||||
response.match(
|
||||
onSuccess: (response) => nn.addAll(response.data),
|
||||
onSuccess: (response) =>
|
||||
notificationsFromRefresh.addAll(response.data),
|
||||
onError: (error) =>
|
||||
_logger.severe('Error getting previous page: $error'));
|
||||
}
|
||||
}
|
||||
|
||||
for (final n in nn) {
|
||||
_notifications[n.id] = n;
|
||||
}
|
||||
|
||||
_notifications.removeWhere(
|
||||
(key, value) => value.type == NotificationType.direct_message,
|
||||
);
|
||||
getIt<NetworkStatusService>().startNotificationUpdate();
|
||||
await getIt<ActiveProfileSelector<DirectMessageService>>()
|
||||
.activeEntry
|
||||
.andThenSuccessAsync((dms) async => await dms.updateThreads());
|
||||
|
||||
final useActualRequests = getIt<FriendicaVersionChecker>()
|
||||
.canUseFeature(RelaticaFeatures.usingActualFollowRequests);
|
||||
|
||||
if (useActualRequests) {
|
||||
await getIt<ActiveProfileSelector<FollowRequestsManager>>()
|
||||
.activeEntry
|
||||
.andThenSuccessAsync((fm) async => fm.update());
|
||||
}
|
||||
|
||||
_notifications.clear();
|
||||
|
||||
notificationsFromRefresh.removeWhere((n) =>
|
||||
n.type == NotificationType.direct_message ||
|
||||
(useActualRequests && n.type == NotificationType.follow_request));
|
||||
for (final n in notificationsFromRefresh) {
|
||||
_notifications[n.id] = n;
|
||||
}
|
||||
|
||||
getIt<NetworkStatusService>().finishNotificationUpdate();
|
||||
for (final n in buildUnreadMessageNotifications()) {
|
||||
for (final n in buildUnreadMessageNotifications(useActualRequests)) {
|
||||
_notifications[n.id] = n;
|
||||
}
|
||||
|
||||
|
@ -135,6 +166,9 @@ class NotificationsManager extends ChangeNotifier {
|
|||
loadNewerNotifications() async {
|
||||
final result = await _pm.previousFromBeginning();
|
||||
result.match(onSuccess: (response) {
|
||||
if (response.data.isEmpty) {
|
||||
return;
|
||||
}
|
||||
for (final n in response.data) {
|
||||
_notifications[n.id] = n;
|
||||
}
|
||||
|
@ -186,30 +220,39 @@ class NotificationsManager extends ChangeNotifier {
|
|||
return updateNotifications();
|
||||
}
|
||||
|
||||
List<UserNotification> buildUnreadMessageNotifications() {
|
||||
List<UserNotification> buildUnreadMessageNotifications(
|
||||
bool useActualRequests) {
|
||||
final myId = getIt<AccountsService>().currentProfile.userId;
|
||||
final result = getIt<ActiveProfileSelector<DirectMessageService>>()
|
||||
final dmsResult = getIt<ActiveProfileSelector<DirectMessageService>>()
|
||||
.activeEntry
|
||||
.value
|
||||
.getThreads(unreadyOnly: true)
|
||||
.map((t) {
|
||||
final fromAccount = t.participants.firstWhere((p) => p.id != myId);
|
||||
final latestMessage =
|
||||
t.messages.reduce((s, m) => s.createdAt > m.createdAt ? s : m);
|
||||
return UserNotification(
|
||||
id: const Uuid().v4(),
|
||||
type: NotificationType.direct_message,
|
||||
fromId: fromAccount.id,
|
||||
fromName: fromAccount.name,
|
||||
fromUrl: fromAccount.profileUrl,
|
||||
timestamp: latestMessage.createdAt,
|
||||
iid: t.parentUri,
|
||||
dismissed: false,
|
||||
content: '${fromAccount.name} sent you a direct message',
|
||||
link: '');
|
||||
}).toList();
|
||||
.andThenSuccess((d) => d.getThreads(unreadyOnly: true).map((t) {
|
||||
final fromAccount =
|
||||
t.participants.firstWhere((p) => p.id != myId);
|
||||
final latestMessage = t.messages
|
||||
.reduce((s, m) => s.createdAt > m.createdAt ? s : m);
|
||||
return UserNotification(
|
||||
id: const Uuid().v4(),
|
||||
type: NotificationType.direct_message,
|
||||
fromId: fromAccount.id,
|
||||
fromName: fromAccount.name,
|
||||
fromUrl: fromAccount.profileUrl,
|
||||
timestamp: latestMessage.createdAt,
|
||||
iid: t.parentUri,
|
||||
dismissed: false,
|
||||
content: '${fromAccount.name} sent you a direct message',
|
||||
link: '');
|
||||
}).toList())
|
||||
.getValueOrElse(() => []);
|
||||
|
||||
return result;
|
||||
final followRequestResult = !useActualRequests
|
||||
? []
|
||||
: getIt<ActiveProfileSelector<FollowRequestsManager>>()
|
||||
.activeEntry
|
||||
.andThenSuccess(
|
||||
(fm) => fm.requests.map((r) => r.toUserNotification()).toList())
|
||||
.getValueOrElse(() => []);
|
||||
|
||||
return [...dmsResult, ...followRequestResult];
|
||||
}
|
||||
|
||||
static FutureResult<PagedResponse<List<UserNotification>>, ExecError>
|
||||
|
|
|
@ -46,7 +46,9 @@ class ActiveProfileSelector<T> extends ChangeNotifier {
|
|||
T _buildNewEntry(Profile p) {
|
||||
final newEntry = _entryBuilder!(p);
|
||||
if (newEntry is ChangeNotifier) {
|
||||
newEntry.addListener(() => notifyListeners());
|
||||
newEntry.addListener(() {
|
||||
notifyListeners();
|
||||
});
|
||||
}
|
||||
|
||||
return newEntry;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import 'package:html/dom.dart';
|
||||
import 'package:html/parser.dart';
|
||||
|
||||
String toEditTextField(String htmlContentFragment) {
|
||||
String htmlToSimpleText(String htmlContentFragment) {
|
||||
final dom = parseFragment(htmlContentFragment);
|
||||
final segments = dom.nodes
|
||||
.map((n) => n is Element ? n.elementToEditText() : n.nodeToEditText())
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import 'package:flutter/widgets.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import '../globals.dart';
|
||||
import '../models/connection.dart';
|
||||
import 'snackbar_builder.dart';
|
||||
|
||||
Future<bool> openUrlStringInSystembrowser(
|
||||
|
@ -28,3 +30,15 @@ Future<bool> openUrlStringInSystembrowser(
|
|||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
Future<void> openProfileExternal(
|
||||
BuildContext context,
|
||||
Connection connection,
|
||||
) async {
|
||||
final openInBrowser =
|
||||
await showYesNoDialog(context, 'Open profile in browser?');
|
||||
if (openInBrowser == true && context.mounted) {
|
||||
await openUrlStringInSystembrowser(
|
||||
context, connection.profileUrl.toString(), 'Post');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ import 'package:flutter_test/flutter_test.dart';
|
|||
import 'package:relatica/utils/html_to_edit_text_helper.dart';
|
||||
|
||||
void testConversion(String original, String expectedOutput) {
|
||||
final output = toEditTextField(original);
|
||||
final output = htmlToSimpleText(original);
|
||||
if (output != expectedOutput) {
|
||||
print(output);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue