Repository: LemmyNet/lemmy (language: rust)

Issue:
Return saved date when fetching saved content
### Requirements

- [X] Is this a feature request? For questions or discussions use https://lemmy.ml/c/lemmy_support
- [X] Did you check to see if this issue already exists?
- [X] Is this only a feature request? Do not put multiple feature requests in one issue.
- [X] Is this a backend issue? Use the [lemmy-ui](https://github.com/LemmyNet/lemmy-ui) repo for UI / frontend issues.
- [X] Do you agree to follow the rules in our [Code of Conduct](https://join-lemmy.org/docs/code_of_conduct.html)?

### Is your proposal related to a problem?

Saved items are currently returned in the order in which they were saved, which is the intuitive behavior; however, the actual save date does not appear to be included in the response. This, combined with the fact that posts and comments are returned in two separate, sorted arrays, makes it impossible to generate a correctly sorted feed of posts and comments sorted by saved date.

### Describe the solution you'd like.

Add a field to the response object containing the date the post/comment was saved.

### Describe alternatives you've considered.

1. Display saved posts and comments in separate feeds. This is bad UX, as it is both inconsistent with the web UI and awkward to navigate
2. Sort using the post publication date as a rough proxy of save date. This only works as long as the user is saving posts from hot/new; posts saved from, e.g., sorting a community by top all time will be buried in the saved feed
3. Combine each page of posts and comments (as lemmy-ui currently does). This produces similar sorting issues to the above point, where if a user saves posts more frequently than comments (or vice versa) then very old saved comments will be displayed before more recently saved posts.

### Additional context

_No response_


Discussion / hints:
#2444 Will be about multiple types for all the responses, and should also handle sort orders on multiple types.

The saved date might be tricky, because that currently isn't a part of `PostView`, so we might need to add it as an optional field there.

Fix patch:
diff --git a/crates/db_views/src/combined/person_content_combined_view.rs b/crates/db_views/src/combined/person_content_combined_view.rs
index 898ece8b7b..414f847542 100644
--- a/crates/db_views/src/combined/person_content_combined_view.rs
+++ b/crates/db_views/src/combined/person_content_combined_view.rs
@@ -312,7 +312,7 @@ impl PersonContentCombinedQuery {
           post_aggregates::comments.nullable() - post_actions::read_comments_amount.nullable(),
           post_aggregates::comments,
         ),
-        post_actions::saved.nullable().is_not_null(),
+        post_actions::saved.nullable(),
         post_actions::read.nullable().is_not_null(),
         post_actions::hidden.nullable().is_not_null(),
         post_actions::like_score.nullable(),
@@ -321,7 +321,7 @@ impl PersonContentCombinedQuery {
         // Comment-specific
         comment::all_columns.nullable(),
         comment_aggregates::all_columns.nullable(),
-        comment_actions::saved.nullable().is_not_null(),
+        comment_actions::saved.nullable(),
         comment_actions::like_score.nullable(),
         // Shared
         post::all_columns,
diff --git a/crates/db_views/src/combined/person_saved_combined_view.rs b/crates/db_views/src/combined/person_saved_combined_view.rs
index 326c5de72f..11356f802a 100644
--- a/crates/db_views/src/combined/person_saved_combined_view.rs
+++ b/crates/db_views/src/combined/person_saved_combined_view.rs
@@ -109,7 +109,7 @@ impl PersonSavedCombinedQuery {
           post_aggregates::comments.nullable() - post_actions::read_comments_amount.nullable(),
           post_aggregates::comments,
         ),
-        post_actions::saved.nullable().is_not_null(),
+        post_actions::saved.nullable(),
         post_actions::read.nullable().is_not_null(),
         post_actions::hidden.nullable().is_not_null(),
         post_actions::like_score.nullable(),
@@ -118,7 +118,7 @@ impl PersonSavedCombinedQuery {
         // Comment-specific
         comment::all_columns.nullable(),
         comment_aggregates::all_columns.nullable(),
-        comment_actions::saved.nullable().is_not_null(),
+        comment_actions::saved.nullable(),
         comment_actions::like_score.nullable(),
         // Shared
         post::all_columns,
diff --git a/crates/db_views/src/combined/search_combined_view.rs b/crates/db_views/src/combined/search_combined_view.rs
index 05c999ef18..adeea855b4 100644
--- a/crates/db_views/src/combined/search_combined_view.rs
+++ b/crates/db_views/src/combined/search_combined_view.rs
@@ -255,7 +255,7 @@ impl SearchCombinedQuery {
           post_aggregates::comments,
         )
         .nullable(),
-        post_actions::saved.nullable().is_not_null(),
+        post_actions::saved.nullable(),
         post_actions::read.nullable().is_not_null(),
         post_actions::hidden.nullable().is_not_null(),
         post_actions::like_score.nullable(),
@@ -264,7 +264,7 @@ impl SearchCombinedQuery {
         // Comment-specific
         comment::all_columns.nullable(),
         comment_aggregates::all_columns.nullable(),
-        comment_actions::saved.nullable().is_not_null(),
+        comment_actions::saved.nullable(),
         comment_actions::like_score.nullable(),
         // Community-specific
         community::all_columns.nullable(),
diff --git a/crates/db_views/src/comment/comment_view.rs b/crates/db_views/src/comment/comment_view.rs
index 9297eec603..2fac1d4201 100644
--- a/crates/db_views/src/comment/comment_view.rs
+++ b/crates/db_views/src/comment/comment_view.rs
@@ -135,7 +135,7 @@ impl CommentView {
       creator_is_moderator: self.creator_is_moderator,
       creator_is_admin: self.creator_is_admin,
       subscribed: self.subscribed,
-      saved: self.saved,
+      saved: self.saved.is_some(),
       creator_blocked: self.creator_blocked,
       my_vote: self.my_vote,
     }
@@ -894,7 +894,7 @@ mod tests {
       creator_is_admin: true,
       my_vote: None,
       subscribed: SubscribedType::NotSubscribed,
-      saved: false,
+      saved: None,
       creator_blocked: false,
       comment: Comment {
         id: data.inserted_comment_0.id,
diff --git a/crates/db_views/src/post/post_view.rs b/crates/db_views/src/post/post_view.rs
index 0500733c9b..eeadc8f627 100644
--- a/crates/db_views/src/post/post_view.rs
+++ b/crates/db_views/src/post/post_view.rs
@@ -164,7 +164,7 @@ impl PostView {
         Self::creator_is_admin(),
         post_aggregates::all_columns,
         CommunityFollower::select_subscribed_type(),
-        post_actions::saved.nullable().is_not_null(),
+        post_actions::saved.nullable(),
         post_actions::read.nullable().is_not_null(),
         post_actions::hidden.nullable().is_not_null(),
         person_actions::blocked.nullable().is_not_null(),
@@ -411,7 +411,7 @@ impl<'a> PostQuery<'a> {
         PostView::creator_is_admin(),
         post_aggregates::all_columns,
         CommunityFollower::select_subscribed_type(),
-        post_actions::saved.nullable().is_not_null(),
+        post_actions::saved.nullable(),
         post_actions::read.nullable().is_not_null(),
         post_actions::hidden.nullable().is_not_null(),
         person_actions::blocked.nullable().is_not_null(),
@@ -1917,7 +1917,7 @@ mod tests {
       subscribed: SubscribedType::NotSubscribed,
       read: false,
       hidden: false,
-      saved: false,
+      saved: None,
       creator_blocked: false,
       tags: PostTags::default(),
     })
diff --git a/crates/db_views/src/structs.rs b/crates/db_views/src/structs.rs
index c224162c72..fe420a6451 100644
--- a/crates/db_views/src/structs.rs
+++ b/crates/db_views/src/structs.rs
@@ -1,3 +1,4 @@
+use chrono::{DateTime, Utc};
 #[cfg(feature = "full")]
 use diesel::{
   deserialize::FromSqlRow,
@@ -174,13 +175,14 @@ pub struct CommentView {
     )
   )]
   pub subscribed: SubscribedType,
+  #[cfg_attr(feature = "full", ts(optional))]
   #[cfg_attr(feature = "full",
     diesel(
       select_expression =
-        comment_actions::saved.nullable().is_not_null()
+        comment_actions::saved.nullable()
     )
   )]
-  pub saved: bool,
+  pub saved: Option<DateTime<Utc>>,
   #[cfg_attr(feature = "full",
     diesel(
       select_expression =
@@ -324,7 +326,8 @@ pub struct PostView {
   pub creator_is_admin: bool,
   pub counts: PostAggregates,
   pub subscribed: SubscribedType,
-  pub saved: bool,
+  #[cfg_attr(feature = "full", ts(optional))]
+  pub saved: Option<DateTime<Utc>>,
   pub read: bool,
   pub hidden: bool,
   pub creator_blocked: bool,
@@ -477,11 +480,11 @@ pub enum ReportCombinedView {
 #[cfg_attr(feature = "full", derive(Queryable))]
 #[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))]
 /// A combined person_content view
-pub struct PersonContentCombinedViewInternal {
+pub(crate) struct PersonContentCombinedViewInternal {
   // Post-specific
   pub post_counts: PostAggregates,
   pub post_unread_comments: i64,
-  pub post_saved: bool,
+  pub post_saved: Option<DateTime<Utc>>,
   pub post_read: bool,
   pub post_hidden: bool,
   pub my_post_vote: Option<i16>,
@@ -490,7 +493,7 @@ pub struct PersonContentCombinedViewInternal {
   // Comment-specific
   pub comment: Option<Comment>,
   pub comment_counts: Option<CommentAggregates>,
-  pub comment_saved: bool,
+  pub comment_saved: Option<DateTime<Utc>>,
   pub my_comment_vote: Option<i16>,
   // Shared
   pub post: Post,
@@ -1026,7 +1029,7 @@ pub struct ModlogCombinedPaginationCursor(pub String);
 #[cfg_attr(feature = "full", derive(Queryable, Selectable))]
 #[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))]
 /// A combined modlog view
-pub struct ModlogCombinedViewInternal {
+pub(crate) struct ModlogCombinedViewInternal {
   // Specific
   #[cfg_attr(feature = "full", diesel(embed))]
   pub admin_allow_instance: Option<AdminAllowInstance>,
@@ -1120,12 +1123,12 @@ pub struct SearchCombinedPaginationCursor(pub String);
 #[cfg_attr(feature = "full", derive(Queryable))]
 #[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))]
 /// A combined search view
-pub struct SearchCombinedViewInternal {
+pub(crate) struct SearchCombinedViewInternal {
   // Post-specific
   pub post: Option<Post>,
   pub post_counts: Option<PostAggregates>,
   pub post_unread_comments: Option<i64>,
-  pub post_saved: bool,
+  pub post_saved: Option<DateTime<Utc>>,
   pub post_read: bool,
   pub post_hidden: bool,
   pub my_post_vote: Option<i16>,
@@ -1134,7 +1137,7 @@ pub struct SearchCombinedViewInternal {
   // // Comment-specific
   pub comment: Option<Comment>,
   pub comment_counts: Option<CommentAggregates>,
-  pub comment_saved: bool,
+  pub comment_saved: Option<DateTime<Utc>>,
   pub my_comment_vote: Option<i16>,
   // // Community-specific
   pub community: Option<Community>,


Test patch: