Merge branch 'advanced-suggestions-ordering' into 'main'

Autocomplete and Profile Load Updates

Closes #62

See merge request mysocialportal/relatica!58
This commit is contained in:
HankG 2024-06-28 14:00:30 +00:00
commit 45c2b3b490
10 changed files with 106 additions and 16 deletions

View file

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

View file

@ -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<String> onHashtagTap;
@override
Widget build(BuildContext context) {
final hashtags = getIt<HashtagService>().getMatchingHashTags(query);
final manager = context
.read<ActiveProfileSelector<EntryManagerService>>()
.activeEntry
.value;
final postTreeHashtags =
manager.getPostTreeHashtags(id).getValueOrElse(() => [])..sort();
final hashtagsFromService =
getIt<HashtagService>().getMatchingHashTags(query);
final hashtags = [...postTreeHashtags, ...hashtagsFromService];
if (hashtags.isEmpty) return const SizedBox.shrink();

View file

@ -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<Connection> onMentionUserTap;
@override
Widget build(BuildContext context) {
final users = getIt<ActiveProfileSelector<ConnectionsManager>>()
final entryManager = context
.read<ActiveProfileSelector<EntryManagerService>>()
.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<ActiveProfileSelector<ConnectionsManager>>()
.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();

View file

@ -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<PagedResponse<List<Connection>>, ExecError>

View file

@ -333,6 +333,7 @@ class _EditorScreenState extends State<EditorScreen> {
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<EditorScreen> {
trigger: '#',
optionsViewBuilder: (context, autocompleteQuery, controller) {
return HashtagAutocompleteOptions(
id: parentEntry?.id ?? '',
query: autocompleteQuery.query,
onHashtagTap: (hashtag) {
final autocomplete = MultiTriggerAutocomplete.of(context);

View file

@ -407,6 +407,7 @@ class _FilterEditorScreenState extends State<FilterEditorScreen> {
optionsViewBuilder:
(ovbContext, autocompleteQuery, controller) {
return MentionAutocompleteOptions(
id: '',
query: autocompleteQuery.query,
onMentionUserTap: (user) {
final autocomplete =
@ -480,6 +481,7 @@ class _FilterEditorScreenState extends State<FilterEditorScreen> {
optionsViewBuilder:
(ovbContext, autocompleteQuery, controller) {
return HashtagAutocompleteOptions(
id: '',
query: autocompleteQuery.query,
onHashtagTap: (hashtag) {
final autocomplete =

View file

@ -49,6 +49,7 @@ class MessagesNewThread extends StatelessWidget {
trigger: '@',
optionsViewBuilder: (context, autocompleteQuery, controller) {
return MentionAutocompleteOptions(
id: '',
query: autocompleteQuery.query,
onMentionUserTap: (user) {
final autocomplete =

View file

@ -44,8 +44,9 @@ class _UserProfileScreenState extends State<UserProfileScreen> {
.value;
final blocksManager =
context.watch<ActiveProfileSelector<BlocksManager>>().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<AccountsService>().currentProfile.userId != profile.id;
@ -54,6 +55,7 @@ class _UserProfileScreenState extends State<UserProfileScreen> {
await connectionManager.fullRefresh(profile,
withNotifications: false);
await blocksManager.updateBlock(profile);
setState(() {});
},
child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),

View file

@ -2,7 +2,7 @@ import '../../models/connection.dart';
extension ConnectionFriendicaExtensions on Connection {
static Connection fromJson(Map<String, dynamic> json) {
final status = json['following'] == 'true'
final status = json['following']
? ConnectionStatus.youFollowThem
: ConnectionStatus.none;
final name = json['name'] ?? '';

View file

@ -23,6 +23,8 @@ class EntryManagerService extends ChangeNotifier {
final _entries = <String, TimelineEntry>{};
final _parentPostIds = <String, String>{};
final _postNodes = <String, _Node>{};
final _postThreadHashtags = <String, Set<String>>{};
final _postTreeConnections = <String, Set<String>>{};
final Profile profile;
EntryManagerService(this.profile);
@ -61,6 +63,32 @@ class EntryManagerService extends ChangeNotifier {
return Result.ok(_nodeToTreeItem(postNode, profile.userId));
}
Result<List<String>, 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<List<String>, 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<TimelineEntry, ExecError> 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;