mirror of
https://gitlab.com/mysocialportal/relatica
synced 2024-10-18 13:33:32 +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/direct_message_service.dart';
|
||||||
import 'services/entry_manager_service.dart';
|
import 'services/entry_manager_service.dart';
|
||||||
import 'services/feature_version_checker.dart';
|
import 'services/feature_version_checker.dart';
|
||||||
|
import 'services/follow_requests_manager.dart';
|
||||||
import 'services/gallery_service.dart';
|
import 'services/gallery_service.dart';
|
||||||
import 'services/hashtag_service.dart';
|
import 'services/hashtag_service.dart';
|
||||||
import 'services/interactions_manager.dart';
|
import 'services/interactions_manager.dart';
|
||||||
|
@ -83,6 +84,8 @@ Future<void> dependencyInjectionInitialization() async {
|
||||||
)));
|
)));
|
||||||
getIt.registerSingleton<ActiveProfileSelector<NotificationsManager>>(
|
getIt.registerSingleton<ActiveProfileSelector<NotificationsManager>>(
|
||||||
ActiveProfileSelector((_) => NotificationsManager()));
|
ActiveProfileSelector((_) => NotificationsManager()));
|
||||||
|
getIt.registerSingleton<ActiveProfileSelector<FollowRequestsManager>>(
|
||||||
|
ActiveProfileSelector((_) => FollowRequestsManager()));
|
||||||
getIt.registerSingleton<ActiveProfileSelector<DirectMessageService>>(
|
getIt.registerSingleton<ActiveProfileSelector<DirectMessageService>>(
|
||||||
ActiveProfileSelector((p) => DirectMessageService()));
|
ActiveProfileSelector((p) => DirectMessageService()));
|
||||||
getIt.registerSingleton<ActiveProfileSelector<InteractionsManager>>(
|
getIt.registerSingleton<ActiveProfileSelector<InteractionsManager>>(
|
||||||
|
|
|
@ -13,6 +13,7 @@ import '../models/auth/profile.dart';
|
||||||
import '../models/connection.dart';
|
import '../models/connection.dart';
|
||||||
import '../models/direct_message.dart';
|
import '../models/direct_message.dart';
|
||||||
import '../models/exec_error.dart';
|
import '../models/exec_error.dart';
|
||||||
|
import '../models/follow_request.dart';
|
||||||
import '../models/gallery_data.dart';
|
import '../models/gallery_data.dart';
|
||||||
import '../models/group_data.dart';
|
import '../models/group_data.dart';
|
||||||
import '../models/image_entry.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/image_entry_friendica_extensions.dart';
|
||||||
import '../serializers/friendica/visibility_friendica_extensions.dart';
|
import '../serializers/friendica/visibility_friendica_extensions.dart';
|
||||||
import '../serializers/mastodon/connection_mastodon_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/group_data_mastodon_extensions.dart';
|
||||||
import '../serializers/mastodon/instance_info_mastodon_extensions.dart';
|
import '../serializers/mastodon/instance_info_mastodon_extensions.dart';
|
||||||
import '../serializers/mastodon/notification_mastodon_extension.dart';
|
import '../serializers/mastodon/notification_mastodon_extension.dart';
|
||||||
|
@ -361,6 +363,22 @@ class RelationshipsClient extends FriendicaClient {
|
||||||
.execErrorCast();
|
.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(
|
FutureResult<PagedResponse<List<Connection>>, ExecError> getMyFollowers(
|
||||||
PagingData page) async {
|
PagingData page) async {
|
||||||
_logger.finest(() => 'Getting followers data with page data $page');
|
_logger.finest(() => 'Getting followers data with page data $page');
|
||||||
|
|
|
@ -70,11 +70,17 @@ class PagesManager<TResult, TID> {
|
||||||
}
|
}
|
||||||
|
|
||||||
FutureResult<PagedResponse<TResult>, ExecError> nextFromEnd() async {
|
FutureResult<PagedResponse<TResult>, ExecError> nextFromEnd() async {
|
||||||
|
if (_pages.isEmpty) {
|
||||||
|
return buildErrorResult(type: ErrorType.rangeError);
|
||||||
|
}
|
||||||
return _previousOrNext(_pages.last.id, false);
|
return _previousOrNext(_pages.last.id, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
FutureResult<PagedResponse<TResult>, ExecError>
|
FutureResult<PagedResponse<TResult>, ExecError>
|
||||||
previousFromBeginning() async {
|
previousFromBeginning() async {
|
||||||
|
if (_pages.isEmpty) {
|
||||||
|
return buildErrorResult(type: ErrorType.rangeError);
|
||||||
|
}
|
||||||
return _previousOrNext(_pages.first.id, true);
|
return _previousOrNext(_pages.first.id, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,7 @@ import 'services/auth_service.dart';
|
||||||
import 'services/connections_manager.dart';
|
import 'services/connections_manager.dart';
|
||||||
import 'services/direct_message_service.dart';
|
import 'services/direct_message_service.dart';
|
||||||
import 'services/entry_manager_service.dart';
|
import 'services/entry_manager_service.dart';
|
||||||
|
import 'services/follow_requests_manager.dart';
|
||||||
import 'services/gallery_service.dart';
|
import 'services/gallery_service.dart';
|
||||||
import 'services/hashtag_service.dart';
|
import 'services/hashtag_service.dart';
|
||||||
import 'services/interactions_manager.dart';
|
import 'services/interactions_manager.dart';
|
||||||
|
@ -84,6 +85,11 @@ class App extends StatelessWidget {
|
||||||
create: (_) =>
|
create: (_) =>
|
||||||
getIt<ActiveProfileSelector<NotificationsManager>>(),
|
getIt<ActiveProfileSelector<NotificationsManager>>(),
|
||||||
),
|
),
|
||||||
|
ChangeNotifierProvider<
|
||||||
|
ActiveProfileSelector<FollowRequestsManager>>(
|
||||||
|
create: (_) =>
|
||||||
|
getIt<ActiveProfileSelector<FollowRequestsManager>>(),
|
||||||
|
),
|
||||||
ChangeNotifierProvider<
|
ChangeNotifierProvider<
|
||||||
ActiveProfileSelector<DirectMessageService>>(
|
ActiveProfileSelector<DirectMessageService>>(
|
||||||
create: (_) =>
|
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 String id;
|
||||||
final NotificationType type;
|
final NotificationType type;
|
||||||
final String fromId;
|
final String fromId;
|
||||||
|
@ -79,4 +79,17 @@ class UserNotification {
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'UserNotification{id: $id, seen: $dismissed, fromName: $fromName, content: $content}';
|
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));
|
.andThenAsync((manager) async => await manager.getEntryById(widget.id));
|
||||||
result.match(onSuccess: (entry) {
|
result.match(onSuccess: (entry) {
|
||||||
_logger.fine('Loading status ${widget.id} information into fields');
|
_logger.fine('Loading status ${widget.id} information into fields');
|
||||||
contentController.text = toEditTextField(entry.body);
|
contentController.text = htmlToSimpleText(entry.body);
|
||||||
spoilerController.text = entry.spoilerText;
|
spoilerController.text = entry.spoilerText;
|
||||||
existingMediaItems
|
existingMediaItems
|
||||||
.addAll(entry.mediaAttachments.map((e) => e.toImageEntry()));
|
.addAll(entry.mediaAttachments.map((e) => e.toImageEntry()));
|
||||||
|
|
|
@ -1,12 +1,21 @@
|
||||||
import 'package:flutter/material.dart';
|
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:go_router/go_router.dart';
|
||||||
import 'package:provider/provider.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/login_aware_cached_network_image.dart';
|
||||||
import '../controls/padding.dart';
|
import '../controls/padding.dart';
|
||||||
|
import '../globals.dart';
|
||||||
import '../models/connection.dart';
|
import '../models/connection.dart';
|
||||||
|
import '../models/exec_error.dart';
|
||||||
|
import '../routes.dart';
|
||||||
import '../services/connections_manager.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 {
|
class FollowRequestAdjudicationScreen extends StatefulWidget {
|
||||||
final String userId;
|
final String userId;
|
||||||
|
@ -24,22 +33,38 @@ class _FollowRequestAdjudicationScreenState
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final manager = context
|
final fm =
|
||||||
|
getIt<ActiveProfileSelector<FollowRequestsManager>>().activeEntry.value;
|
||||||
|
final cm = context
|
||||||
.watch<ActiveProfileSelector<ConnectionsManager>>()
|
.watch<ActiveProfileSelector<ConnectionsManager>>()
|
||||||
.activeEntry
|
.activeEntry
|
||||||
.value;
|
.value;
|
||||||
final connResult = manager.getById(widget.userId);
|
|
||||||
late final Widget body;
|
late final Result<Connection, ExecError> result;
|
||||||
if (connResult.isFailure) {
|
if (getIt<FriendicaVersionChecker>()
|
||||||
body = Text('Error getting contact information: ${connResult.error}');
|
.canUseFeature(RelaticaFeatures.usingActualFollowRequests)) {
|
||||||
|
result = fm
|
||||||
|
.getByUserId(widget.userId)
|
||||||
|
.mapValue((request) => request.connection);
|
||||||
|
} else {
|
||||||
|
result = cm.getById(widget.userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
final contact = connResult.value;
|
late final Widget body;
|
||||||
switch (contact.status) {
|
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.theyFollowYou:
|
||||||
case ConnectionStatus.youFollowThem:
|
case ConnectionStatus.youFollowThem:
|
||||||
case ConnectionStatus.none:
|
case ConnectionStatus.none:
|
||||||
body = _buildMainPanel(context, manager, contact);
|
body = _buildMainPanel(context, contact, cm, fm);
|
||||||
break;
|
break;
|
||||||
case ConnectionStatus.mutual:
|
case ConnectionStatus.mutual:
|
||||||
body = const Text('Already allowed them to connect');
|
body = const Text('Already allowed them to connect');
|
||||||
|
@ -49,6 +74,7 @@ class _FollowRequestAdjudicationScreenState
|
||||||
body = Text('Invalid state, nothing to do here: ${contact.status}');
|
body = Text('Invalid state, nothing to do here: ${contact.status}');
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
|
@ -63,7 +89,11 @@ class _FollowRequestAdjudicationScreenState
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildMainPanel(
|
Widget _buildMainPanel(
|
||||||
BuildContext context, ConnectionsManager manager, Connection contact) {
|
BuildContext context,
|
||||||
|
Connection contact,
|
||||||
|
ConnectionsManager connectionsManager,
|
||||||
|
FollowRequestsManager followRequestsManager,
|
||||||
|
) {
|
||||||
// Options are:
|
// Options are:
|
||||||
// Accept and follow back
|
// Accept and follow back
|
||||||
// Accept and don't follow back
|
// Accept and don't follow back
|
||||||
|
@ -71,7 +101,8 @@ class _FollowRequestAdjudicationScreenState
|
||||||
// Back with no action
|
// Back with no action
|
||||||
// Calling method should check if completed (true) or not (false) to decide if updating their view of that item
|
// Calling method should check if completed (true) or not (false) to decide if updating their view of that item
|
||||||
|
|
||||||
return Column(
|
return SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
LoginAwareCachedNetworkImage(imageUrl: contact.avatarUrl.toString()),
|
LoginAwareCachedNetworkImage(imageUrl: contact.avatarUrl.toString()),
|
||||||
|
@ -80,41 +111,92 @@ class _FollowRequestAdjudicationScreenState
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
contact.name,
|
'${contact.name}(${contact.handle})',
|
||||||
style: Theme.of(context).textTheme.titleLarge,
|
style: Theme.of(context).textTheme.titleLarge,
|
||||||
),
|
),
|
||||||
const HorizontalPadding(),
|
const HorizontalPadding(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const VerticalPadding(),
|
const VerticalPadding(),
|
||||||
|
Wrap(
|
||||||
|
runSpacing: 5.0,
|
||||||
|
spacing: 5.0,
|
||||||
|
alignment: WrapAlignment.center,
|
||||||
|
children: [
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: processing
|
onPressed: processing
|
||||||
? null
|
? null
|
||||||
: () async => await accept(manager, contact, true),
|
: () async => await accept(connectionsManager,
|
||||||
|
followRequestsManager, contact, true),
|
||||||
child: const Text('Accept and follow back'),
|
child: const Text('Accept and follow back'),
|
||||||
),
|
),
|
||||||
const VerticalPadding(),
|
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed:
|
onPressed: processing
|
||||||
processing ? null : () async => accept(manager, contact, false),
|
? 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"),
|
child: const Text("Accept but don't follow back"),
|
||||||
),
|
),
|
||||||
const VerticalPadding(),
|
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: processing ? null : () async => reject(manager, contact),
|
onPressed: processing
|
||||||
|
? null
|
||||||
|
: () async => reject(
|
||||||
|
connectionsManager, followRequestsManager, contact),
|
||||||
child: const Text('Reject'),
|
child: const Text('Reject'),
|
||||||
),
|
),
|
||||||
const VerticalPadding(),
|
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: processing ? null : () async => ignore(manager, contact),
|
onPressed: processing
|
||||||
|
? null
|
||||||
|
: () async => ignore(
|
||||||
|
connectionsManager, followRequestsManager, contact),
|
||||||
child: const Text('Ignore (Rejects but user cannot ask again)'),
|
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(
|
Future<void> accept(
|
||||||
ConnectionsManager manager,
|
ConnectionsManager manager,
|
||||||
|
FollowRequestsManager followRequestsManager,
|
||||||
Connection contact,
|
Connection contact,
|
||||||
bool followBack,
|
bool followBack,
|
||||||
) async {
|
) async {
|
||||||
|
@ -127,6 +209,8 @@ class _FollowRequestAdjudicationScreenState
|
||||||
await manager.follow(contact);
|
await manager.follow(contact);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_performUpdates(followRequestsManager);
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
processing = false;
|
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(() {
|
setState(() {
|
||||||
processing = true;
|
processing = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
await manager.rejectFollowRequest(contact);
|
await manager.rejectFollowRequest(contact);
|
||||||
|
|
||||||
|
_performUpdates(followRequestsManager);
|
||||||
setState(() {
|
setState(() {
|
||||||
processing = false;
|
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(() {
|
setState(() {
|
||||||
processing = true;
|
processing = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
await manager.ignoreFollowRequest(contact);
|
await manager.ignoreFollowRequest(contact);
|
||||||
|
_performUpdates(followRequestsManager);
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
processing = false;
|
processing = false;
|
||||||
|
@ -167,4 +255,11 @@ class _FollowRequestAdjudicationScreenState
|
||||||
context.pop();
|
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> {
|
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;
|
var isUpdating = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -107,7 +95,7 @@ class _UserProfileScreenState extends State<UserProfileScreen> {
|
||||||
Text(
|
Text(
|
||||||
'#Followers: ${profile.followerCount} followers, #Following, ${profile.followingCount}, #Statuses: ${profile.statusesCount}'),
|
'#Followers: ${profile.followerCount} followers, #Following, ${profile.followingCount}, #Statuses: ${profile.statusesCount}'),
|
||||||
const VerticalPadding(),
|
const VerticalPadding(),
|
||||||
Text('Last Status: ${profile.lastStatus}'),
|
Text('Last Status: ${profile.lastStatus ?? "Unknown"}'),
|
||||||
const VerticalPadding(),
|
const VerticalPadding(),
|
||||||
if (profile.status == ConnectionStatus.mutual ||
|
if (profile.status == ConnectionStatus.mutual ||
|
||||||
profile.status == ConnectionStatus.youFollowThem)
|
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 '../../models/link_preview_data.dart';
|
||||||
|
import '../../utils/html_to_edit_text_helper.dart';
|
||||||
|
import '../../utils/string_utils.dart';
|
||||||
|
|
||||||
extension LinkPreviewExtension on LinkPreviewData {
|
extension LinkPreviewExtension on LinkPreviewData {
|
||||||
String toBodyAttachment() {
|
String toBodyAttachment() {
|
||||||
|
@ -9,8 +8,9 @@ extension LinkPreviewExtension on LinkPreviewData {
|
||||||
return "[attachment type='link' url='$link' title='$title']$description[/attachment]";
|
return "[attachment type='link' url='$link' title='$title']$description[/attachment]";
|
||||||
}
|
}
|
||||||
|
|
||||||
final sanitizedTitle = toEditTextField(title).stripHyperlinks();
|
final sanitizedTitle = htmlToSimpleText(title).stripHyperlinks();
|
||||||
final sanitizedDescription = toEditTextField(description).stripHyperlinks();
|
final sanitizedDescription =
|
||||||
|
htmlToSimpleText(description).stripHyperlinks();
|
||||||
|
|
||||||
return "[attachment type='link' url='$link' title='$sanitizedTitle' image='$selectedImageUrl']$sanitizedDescription[/attachment]";
|
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 '../../services/connections_manager.dart';
|
||||||
import '../../utils/active_profile_selector.dart';
|
import '../../utils/active_profile_selector.dart';
|
||||||
import '../../utils/dateutils.dart';
|
import '../../utils/dateutils.dart';
|
||||||
|
import '../../utils/html_to_edit_text_helper.dart';
|
||||||
|
import '../../utils/string_utils.dart';
|
||||||
import 'connection_mastodon_extensions.dart';
|
import 'connection_mastodon_extensions.dart';
|
||||||
import 'timeline_entry_mastodon_extensions.dart';
|
import 'timeline_entry_mastodon_extensions.dart';
|
||||||
|
|
||||||
|
@ -32,10 +34,10 @@ extension NotificationMastodonExtension on UserNotification {
|
||||||
var content = '';
|
var content = '';
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case NotificationType.follow:
|
case NotificationType.follow:
|
||||||
content = '${from.name} is now following you';
|
content = '${from.name}(${from.handle}) is now following you';
|
||||||
break;
|
break;
|
||||||
case NotificationType.follow_request:
|
case NotificationType.follow_request:
|
||||||
content = '${from.name} submitted a follow request ';
|
content = '${from.name}(${from.handle}) submitted a follow request ';
|
||||||
break;
|
break;
|
||||||
case NotificationType.unknown:
|
case NotificationType.unknown:
|
||||||
content = '${from.name} has unknown interaction notification';
|
content = '${from.name} has unknown interaction notification';
|
||||||
|
@ -59,7 +61,8 @@ extension NotificationMastodonExtension on UserNotification {
|
||||||
final shareInfo = status.reshareAuthorId.isNotEmpty
|
final shareInfo = status.reshareAuthorId.isNotEmpty
|
||||||
? "reshare of ${status.reshareAuthor}'s"
|
? "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;
|
break;
|
||||||
case NotificationType.direct_message:
|
case NotificationType.direct_message:
|
||||||
// this is a Relatica internal type so nothing to do here
|
// this is a Relatica internal type so nothing to do here
|
||||||
|
|
|
@ -8,6 +8,7 @@ import '../models/friendica_version.dart';
|
||||||
enum RelaticaFeatures {
|
enum RelaticaFeatures {
|
||||||
postSpoilerText,
|
postSpoilerText,
|
||||||
statusEditing,
|
statusEditing,
|
||||||
|
usingActualFollowRequests,
|
||||||
}
|
}
|
||||||
|
|
||||||
class FriendicaVersionChecker {
|
class FriendicaVersionChecker {
|
||||||
|
@ -46,5 +47,6 @@ class FriendicaVersionChecker {
|
||||||
static final featureVersionRequirement = <RelaticaFeatures, FriendicaVersion>{
|
static final featureVersionRequirement = <RelaticaFeatures, FriendicaVersion>{
|
||||||
RelaticaFeatures.postSpoilerText: v2023_03,
|
RelaticaFeatures.postSpoilerText: v2023_03,
|
||||||
RelaticaFeatures.statusEditing: 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 '../globals.dart';
|
||||||
import '../models/exec_error.dart';
|
import '../models/exec_error.dart';
|
||||||
import '../models/user_notification.dart';
|
import '../models/user_notification.dart';
|
||||||
|
import '../serializers/mastodon/follow_request_mastodon_extensions.dart';
|
||||||
import '../utils/active_profile_selector.dart';
|
import '../utils/active_profile_selector.dart';
|
||||||
import 'auth_service.dart';
|
import 'auth_service.dart';
|
||||||
import 'direct_message_service.dart';
|
import 'direct_message_service.dart';
|
||||||
|
import 'feature_version_checker.dart';
|
||||||
|
import 'follow_requests_manager.dart';
|
||||||
import 'network_status_service.dart';
|
import 'network_status_service.dart';
|
||||||
|
|
||||||
class NotificationsManager extends ChangeNotifier {
|
class NotificationsManager extends ChangeNotifier {
|
||||||
|
@ -29,20 +32,34 @@ class NotificationsManager extends ChangeNotifier {
|
||||||
updateNotifications();
|
updateNotifications();
|
||||||
_firstLoad = false;
|
_firstLoad = false;
|
||||||
}
|
}
|
||||||
final result = List<UserNotification>.from(_notifications.values);
|
final dms = <UserNotification>[];
|
||||||
result.sort((n1, n2) {
|
final connectionRequests = <UserNotification>[];
|
||||||
if (n1.dismissed == n2.dismissed) {
|
final unread = <UserNotification>[];
|
||||||
return n2.timestamp.compareTo(n1.timestamp);
|
final read = <UserNotification>[];
|
||||||
|
for (final n in _notifications.values) {
|
||||||
|
if (n.dismissed) {
|
||||||
|
read.add(n);
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (n1.dismissed && !n2.dismissed) {
|
switch (n.type) {
|
||||||
return 1;
|
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 [...connectionRequests, ...dms, ...unread, ...read];
|
||||||
});
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void clear() {
|
void clear() {
|
||||||
|
@ -52,10 +69,11 @@ class NotificationsManager extends ChangeNotifier {
|
||||||
|
|
||||||
FutureResult<List<UserNotification>, ExecError> updateNotifications() async {
|
FutureResult<List<UserNotification>, ExecError> updateNotifications() async {
|
||||||
const initialPull = 100;
|
const initialPull = 100;
|
||||||
final nn = <UserNotification>[];
|
final notificationsFromRefresh = <UserNotification>[];
|
||||||
if (_pm.pages.isEmpty) {
|
if (_pm.pages.isEmpty) {
|
||||||
final result = await _pm.initialize(initialPull);
|
final result = await _pm.initialize(initialPull);
|
||||||
result.andThenSuccess((response) => nn.addAll(response.data));
|
result.andThenSuccess(
|
||||||
|
(response) => notificationsFromRefresh.addAll(response.data));
|
||||||
} else {
|
} else {
|
||||||
for (var i = 0; i < _pm.pages.length; i++) {
|
for (var i = 0; i < _pm.pages.length; i++) {
|
||||||
if (i > 0 && i == _pm.pages.length - 1) {
|
if (i > 0 && i == _pm.pages.length - 1) {
|
||||||
|
@ -78,8 +96,7 @@ class NotificationsManager extends ChangeNotifier {
|
||||||
_logger.severe(
|
_logger.severe(
|
||||||
'Next page returned no results and no previous page so need to re-initalize');
|
'Next page returned no results and no previous page so need to re-initalize');
|
||||||
} else {
|
} else {
|
||||||
final response =
|
final response = await _clientGetNotificationsRequest(pd!);
|
||||||
await _clientGetNotificationsRequest(page.previous!);
|
|
||||||
response.match(
|
response.match(
|
||||||
onSuccess: (response) => pd = response.next,
|
onSuccess: (response) => pd = response.next,
|
||||||
onError: (error) =>
|
onError: (error) =>
|
||||||
|
@ -91,7 +108,8 @@ class NotificationsManager extends ChangeNotifier {
|
||||||
'Previous and next page both returned nulls so need to reinitialize');
|
'Previous and next page both returned nulls so need to reinitialize');
|
||||||
_pm.clear();
|
_pm.clear();
|
||||||
final result = await _pm.initialize(initialPull);
|
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!);
|
final response = await _clientGetNotificationsRequest(page.next!);
|
||||||
response.match(
|
response.match(
|
||||||
onSuccess: (response) => nn.addAll(response.data),
|
onSuccess: (response) =>
|
||||||
|
notificationsFromRefresh.addAll(response.data),
|
||||||
onError: (error) =>
|
onError: (error) =>
|
||||||
_logger.severe('Error getting previous page: $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();
|
getIt<NetworkStatusService>().startNotificationUpdate();
|
||||||
await getIt<ActiveProfileSelector<DirectMessageService>>()
|
await getIt<ActiveProfileSelector<DirectMessageService>>()
|
||||||
.activeEntry
|
.activeEntry
|
||||||
.andThenSuccessAsync((dms) async => await dms.updateThreads());
|
.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();
|
getIt<NetworkStatusService>().finishNotificationUpdate();
|
||||||
for (final n in buildUnreadMessageNotifications()) {
|
for (final n in buildUnreadMessageNotifications(useActualRequests)) {
|
||||||
_notifications[n.id] = n;
|
_notifications[n.id] = n;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -135,6 +166,9 @@ class NotificationsManager extends ChangeNotifier {
|
||||||
loadNewerNotifications() async {
|
loadNewerNotifications() async {
|
||||||
final result = await _pm.previousFromBeginning();
|
final result = await _pm.previousFromBeginning();
|
||||||
result.match(onSuccess: (response) {
|
result.match(onSuccess: (response) {
|
||||||
|
if (response.data.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
for (final n in response.data) {
|
for (final n in response.data) {
|
||||||
_notifications[n.id] = n;
|
_notifications[n.id] = n;
|
||||||
}
|
}
|
||||||
|
@ -186,16 +220,16 @@ class NotificationsManager extends ChangeNotifier {
|
||||||
return updateNotifications();
|
return updateNotifications();
|
||||||
}
|
}
|
||||||
|
|
||||||
List<UserNotification> buildUnreadMessageNotifications() {
|
List<UserNotification> buildUnreadMessageNotifications(
|
||||||
|
bool useActualRequests) {
|
||||||
final myId = getIt<AccountsService>().currentProfile.userId;
|
final myId = getIt<AccountsService>().currentProfile.userId;
|
||||||
final result = getIt<ActiveProfileSelector<DirectMessageService>>()
|
final dmsResult = getIt<ActiveProfileSelector<DirectMessageService>>()
|
||||||
.activeEntry
|
.activeEntry
|
||||||
.value
|
.andThenSuccess((d) => d.getThreads(unreadyOnly: true).map((t) {
|
||||||
.getThreads(unreadyOnly: true)
|
final fromAccount =
|
||||||
.map((t) {
|
t.participants.firstWhere((p) => p.id != myId);
|
||||||
final fromAccount = t.participants.firstWhere((p) => p.id != myId);
|
final latestMessage = t.messages
|
||||||
final latestMessage =
|
.reduce((s, m) => s.createdAt > m.createdAt ? s : m);
|
||||||
t.messages.reduce((s, m) => s.createdAt > m.createdAt ? s : m);
|
|
||||||
return UserNotification(
|
return UserNotification(
|
||||||
id: const Uuid().v4(),
|
id: const Uuid().v4(),
|
||||||
type: NotificationType.direct_message,
|
type: NotificationType.direct_message,
|
||||||
|
@ -207,9 +241,18 @@ class NotificationsManager extends ChangeNotifier {
|
||||||
dismissed: false,
|
dismissed: false,
|
||||||
content: '${fromAccount.name} sent you a direct message',
|
content: '${fromAccount.name} sent you a direct message',
|
||||||
link: '');
|
link: '');
|
||||||
}).toList();
|
}).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>
|
static FutureResult<PagedResponse<List<UserNotification>>, ExecError>
|
||||||
|
|
|
@ -46,7 +46,9 @@ class ActiveProfileSelector<T> extends ChangeNotifier {
|
||||||
T _buildNewEntry(Profile p) {
|
T _buildNewEntry(Profile p) {
|
||||||
final newEntry = _entryBuilder!(p);
|
final newEntry = _entryBuilder!(p);
|
||||||
if (newEntry is ChangeNotifier) {
|
if (newEntry is ChangeNotifier) {
|
||||||
newEntry.addListener(() => notifyListeners());
|
newEntry.addListener(() {
|
||||||
|
notifyListeners();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return newEntry;
|
return newEntry;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import 'package:html/dom.dart';
|
import 'package:html/dom.dart';
|
||||||
import 'package:html/parser.dart';
|
import 'package:html/parser.dart';
|
||||||
|
|
||||||
String toEditTextField(String htmlContentFragment) {
|
String htmlToSimpleText(String htmlContentFragment) {
|
||||||
final dom = parseFragment(htmlContentFragment);
|
final dom = parseFragment(htmlContentFragment);
|
||||||
final segments = dom.nodes
|
final segments = dom.nodes
|
||||||
.map((n) => n is Element ? n.elementToEditText() : n.nodeToEditText())
|
.map((n) => n is Element ? n.elementToEditText() : n.nodeToEditText())
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
|
import '../globals.dart';
|
||||||
|
import '../models/connection.dart';
|
||||||
import 'snackbar_builder.dart';
|
import 'snackbar_builder.dart';
|
||||||
|
|
||||||
Future<bool> openUrlStringInSystembrowser(
|
Future<bool> openUrlStringInSystembrowser(
|
||||||
|
@ -28,3 +30,15 @@ Future<bool> openUrlStringInSystembrowser(
|
||||||
}
|
}
|
||||||
return true;
|
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';
|
import 'package:relatica/utils/html_to_edit_text_helper.dart';
|
||||||
|
|
||||||
void testConversion(String original, String expectedOutput) {
|
void testConversion(String original, String expectedOutput) {
|
||||||
final output = toEditTextField(original);
|
final output = htmlToSimpleText(original);
|
||||||
if (output != expectedOutput) {
|
if (output != expectedOutput) {
|
||||||
print(output);
|
print(output);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue