diff --git a/CHANGELOG.md b/CHANGELOG.md index b653521..bd95f4f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,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/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/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;