From 069eb3700b93a9314d8976763b1c6b669b163d5a Mon Sep 17 00:00:00 2001 From: John Wesley Date: Thu, 22 Feb 2024 10:29:48 -0500 Subject: [PATCH] Add lemmy support for comment viewing and voting --- lib/main.dart | 2 +- lib/src/api/api.dart | 4 +- lib/src/api/comments.dart | 153 +++++++++++++++++++------ lib/src/models/comment.dart | 99 +++++++++------- lib/src/screens/feed/post_comment.dart | 8 +- lib/src/screens/feed/post_page.dart | 17 ++- 6 files changed, 195 insertions(+), 88 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 93282fd..02094d5 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -28,6 +28,6 @@ void main() async { // Load user settings final settingsController = SettingsController(); await settingsController.loadSettings(); - print(settingsController.api); + runApp(MyApp(settingsController: settingsController)); } diff --git a/lib/src/api/api.dart b/lib/src/api/api.dart index df17887..6bf3e17 100644 --- a/lib/src/api/api.dart +++ b/lib/src/api/api.dart @@ -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), diff --git a/lib/src/api/comments.dart b/lib/src/api/comments.dart index dcfc3fe..90d6fe4 100644 --- a/lib/src/api/comments.dart +++ b/lib/src/api/comments.dart @@ -10,6 +10,14 @@ import 'package:interstellar/src/widgets/selection_menu.dart'; enum CommentSort { newest, top, hot, active, oldest } +const Map lemmyCommentSortMap = { + CommentSort.active: 'Controversial', + CommentSort.hot: 'Hot', + CommentSort.newest: 'New', + CommentSort.oldest: 'Old', + CommentSort.top: 'Top', +}; + const SelectionMenu 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? 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); + return CommentListModel.fromKbin( + jsonDecode(response.body) as Map); + + 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); + } } Future listFromUser( - PostType postType, - int userId, { - String? page, - CommentSort? sort, - List? langs, - bool? usePreferredLangs, - }) async { + PostType postType, + int userId, { + String? page, + CommentSort? sort, + List? langs, + bool? usePreferredLangs, + }) async { final path = '/api/users/$userId/${_postTypeKbinComment[postType]}'; final query = queryParams({ 'p': page, @@ -110,41 +138,90 @@ class KbinAPIComments { } Future 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); + return CommentModel.fromKbin( + jsonDecode(response.body) as Map); + + 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).first, + possibleChildren: + jsonDecode(response.body)['comments'] as List, + ); + } } - Future putVote( + Future 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); + return CommentModel.fromKbin( + jsonDecode(response.body) as Map); + 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); + } } - Future putFavorite(PostType postType, int commentId) async { - final path = '/api/${_postTypeKbinComment[postType]}/$commentId/favourite'; + Future 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); + return CommentModel.fromKbin( + jsonDecode(response.body) as Map); + + case ServerSoftware.lemmy: + throw Exception('Tried to boost on lemmy'); + } } Future create( diff --git a/lib/src/models/comment.dart b/lib/src/models/comment.dart index 4ef1155..5a3382a 100644 --- a/lib/src/models/comment.dart +++ b/lib/src/models/comment.dart @@ -21,6 +21,19 @@ class CommentListModel with _$CommentListModel { nextPage: kbinCalcNextPaginationPage( json['pagination'] as Map), ); + + factory CommentListModel.fromLemmy(Map json) => + CommentListModel( + items: (json['comments'] as List) + .where( + (c) => (c['comment']['path'] as String).split('.').length == 2) + .map((c) => CommentModel.fromLemmy( + c as Map, + possibleChildren: json['comments'] as List, + )) + .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? 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) .map((c) => CommentModel.fromKbin(c as Map)) .toList(), @@ -81,43 +92,53 @@ class CommentModel with _$CommentModel { visibility: json['visibility'] as String, ); - // factory CommentModel.fromLemmy( - // Map json, List allCommentsJson) { - // final lemmyComment = json['comment'] as Map; - // final lemmyCounts = json['counts'] as Map; + factory CommentModel.fromLemmy( + Map json, { + List possibleChildren = const [], + }) { + final lemmyComment = json['comment'] as Map; + final lemmyCounts = json['counts'] as Map; - // 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), - // magazine: - // MagazineModel.fromLemmy(json['community'] as Map), - // postType: PostType.thread, - // postId: (json['post'] as Map)['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) - // .map((c) => CommentModel.fromKbin(c as Map)) - // .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), + magazine: + MagazineModel.fromLemmy(json['community'] as Map), + postType: PostType.thread, + postId: (json['post'] as Map)['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', + ); + } } diff --git a/lib/src/screens/feed/post_comment.dart b/lib/src/screens/feed/post_comment.dart index ff9fc07..0531f50 100644 --- a/lib/src/screens/feed/post_comment.dart +++ b/lib/src/screens/feed/post_comment.dart @@ -54,7 +54,7 @@ class _EntryCommentState extends State { .read() .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 { .read() .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 { .read() .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, diff --git a/lib/src/screens/feed/post_page.dart b/lib/src/screens/feed/post_page.dart index f21a924..29bed2c 100644 --- a/lib/src/screens/feed/post_page.dart +++ b/lib/src/screens/feed/post_page.dart @@ -28,8 +28,8 @@ class _PostPageState extends State { CommentSort commentSort = CommentSort.hot; - final PagingController _pagingController = - PagingController(firstPageKey: '1'); + final PagingController _pagingController = + PagingController(firstPageKey: 1); @override void initState() { @@ -48,13 +48,13 @@ class _PostPageState extends State { widget.onUpdate(newValue); } - Future _fetchPage(String pageKey) async { + Future _fetchPage(int pageKey) async { try { final newPage = await context.read().api.comments.list( _data.type, _data.id, - page: int.parse(pageKey), + page: pageKey, sort: commentSort, usePreferredLangs: whenLoggedIn(context, context.read().useAccountLangFilter), @@ -69,7 +69,14 @@ class _PostPageState extends State { final newItems = newPage.items.where((e) => !currentItemIds.contains(e.id)).toList(); - _pagingController.appendPage(newItems, newPage.nextPage); + _pagingController.appendPage( + newItems, + context.read().serverSoftware == + ServerSoftware.lemmy + ? (newPage.items.isEmpty ? null : pageKey + 1) + : (newPage.nextPage == null + ? null + : int.parse(newPage.nextPage!))); } catch (error) { _pagingController.error = error; }