Refactor follow requests to use actual follow request system if available

codemagic-setup
Hank Grabowski 2023-03-21 14:27:38 -04:00
rodzic 1801780dcf
commit bd02a01d08
19 zmienionych plików z 438 dodań i 133 usunięć

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

@ -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: (_) =>

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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