diff --git a/CHANGELOG.md b/CHANGELOG.md index 1da4e8c..bd95f4f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,7 +24,10 @@ * Multiple profiles from the same server now works again. Affected users have to use the new "Clear All" button to clear out existing credentials and re-add them all to fix though. ([Feature #72](https://gitlab.com/mysocialportal/relatica/-/issues/72)) -* + * Fix empty profiles and/or sometimes lack of bidirectional contact data by always pulling profile data on refresh + requests and adding explicit redraw of panel after + user requests refresh ([Issue #36](https://gitlab.com/mysocialportal/relatica/-/issues/36) + and [Issue #62](https://gitlab.com/mysocialportal/relatica/-/issues/62)) * New Features * Shows the network of the post/comment ([Feature #82](https://gitlab.com/mysocialportal/relatica/-/issues/82)) * User configurable ability to limit reacting to, commenting on, or resharing posts by network @@ -35,7 +38,9 @@ * Ability to turn off Spoiler Alert/CWs at the application level. Defaults to on. ([Feature #42](https://gitlab.com/mysocialportal/relatica/-/issues/42)) * Throws a confirm dialog box up if adding a comment to a post/comment over 30 days - old. ([Feature #58](https://gitlab.com/mysocialportal/relatica/-/issues/58)) + old. ([Feature #58](https://gitlab.com/mysocialportal/relatica/-/issues/58)) + * Autocomplete now lists hashtags and accounts that are used in a post or post above the rest of the + results. ([Feature #28](https://gitlab.com/mysocialportal/relatica/-/issues/28)) ## Version 0.10.1 (beta) diff --git a/lib/controls/autocomplete/hashtag_autocomplete_options.dart b/lib/controls/autocomplete/hashtag_autocomplete_options.dart index b45dc0b..0fc09ce 100644 --- a/lib/controls/autocomplete/hashtag_autocomplete_options.dart +++ b/lib/controls/autocomplete/hashtag_autocomplete_options.dart @@ -1,21 +1,34 @@ import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; import '../../globals.dart'; +import '../../services/entry_manager_service.dart'; import '../../services/hashtag_service.dart'; +import '../../utils/active_profile_selector.dart'; class HashtagAutocompleteOptions extends StatelessWidget { const HashtagAutocompleteOptions({ super.key, + required this.id, required this.query, required this.onHashtagTap, }); + final String id; final String query; final ValueSetter onHashtagTap; @override Widget build(BuildContext context) { - final hashtags = getIt().getMatchingHashTags(query); + final manager = context + .read>() + .activeEntry + .value; + final postTreeHashtags = + manager.getPostTreeHashtags(id).getValueOrElse(() => [])..sort(); + final hashtagsFromService = + getIt().getMatchingHashTags(query); + final hashtags = [...postTreeHashtags, ...hashtagsFromService]; if (hashtags.isEmpty) return const SizedBox.shrink(); diff --git a/lib/controls/autocomplete/mention_autocomplete_options.dart b/lib/controls/autocomplete/mention_autocomplete_options.dart index d7f83d4..de3bdd4 100644 --- a/lib/controls/autocomplete/mention_autocomplete_options.dart +++ b/lib/controls/autocomplete/mention_autocomplete_options.dart @@ -1,9 +1,10 @@ import 'package:flutter/material.dart'; import 'package:logging/logging.dart'; +import 'package:provider/provider.dart'; -import '../../globals.dart'; import '../../models/connection.dart'; import '../../services/connections_manager.dart'; +import '../../services/entry_manager_service.dart'; import '../../utils/active_profile_selector.dart'; import '../image_control.dart'; @@ -12,24 +13,39 @@ class MentionAutocompleteOptions extends StatelessWidget { const MentionAutocompleteOptions({ super.key, + required this.id, required this.query, required this.onMentionUserTap, }); + final String id; final String query; final ValueSetter onMentionUserTap; @override Widget build(BuildContext context) { - final users = getIt>() + final entryManager = context + .read>() .activeEntry - .andThenSuccess((manager) => manager.getKnownUsersByName(query)) - .fold( - onSuccess: (users) => users, - onError: (error) { - _logger.severe('Error getting users list: $error'); - return []; - }); + .value; + + final connectionManager = context + .read>() + .activeEntry + .value; + + final postTreeUsers = entryManager + .getPostTreeConnectionIds(id) + .getValueOrElse(() => []) + .map((id) => connectionManager.getById(id)) + .where((result) => result.isSuccess) + .map((result) => result.value) + .toList() + ..sort((u1, u2) => u1.name.compareTo(u2.name)); + + final knownUsers = connectionManager.getKnownUsersByName(query); + + final users = [...postTreeUsers, ...knownUsers]; if (users.isEmpty) return const SizedBox.shrink(); diff --git a/lib/friendica_client/friendica_client.dart b/lib/friendica_client/friendica_client.dart index f72d85f..e74c1d6 100644 --- a/lib/friendica_client/friendica_client.dart +++ b/lib/friendica_client/friendica_client.dart @@ -554,6 +554,14 @@ class RelationshipsClient extends FriendicaClient { _networkStatusService.startConnectionUpdateStatus(); final myId = profile.userId; final id = int.parse(connection.id); + final connectionUpdateUrl = + Uri.parse('https://$serverName/api/v1/accounts/$id'); + final updatedConnection = await _getApiRequest(connectionUpdateUrl).fold( + onSuccess: (json) => ConnectionMastodonExtensions.fromJson(json), + onError: (error) { + _logger.severe('Error getting connection for $id'); + return connection; + }); final paging = '?min_id=${id - 1}&max_id=${id + 1}'; final baseUrl = 'https://$serverName/api/v1/accounts/$myId'; final following = @@ -581,7 +589,7 @@ class RelationshipsClient extends FriendicaClient { } _networkStatusService.finishConnectionUpdateStatus(); - return Result.ok(connection.copy(status: status)); + return Result.ok(updatedConnection.copy(status: status)); } FutureResult>, ExecError> diff --git a/lib/screens/editor.dart b/lib/screens/editor.dart index f750633..a030f6c 100644 --- a/lib/screens/editor.dart +++ b/lib/screens/editor.dart @@ -333,6 +333,7 @@ class _EditorScreenState extends State { trigger: '@', optionsViewBuilder: (context, autocompleteQuery, controller) { return MentionAutocompleteOptions( + id: parentEntry?.id ?? '', query: autocompleteQuery.query, onMentionUserTap: (user) { final autocomplete = MultiTriggerAutocomplete.of(context); @@ -345,6 +346,7 @@ class _EditorScreenState extends State { trigger: '#', optionsViewBuilder: (context, autocompleteQuery, controller) { return HashtagAutocompleteOptions( + id: parentEntry?.id ?? '', query: autocompleteQuery.query, onHashtagTap: (hashtag) { final autocomplete = MultiTriggerAutocomplete.of(context); diff --git a/lib/screens/filter_editor_screen.dart b/lib/screens/filter_editor_screen.dart index 244a9d8..b772bf4 100644 --- a/lib/screens/filter_editor_screen.dart +++ b/lib/screens/filter_editor_screen.dart @@ -407,6 +407,7 @@ class _FilterEditorScreenState extends State { optionsViewBuilder: (ovbContext, autocompleteQuery, controller) { return MentionAutocompleteOptions( + id: '', query: autocompleteQuery.query, onMentionUserTap: (user) { final autocomplete = @@ -480,6 +481,7 @@ class _FilterEditorScreenState extends State { optionsViewBuilder: (ovbContext, autocompleteQuery, controller) { return HashtagAutocompleteOptions( + id: '', query: autocompleteQuery.query, onHashtagTap: (hashtag) { final autocomplete = diff --git a/lib/screens/messages_new_thread.dart b/lib/screens/messages_new_thread.dart index 55ff738..74edf71 100644 --- a/lib/screens/messages_new_thread.dart +++ b/lib/screens/messages_new_thread.dart @@ -49,6 +49,7 @@ class MessagesNewThread extends StatelessWidget { trigger: '@', optionsViewBuilder: (context, autocompleteQuery, controller) { return MentionAutocompleteOptions( + id: '', query: autocompleteQuery.query, onMentionUserTap: (user) { final autocomplete = diff --git a/lib/screens/user_profile_screen.dart b/lib/screens/user_profile_screen.dart index d58a6cb..2f694d2 100644 --- a/lib/screens/user_profile_screen.dart +++ b/lib/screens/user_profile_screen.dart @@ -44,8 +44,9 @@ class _UserProfileScreenState extends State { .value; final blocksManager = context.watch>().activeEntry.value; - final body = - connectionManager.getById(widget.userId).fold(onSuccess: (profile) { + final body = connectionManager + .getById(widget.userId, forceUpdate: true) + .fold(onSuccess: (profile) { final notMyProfile = getIt().currentProfile.userId != profile.id; @@ -54,6 +55,7 @@ class _UserProfileScreenState extends State { await connectionManager.fullRefresh(profile, withNotifications: false); await blocksManager.updateBlock(profile); + setState(() {}); }, child: SingleChildScrollView( physics: const AlwaysScrollableScrollPhysics(), diff --git a/lib/serializers/friendica/connection_friendica_extensions.dart b/lib/serializers/friendica/connection_friendica_extensions.dart index 99049f8..e05c7ce 100644 --- a/lib/serializers/friendica/connection_friendica_extensions.dart +++ b/lib/serializers/friendica/connection_friendica_extensions.dart @@ -2,7 +2,7 @@ import '../../models/connection.dart'; extension ConnectionFriendicaExtensions on Connection { static Connection fromJson(Map json) { - final status = json['following'] == 'true' + final status = json['following'] ? ConnectionStatus.youFollowThem : ConnectionStatus.none; final name = json['name'] ?? ''; diff --git a/lib/services/entry_manager_service.dart b/lib/services/entry_manager_service.dart index b85ff4e..3460438 100644 --- a/lib/services/entry_manager_service.dart +++ b/lib/services/entry_manager_service.dart @@ -23,6 +23,8 @@ class EntryManagerService extends ChangeNotifier { final _entries = {}; final _parentPostIds = {}; final _postNodes = {}; + final _postThreadHashtags = >{}; + final _postTreeConnections = >{}; final Profile profile; EntryManagerService(this.profile); @@ -61,6 +63,32 @@ class EntryManagerService extends ChangeNotifier { return Result.ok(_nodeToTreeItem(postNode, profile.userId)); } + Result, ExecError> getPostTreeHashtags(String id) { + final postId = _getPostRootNode(id)?.id ?? ''; + if (postId.isEmpty) { + return buildErrorResult( + type: ErrorType.notFound, + message: 'Root Post ID not found for $id', + ); + } + final hashtags = _postThreadHashtags[postId]?.toList() ?? []; + + return Result.ok(hashtags); + } + + Result, ExecError> getPostTreeConnectionIds(String id) { + final postId = _getPostRootNode(id)?.id ?? ''; + if (postId.isEmpty) { + return buildErrorResult( + type: ErrorType.notFound, + message: 'Root Post ID not found for $id', + ); + } + final hashtags = _postTreeConnections[postId]?.toList() ?? []; + + return Result.ok(hashtags); + } + Result getEntryById(String id) { if (_entries.containsKey(id)) { return Result.ok(_entries[id]!); @@ -353,6 +381,11 @@ class EntryManagerService extends ChangeNotifier { if (item.parentId.isEmpty) { final postNode = _postNodes.putIfAbsent(item.id, () => _Node(item.id)); + final pth = _postThreadHashtags.putIfAbsent(item.id, () => {}); + final ptc = _postTreeConnections.putIfAbsent(item.id, () => {}); + pth.addAll(item.tags); + ptc.add(item.authorId); + ptc.add(item.parentAuthorId); postNodesToReturn.add(postNode); allSeenItems.remove(item); } else { @@ -364,6 +397,14 @@ class EntryManagerService extends ChangeNotifier { 'Error finding parent ${item.parentId} for entry ${item.id}'); continue; } + final pth = + _postThreadHashtags.putIfAbsent(parentParentPostId!, () => {}); + final ptc = + _postTreeConnections.putIfAbsent(parentParentPostId, () => {}); + pth.addAll(item.tags); + ptc.add(item.authorId); + ptc.add(item.parentAuthorId); + final parentPostNode = _postNodes[parentParentPostId]!; postNodesToReturn.add(parentPostNode); _parentPostIds[item.id] = parentPostNode.id;