Skip to content
Open
1,105 changes: 1,105 additions & 0 deletions android/app/src/main/AndroidManifest.xml

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions lib/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,7 @@
"settings_debug_clearFeedCache": "Clear feed cache",
"settings_debug_clearTags": "Clear user tags",
"settings_debug_clearOauth": "Clear Oauth identifiers",
"settings_debug_clearInstancesCache": "Clear federated instances cache",
"settings_debug_log": "Log",
"profiles": "Profiles",
"profile_switch": "Switch profiles",
Expand Down
84 changes: 83 additions & 1 deletion lib/src/api/search.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import 'package:extended_image/extended_image.dart';
import 'package:interstellar/src/api/client.dart';
import 'package:interstellar/src/controller/server.dart';
import 'package:interstellar/src/models/comment.dart';
import 'package:interstellar/src/models/community.dart';
import 'package:interstellar/src/models/feed.dart';
import 'package:interstellar/src/models/post.dart';
import 'package:interstellar/src/models/search.dart';
import 'package:interstellar/src/models/user.dart';
import 'package:interstellar/src/screens/explore/explore_screen.dart';
import 'package:interstellar/src/utils/utils.dart';

class APISearch {
APISearch(this.client);
Expand All @@ -17,7 +24,7 @@ class APISearch {
}) async {
switch (client.software) {
case ServerSoftware.mbin:
const path = '/search';
const path = '/search/v2';

final response = await client.get(
path,
Expand Down Expand Up @@ -98,4 +105,79 @@ class APISearch {
);
}
}

Future<dynamic> resolveObject(String search) async {
try {
switch (client.software) {
case ServerSoftware.mbin:
const path = '/search/v2';

final response = await client.get(
path,
queryParams: {'q': search, 'onlyAP': 'true'},
);

// get first of the ap objects found.
return SearchListModel.fromMbin(response.bodyJson).items.firstOrNull;

case ServerSoftware.lemmy:
const path = '/resolve_object';

final response = await client.get(path, queryParams: {'q': search});

final json = response.bodyJson;
if (json['comment'] != null) {
return CommentModel.fromLemmy(
json['comment']! as JsonMap,
langCodeIdPairs: await client.languageCodeIdPairs(),
);
} else if (json['post'] != null) {
return PostModel.fromLemmy(
json['post']! as JsonMap,
langCodeIdPairs: await client.languageCodeIdPairs(),
);
} else if (json['community'] != null) {
return DetailedCommunityModel.fromLemmy(
json['community']! as JsonMap,
);
} else if (json['person'] != null) {
return DetailedUserModel.fromLemmy(json['person']! as JsonMap);
}
return null;

case ServerSoftware.piefed:
const path = '/resolve_object';

final response = await client.get(path, queryParams: {'q': search});

final json = response.bodyJson;
if (json['comment'] != null) {
return CommentModel.fromPiefed(
json['comment']! as JsonMap,
langCodeIdPairs: await client.languageCodeIdPairs(),
);
} else if (json['post'] != null) {
return PostModel.fromPiefed(
json['post']! as JsonMap,
langCodeIdPairs: await client.languageCodeIdPairs(),
);
} else if (json['community'] != null) {
return DetailedCommunityModel.fromPiefed(
json['community']! as JsonMap,
);
} else if (json['person'] != null) {
return DetailedUserModel.fromPiefed(json['person']! as JsonMap);
} else if (json['feed'] != null) {
return FeedModel.fromPiefed(json['feed']! as JsonMap);
}
return null;
}
} on ClientException catch (e) {
if (e.message.contains('No object found') ||
e.message.contains('couldnt_find_object')) {
return null;
}
rethrow;
}
}
}
47 changes: 46 additions & 1 deletion lib/src/app.dart
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
import 'package:dynamic_color/dynamic_color.dart';
import 'package:flex_color_scheme/flex_color_scheme.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_localized_locales/flutter_localized_locales.dart';
import 'package:interstellar/l10n/app_localizations.dart';
import 'package:interstellar/src/controller/controller.dart';
import 'package:interstellar/src/controller/router.dart';
import 'package:interstellar/src/models/comment.dart';
import 'package:interstellar/src/models/community.dart';
import 'package:interstellar/src/models/post.dart';
import 'package:interstellar/src/models/user.dart';
import 'package:interstellar/src/screens/account/notification/notification_count_controller.dart';
import 'package:interstellar/src/utils/globals.dart';
import 'package:interstellar/src/utils/instances.dart';
import 'package:interstellar/src/utils/utils.dart';
import 'package:intl/locale.dart' as intl_locale;
import 'package:provider/provider.dart';
Expand Down Expand Up @@ -45,7 +51,46 @@ class App extends StatelessWidget {
textScaler: TextScaler.linear(ac.profile.globalTextScale),
),
child: MaterialApp.router(
routerConfig: router.config(),
routerConfig: router.config(
deepLinkTransformer: (link) async {
final knownInstance = knownInstances[link.host];
if (knownInstance == null) return SynchronousFuture(link);

// Ensure item is federated to current instance.
final item = await ac.api.search.resolveObject(
link.toString(),
);

if (item == null) {
scaffoldMessengerKey.currentState?.showSnackBar(
const SnackBar(
content: Text('Failed to open link.'),
showCloseIcon: true,
),
);
}

// Construct interstellar compatible uri.
final newUri = switch (item) {
final PostModel p => Uri.parse(
'/${ac.instanceHost}/c/${p.community.name}/${p.type == PostType.thread ? 'thread' : 'microblog'}/${p.id}',
),
final CommentModel c => Uri.parse(
'/${ac.instanceHost}/comment/${c.id}',
),
final DetailedUserModel u => Uri.parse(
'/${ac.instanceHost}/u/${u.name}',
),
final DetailedCommunityModel c => Uri.parse(
'/${ac.instanceHost}/c/${c.name}',
),
Object() => link,
null => link,
};

return SynchronousFuture(newUri);
},
),
restorationScopeId: 'app',
localizationsDelegates: const [
...AppLocalizations.localizationsDelegates,
Expand Down
3 changes: 2 additions & 1 deletion lib/src/controller/controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,7 @@ class AppController with ChangeNotifier {
_defaultDownloadsDir = await getDefaultDownloadDir();

await _updateAPI();

logger.i('Finished init');
}

Expand Down Expand Up @@ -595,7 +596,7 @@ class AppController with ChangeNotifier {
await switchAccounts(key, force: forceSwitch);
} else {
// The following is already done when switchAccounts is run, so only needed without switchAccounts.
_updateAPI();
await _updateAPI();

notifyListeners();
}
Expand Down
2 changes: 1 addition & 1 deletion lib/src/models/post.dart
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ abstract class PostModel with _$PostModel {
JsonMap json, {
required List<(String, int)> langCodeIdPairs,
}) {
final postView = json['post_view']! as JsonMap;
final postView = json['post_view'] as JsonMap? ?? json;
final lemmyPost = postView['post']! as JsonMap;
final lemmyCounts = postView['counts'] as JsonMap?;

Expand Down
69 changes: 46 additions & 23 deletions lib/src/models/search.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,32 +18,55 @@ abstract class SearchListModel with _$SearchListModel {
factory SearchListModel.fromMbin(Map<String, dynamic> json) {
final items = <Object>[];

for (final actor in json['apActors']) {
final type = actor['type'];
if (type == 'user') {
items.add(DetailedUserModel.fromMbin(actor['object'] as JsonMap));
} else if (type == 'magazine') {
items.add(DetailedCommunityModel.fromMbin(actor['object'] as JsonMap));
final itemsJson = json['items']! as List<dynamic>;
final apResultsJson = json['apResults']! as List<dynamic>;

for (final itemJson in itemsJson) {
if (itemJson['entry'] != null) {
items.add(PostModel.fromMbinEntry(itemJson['entry'] as JsonMap));
}
}
for (final item in json['apObjects']) {
final itemType = item['itemType'];
if (itemType == 'entry') {
items.add(PostModel.fromMbinEntry(item as JsonMap));
} else if (itemType == 'post') {
items.add(PostModel.fromMbinPost(item as JsonMap));
} else if (itemType == 'entry_comment' || itemType == 'post_comment') {
items.add(CommentModel.fromMbin(item as JsonMap));
if (itemJson['entryComment'] != null) {
items.add(CommentModel.fromMbin(itemJson['entryComment'] as JsonMap));
}
if (itemJson['post'] != null) {
items.add(PostModel.fromMbinPost(itemJson['post'] as JsonMap));
}
if (itemJson['postComment'] != null) {
items.add(CommentModel.fromMbin(itemJson['postComment'] as JsonMap));
}
if (itemJson['magazine'] != null) {
items.add(
DetailedCommunityModel.fromMbin(itemJson['magazine'] as JsonMap),
);
}
if (itemJson['user'] != null) {
items.add(DetailedUserModel.fromMbin(itemJson['user'] as JsonMap));
}
}
for (final item in json['items']) {
final itemType = item['itemType'];
if (itemType == 'entry') {
items.add(PostModel.fromMbinEntry(item as JsonMap));
} else if (itemType == 'post') {
items.add(PostModel.fromMbinPost(item as JsonMap));
} else if (itemType == 'entry_comment' || itemType == 'post_comment') {
items.add(CommentModel.fromMbin(item as JsonMap));
for (final apResultJson in apResultsJson) {
if (apResultJson['entry'] != null) {
items.add(PostModel.fromMbinEntry(apResultJson['entry'] as JsonMap));
}
if (apResultJson['entryComment'] != null) {
items.add(
CommentModel.fromMbin(apResultJson['entryComment'] as JsonMap),
);
}
if (apResultJson['post'] != null) {
items.add(PostModel.fromMbinPost(apResultJson['post'] as JsonMap));
}
if (apResultJson['postComment'] != null) {
items.add(
CommentModel.fromMbin(apResultJson['postComment'] as JsonMap),
);
}
if (apResultJson['magazine'] != null) {
items.add(
DetailedCommunityModel.fromMbin(apResultJson['magazine'] as JsonMap),
);
}
if (apResultJson['user'] != null) {
items.add(DetailedUserModel.fromMbin(apResultJson['user'] as JsonMap));
}
}

Expand Down
32 changes: 32 additions & 0 deletions lib/src/utils/ap_urls.dart
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:interstellar/src/controller/controller.dart';
import 'package:interstellar/src/controller/router.gr.dart';
import 'package:interstellar/src/controller/server.dart';
import 'package:interstellar/src/models/comment.dart';
import 'package:interstellar/src/models/community.dart';
import 'package:interstellar/src/models/feed.dart';
import 'package:interstellar/src/models/post.dart';
import 'package:interstellar/src/models/user.dart';
import 'package:interstellar/src/screens/feed/post_page.dart';
import 'package:interstellar/src/utils/utils.dart';
import 'package:provider/provider.dart';

Expand Down Expand Up @@ -103,3 +106,32 @@ List<Uri> genFeedUrls(BuildContext context, FeedModel feed) {
?apUrl,
];
}

Future<bool> navigateApObject(BuildContext context, String apId) async {
final ac = context.read<AppController>();

final object = await ac.api.search.resolveObject(apId);
if (object == null || !context.mounted) return false;

switch (object) {
case final CommentModel i:
context.router.push(
PostCommentRoute(postType: i.postType, commentId: i.id),
);
case final PostModel i:
pushPostPage(
context,
communityName: i.community.name,
postId: i.id,
postType: i.type,
initData: i,
);
case final DetailedCommunityModel i:
context.router.push(CommunityRoute(communityName: i.name, initData: i));
case final DetailedUserModel i:
context.router.push(UserRoute(username: i.name, initData: i));
case final FeedModel i:
context.router.push(FeedRoute(feedName: i.name));
}
return true;
}
Loading
Loading