Add lemmy support for comment viewing and voting

This commit is contained in:
John Wesley 2024-02-22 10:29:48 -05:00
parent 847c84431c
commit 069eb3700b
6 changed files with 195 additions and 88 deletions

View File

@ -28,6 +28,6 @@ void main() async {
// Load user settings
final settingsController = SettingsController();
await settingsController.loadSettings();
print(settingsController.api);
runApp(MyApp(settingsController: settingsController));
}

View File

@ -18,7 +18,7 @@ class API {
final http.Client httpClient;
final String server;
final KbinAPIComments comments;
final APIComments comments;
final KbinAPIDomains domains;
final APIThreads entries;
final APIMagazines magazines;
@ -32,7 +32,7 @@ class API {
this.software,
this.httpClient,
this.server,
) : comments = KbinAPIComments(software, httpClient, server),
) : comments = APIComments(software, httpClient, server),
domains = KbinAPIDomains(software, httpClient, server),
entries = APIThreads(software, httpClient, server),
magazines = APIMagazines(software, httpClient, server),

View File

@ -10,6 +10,14 @@ import 'package:interstellar/src/widgets/selection_menu.dart';
enum CommentSort { newest, top, hot, active, oldest }
const Map<CommentSort, String> lemmyCommentSortMap = {
CommentSort.active: 'Controversial',
CommentSort.hot: 'Hot',
CommentSort.newest: 'New',
CommentSort.oldest: 'Old',
CommentSort.top: 'Top',
};
const SelectionMenu<CommentSort> commentSortSelect = SelectionMenu(
'Sort Comments',
[
@ -50,12 +58,12 @@ const _postTypeKbinComment = {
PostType.microblog: 'post-comments',
};
class KbinAPIComments {
class APIComments {
final ServerSoftware software;
final http.Client httpClient;
final String server;
KbinAPIComments(
APIComments(
this.software,
this.httpClient,
this.server,
@ -69,30 +77,50 @@ class KbinAPIComments {
List<String>? langs,
bool? usePreferredLangs,
}) async {
final path = '/api/${_postTypeKbin[postType]}/$postId/comments';
final query = queryParams({
'p': page?.toString(),
'sortBy': sort?.name,
'lang': langs?.join(','),
'usePreferredLangs': (usePreferredLangs ?? false).toString(),
});
switch (software) {
case ServerSoftware.kbin:
case ServerSoftware.mbin:
final path = '/api/${_postTypeKbin[postType]}/$postId/comments';
final query = queryParams({
'p': page?.toString(),
'sortBy': sort?.name,
'lang': langs?.join(','),
'usePreferredLangs': (usePreferredLangs ?? false).toString(),
});
final response = await httpClient.get(Uri.https(server, path, query));
final response = await httpClient.get(Uri.https(server, path, query));
httpErrorHandler(response, message: 'Failed to load comments');
httpErrorHandler(response, message: 'Failed to load comments');
return CommentListModel.fromKbin(
jsonDecode(response.body) as Map<String, Object?>);
return CommentListModel.fromKbin(
jsonDecode(response.body) as Map<String, Object?>);
case ServerSoftware.lemmy:
const path = '/api/v3/comment/list';
final query = queryParams({
'post_id': postId.toString(),
'page': page?.toString(),
'sort': lemmyCommentSortMap[sort],
'max_depth': '8',
});
final response = await httpClient.get(Uri.https(server, path, query));
httpErrorHandler(response, message: 'Failed to load comments');
return CommentListModel.fromLemmy(
jsonDecode(response.body) as Map<String, Object?>);
}
}
Future<CommentListModel> listFromUser(
PostType postType,
int userId, {
String? page,
CommentSort? sort,
List<String>? langs,
bool? usePreferredLangs,
}) async {
PostType postType,
int userId, {
String? page,
CommentSort? sort,
List<String>? langs,
bool? usePreferredLangs,
}) async {
final path = '/api/users/$userId/${_postTypeKbinComment[postType]}';
final query = queryParams({
'p': page,
@ -110,41 +138,90 @@ class KbinAPIComments {
}
Future<CommentModel> get(PostType postType, int commentId) async {
final path = '/api/${_postTypeKbinComment[postType]}/$commentId';
switch (software) {
case ServerSoftware.kbin:
case ServerSoftware.mbin:
final path = '/api/${_postTypeKbinComment[postType]}/$commentId';
final response = await httpClient.get(Uri.https(server, path));
final response = await httpClient.get(Uri.https(server, path));
httpErrorHandler(response, message: 'Failed to load comment');
httpErrorHandler(response, message: 'Failed to load comment');
return CommentModel.fromKbin(
jsonDecode(response.body) as Map<String, Object?>);
return CommentModel.fromKbin(
jsonDecode(response.body) as Map<String, Object?>);
case ServerSoftware.lemmy:
const path = '/api/v3/comment/list';
final query = queryParams({
'parent_id': commentId.toString(),
});
final response = await httpClient.get(Uri.https(server, path, query));
httpErrorHandler(response, message: 'Failed to load comment');
return CommentModel.fromLemmy(
(jsonDecode(response.body)['comments'] as List<dynamic>).first,
possibleChildren:
jsonDecode(response.body)['comments'] as List<dynamic>,
);
}
}
Future<CommentModel> putVote(
Future<CommentModel> vote(
PostType postType,
int commentId,
int choice,
int newScore,
) async {
final path =
'/api/${_postTypeKbinComment[postType]}/$commentId/vote/$choice';
switch (software) {
case ServerSoftware.kbin:
case ServerSoftware.mbin:
final path = choice == 1
? '/api/${_postTypeKbinComment[postType]}/$commentId/favourite'
: '/api/${_postTypeKbinComment[postType]}/$commentId/vote/$choice';
final response = await httpClient.put(Uri.https(server, path));
final response = await httpClient.put(Uri.https(server, path));
httpErrorHandler(response, message: 'Failed to send vote');
httpErrorHandler(response, message: 'Failed to send vote');
return CommentModel.fromKbin(
jsonDecode(response.body) as Map<String, Object?>);
return CommentModel.fromKbin(
jsonDecode(response.body) as Map<String, Object?>);
case ServerSoftware.lemmy:
const path = '/api/v3/comment/like';
final response = await httpClient.post(
Uri.https(server, path),
headers: {'Content-Type': 'application/json'},
body: jsonEncode({
'comment_id': commentId,
'score': newScore,
}),
);
httpErrorHandler(response, message: 'Failed to send vote');
return CommentModel.fromLemmy(
jsonDecode(response.body)['comment_view'] as Map<String, Object?>);
}
}
Future<CommentModel> putFavorite(PostType postType, int commentId) async {
final path = '/api/${_postTypeKbinComment[postType]}/$commentId/favourite';
Future<CommentModel> boost(PostType postType, int commentId) async {
switch (software) {
case ServerSoftware.kbin:
case ServerSoftware.mbin:
final path = '/api/${_postTypeKbinComment[postType]}/$commentId/vote/1';
final response = await httpClient.put(Uri.https(server, path));
final response = await httpClient.put(Uri.https(server, path));
httpErrorHandler(response, message: 'Failed to send vote');
httpErrorHandler(response, message: 'Failed to send boost');
return CommentModel.fromKbin(
jsonDecode(response.body) as Map<String, Object?>);
return CommentModel.fromKbin(
jsonDecode(response.body) as Map<String, Object?>);
case ServerSoftware.lemmy:
throw Exception('Tried to boost on lemmy');
}
}
Future<CommentModel> create(

View File

@ -21,6 +21,19 @@ class CommentListModel with _$CommentListModel {
nextPage: kbinCalcNextPaginationPage(
json['pagination'] as Map<String, Object?>),
);
factory CommentListModel.fromLemmy(Map<String, Object?> json) =>
CommentListModel(
items: (json['comments'] as List<dynamic>)
.where(
(c) => (c['comment']['path'] as String).split('.').length == 2)
.map((c) => CommentModel.fromLemmy(
c as Map<String, Object?>,
possibleChildren: json['comments'] as List<dynamic>,
))
.toList(),
nextPage: null,
);
}
@freezed
@ -44,7 +57,6 @@ class CommentModel with _$CommentModel {
required bool? isAdult,
required DateTime createdAt,
required DateTime? editedAt,
required DateTime lastActive,
required List<CommentModel>? children,
required int childCount,
required String visibility,
@ -73,7 +85,6 @@ class CommentModel with _$CommentModel {
isAdult: json['isAdult'] as bool,
createdAt: DateTime.parse(json['createdAt'] as String),
editedAt: optionalDateTime(json['editedAt'] as String?),
lastActive: DateTime.parse(json['lastActive'] as String),
children: (json['children'] as List<dynamic>)
.map((c) => CommentModel.fromKbin(c as Map<String, Object?>))
.toList(),
@ -81,43 +92,53 @@ class CommentModel with _$CommentModel {
visibility: json['visibility'] as String,
);
// factory CommentModel.fromLemmy(
// Map<String, Object?> json, List<dynamic> allCommentsJson) {
// final lemmyComment = json['comment'] as Map<String, Object?>;
// final lemmyCounts = json['counts'] as Map<String, Object?>;
factory CommentModel.fromLemmy(
Map<String, Object?> json, {
List<dynamic> possibleChildren = const [],
}) {
final lemmyComment = json['comment'] as Map<String, Object?>;
final lemmyCounts = json['counts'] as Map<String, Object?>;
// final lemmyPath = lemmyComment['path'] as String;
// final lemmyPathSegments =
// lemmyPath.split('.').map((e) => int.parse(e)).toList();
final lemmyPath = lemmyComment['path'] as String;
final lemmyPathSegments =
lemmyPath.split('.').map((e) => int.parse(e)).toList();
// return CommentModel(
// id: json['commentId'] as int,
// user: UserModel.fromLemmy(json['creator'] as Map<String, Object?>),
// magazine:
// MagazineModel.fromLemmy(json['community'] as Map<String, Object?>),
// postType: PostType.thread,
// postId: (json['post'] as Map<String, Object?>)['id'] as int,
// rootId: lemmyPathSegments.length > 2 ? lemmyPathSegments[1] : null,
// parentId: lemmyPathSegments.length > 2
// ? lemmyPathSegments[lemmyPathSegments.length - 2]
// : null,
// image: null,
// body: json['content'] as String,
// lang: null,
// upvotes: lemmyCounts['upvotes'] as int,
// downvotes: lemmyCounts['downvotes'] as int,
// boosts: null,
// myVote: json['my_vote'] as int?,
// myBoost: null,
// isAdult: json['isAdult'] as bool,
// createdAt: DateTime.parse(json['createdAt'] as String),
// editedAt: optionalDateTime(json['editedAt'] as String?),
// lastActive: DateTime.parse(json['lastActive'] as String),
// children: (json['children'] as List<dynamic>)
// .map((c) => CommentModel.fromKbin(c as Map<String, Object?>))
// .toList(),
// childCount: json['childCount'] as int,
// visibility: json['visibility'] as String,
// );
// }
final children = possibleChildren
.where((c) {
String childPath = c['comment']['path'];
return childPath.startsWith('$lemmyPath.') &&
(childPath.split('.').length == lemmyPathSegments.length + 1);
})
.map((c) =>
CommentModel.fromLemmy(c, possibleChildren: possibleChildren))
.toList();
return CommentModel(
id: lemmyComment['id'] as int,
user: UserModel.fromLemmy(json['creator'] as Map<String, Object?>),
magazine:
MagazineModel.fromLemmy(json['community'] as Map<String, Object?>),
postType: PostType.thread,
postId: (json['post'] as Map<String, Object?>)['id'] as int,
rootId: lemmyPathSegments.length > 2 ? lemmyPathSegments[1] : null,
parentId: lemmyPathSegments.length > 2
? lemmyPathSegments[lemmyPathSegments.length - 2]
: null,
image: null,
body: lemmyComment['content'] as String,
lang: null,
upvotes: lemmyCounts['upvotes'] as int,
downvotes: lemmyCounts['downvotes'] as int,
boosts: null,
myVote: json['my_vote'] as int?,
myBoost: null,
isAdult: null,
createdAt: DateTime.parse(lemmyComment['published'] as String),
editedAt: optionalDateTime(json['updated'] as String?),
children: children,
childCount: lemmyCounts['child_count'] as int,
visibility: 'visible',
);
}
}

View File

@ -54,7 +54,7 @@ class _EntryCommentState extends State<PostComment> {
.read<SettingsController>()
.api
.comments
.putVote(widget.comment.postType, widget.comment.id, 1);
.boost(widget.comment.postType, widget.comment.id);
widget.onUpdate(newValue.copyWith(
childCount: widget.comment.childCount,
children: widget.comment.children,
@ -66,7 +66,8 @@ class _EntryCommentState extends State<PostComment> {
.read<SettingsController>()
.api
.comments
.putFavorite(widget.comment.postType, widget.comment.id);
.vote(widget.comment.postType, widget.comment.id, 1,
widget.comment.myVote == 1 ? 0 : 1);
widget.onUpdate(newValue.copyWith(
childCount: widget.comment.childCount,
children: widget.comment.children,
@ -80,7 +81,8 @@ class _EntryCommentState extends State<PostComment> {
.read<SettingsController>()
.api
.comments
.putVote(widget.comment.postType, widget.comment.id, -1);
.vote(widget.comment.postType, widget.comment.id, -1,
widget.comment.myVote == -1 ? 0 : -1);
widget.onUpdate(newValue.copyWith(
childCount: widget.comment.childCount,
children: widget.comment.children,

View File

@ -28,8 +28,8 @@ class _PostPageState extends State<PostPage> {
CommentSort commentSort = CommentSort.hot;
final PagingController<String, CommentModel> _pagingController =
PagingController(firstPageKey: '1');
final PagingController<int, CommentModel> _pagingController =
PagingController(firstPageKey: 1);
@override
void initState() {
@ -48,13 +48,13 @@ class _PostPageState extends State<PostPage> {
widget.onUpdate(newValue);
}
Future<void> _fetchPage(String pageKey) async {
Future<void> _fetchPage(int pageKey) async {
try {
final newPage =
await context.read<SettingsController>().api.comments.list(
_data.type,
_data.id,
page: int.parse(pageKey),
page: pageKey,
sort: commentSort,
usePreferredLangs: whenLoggedIn(context,
context.read<SettingsController>().useAccountLangFilter),
@ -69,7 +69,14 @@ class _PostPageState extends State<PostPage> {
final newItems =
newPage.items.where((e) => !currentItemIds.contains(e.id)).toList();
_pagingController.appendPage(newItems, newPage.nextPage);
_pagingController.appendPage(
newItems,
context.read<SettingsController>().serverSoftware ==
ServerSoftware.lemmy
? (newPage.items.isEmpty ? null : pageKey + 1)
: (newPage.nextPage == null
? null
: int.parse(newPage.nextPage!)));
} catch (error) {
_pagingController.error = error;
}