Link user profile from article history and diff view

This commit is contained in:
Felix Ableitner 2024-02-13 16:49:26 +01:00
parent 47362b52da
commit 911fadb94b
13 changed files with 97 additions and 49 deletions

View File

@ -21,4 +21,8 @@ main {
background-color: #ffffff;
flex-grow: 1;
padding: 20px;
}
pre {
white-space: pre-wrap;
}

View File

@ -191,7 +191,10 @@ pub(in crate::backend::api) async fn fork_article(
// copy edits to new article
// this could also be done in sql
let edits = DbEdit::read_for_article(&original_article, &data.db_connection)?;
let edits = DbEdit::read_for_article(&original_article, &data.db_connection)?
.into_iter()
.map(|e| e.edit)
.collect::<Vec<_>>();
for e in edits {
let ap_id = DbEditForm::generate_ap_id(&article, &e.hash)?;
let form = DbEditForm {
@ -221,7 +224,7 @@ pub(super) async fn resolve_article(
) -> MyResult<Json<ArticleView>> {
let article: DbArticle = ObjectId::from(query.id).dereference(&data).await?;
let edits = DbEdit::read_for_article(&article, &data.db_connection)?;
let latest_version = edits.last().unwrap().hash.clone();
let latest_version = edits.last().unwrap().edit.hash.clone();
Ok(Json(ArticleView {
article,
edits,

View File

@ -1,7 +1,7 @@
use crate::backend::database::schema::edit;
use crate::backend::database::schema::{edit, person};
use crate::backend::error::MyResult;
use crate::common::EditVersion;
use crate::common::{DbArticle, DbEdit};
use crate::common::{EditVersion, EditView};
use activitypub_federation::fetch::object_id::ObjectId;
use chrono::{DateTime, Utc};
use diesel::ExpressionMethods;
@ -83,13 +83,15 @@ impl DbEdit {
.get_result(conn.deref_mut())?)
}
// TODO: create internal variant which doesnt return person?
pub fn read_for_article(
article: &DbArticle,
conn: &Mutex<PgConnection>,
) -> MyResult<Vec<Self>> {
) -> MyResult<Vec<EditView>> {
let mut conn = conn.lock().unwrap();
Ok(edit::table
.filter(edit::dsl::article_id.eq(article.id))
.inner_join(person::table)
.filter(edit::article_id.eq(article.id))
.get_results(conn.deref_mut())?)
}
}

View File

@ -44,7 +44,7 @@ impl Collection for DbEditCollection {
article
.edits
.into_iter()
.map(|a| a.into_json(data))
.map(|a| a.edit.into_json(data))
.collect::<Vec<_>>(),
)
.await?;

View File

@ -1,6 +1,6 @@
use crate::backend::error::MyResult;
use crate::common::DbEdit;
use crate::common::EditVersion;
use crate::common::EditView;
use activitypub_federation::fetch::object_id::ObjectId;
use activitypub_federation::traits::Object;
use anyhow::anyhow;
@ -30,15 +30,15 @@ where
///
/// TODO: testing
/// TODO: should cache all these generated versions
pub fn generate_article_version(edits: &Vec<DbEdit>, version: &EditVersion) -> MyResult<String> {
pub fn generate_article_version(edits: &Vec<EditView>, version: &EditVersion) -> MyResult<String> {
let mut generated = String::new();
if version == &EditVersion::default() {
return Ok(generated);
}
for e in edits {
let patch = Patch::from_str(&e.diff)?;
let patch = Patch::from_str(&e.edit.diff)?;
generated = apply(&generated, &patch)?;
if &e.hash == version {
if &e.edit.hash == version {
return Ok(generated);
}
}
@ -48,23 +48,36 @@ pub fn generate_article_version(edits: &Vec<DbEdit>, version: &EditVersion) -> M
#[cfg(test)]
mod test {
use super::*;
use crate::common::{DbEdit, DbPerson};
use activitypub_federation::fetch::object_id::ObjectId;
use chrono::Utc;
use diffy::create_patch;
fn create_edits() -> MyResult<Vec<DbEdit>> {
let generate_edit = |a, b| -> MyResult<DbEdit> {
fn create_edits() -> MyResult<Vec<EditView>> {
let generate_edit = |a, b| -> MyResult<EditView> {
let diff = create_patch(a, b).to_string();
Ok(DbEdit {
id: 0,
creator_id: 0,
hash: EditVersion::new(&diff),
ap_id: ObjectId::parse("http://example.com")?,
diff,
summary: String::new(),
article_id: 0,
previous_version_id: Default::default(),
created: Utc::now(),
Ok(EditView {
edit: DbEdit {
id: 0,
creator_id: 0,
hash: EditVersion::new(&diff),
ap_id: ObjectId::parse("http://example.com")?,
diff,
summary: String::new(),
article_id: 0,
previous_version_id: Default::default(),
created: Utc::now(),
},
creator: DbPerson {
id: 0,
username: "".to_string(),
ap_id: ObjectId::parse("http://example.com").unwrap(),
inbox_url: "".to_string(),
public_key: "".to_string(),
private_key: None,
last_refreshed_at: Default::default(),
local: false,
},
})
};
Ok([
@ -78,7 +91,7 @@ mod test {
#[test]
fn test_generate_article_version() -> MyResult<()> {
let edits = create_edits()?;
let generated = generate_article_version(&edits, &edits[1].hash)?;
let generated = generate_article_version(&edits, &edits[1].edit.hash)?;
assert_eq!("sda\n", generated);
Ok(())
}

View File

@ -37,7 +37,7 @@ pub struct ListArticlesData {
pub struct ArticleView {
pub article: DbArticle,
pub latest_version: EditVersion,
pub edits: Vec<DbEdit>,
pub edits: Vec<EditView>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
@ -63,6 +63,7 @@ pub struct DbEdit {
// TODO: we could use hash as primary key, but that gives errors on forking because
// the same edit is used for multiple articles
pub id: i32,
#[serde(skip)]
pub creator_id: i32,
/// UUID built from sha224 hash of diff
pub hash: EditVersion,
@ -78,6 +79,14 @@ pub struct DbEdit {
pub created: DateTime<Utc>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "ssr", derive(Queryable))]
#[cfg_attr(feature = "ssr", diesel(check_for_backend(diesel::pg::Pg)))]
pub struct EditView {
pub edit: DbEdit,
pub creator: DbPerson,
}
/// The version hash of a specific edit. Generated by taking an SHA256 hash of the diff
/// and using the first 16 bytes so that it fits into UUID.
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]

View File

@ -23,7 +23,7 @@ pub fn Nav() -> impl IntoView {
let (search_query, set_search_query) = create_signal(String::new());
view! {
<nav class="inner">
<nav class="inner" style="min-width: 250px;">
<li>
<A href="/">"Main Page"</A>
</li>

View File

@ -1,5 +1,7 @@
use crate::common::utils::extract_domain;
use crate::common::DbArticle;
use crate::common::{DbArticle, DbPerson};
use leptos::IntoAttribute;
use leptos::{view, IntoView};
pub mod api;
pub mod app;
@ -31,3 +33,18 @@ fn article_title(article: &DbArticle) -> String {
format!("{}@{}", title, extract_domain(&article.ap_id))
}
}
fn user_title(person: &DbPerson) -> String {
if person.local {
person.username.clone()
} else {
format!("{}@{}", person.username, extract_domain(&person.ap_id))
}
}
fn user_link(person: &DbPerson) -> impl IntoView {
let creator_path = format!("/user/{}", person.username);
view! {
<a href={creator_path}>{user_title(person)}</a>
}
}

View File

@ -1,6 +1,6 @@
use crate::frontend::article_title;
use crate::frontend::components::article_nav::ArticleNav;
use crate::frontend::pages::article_resource;
use crate::frontend::{article_title, user_link};
use leptos::*;
use leptos_router::*;
@ -19,10 +19,9 @@ pub fn ArticleHistory() -> impl IntoView {
<h1>{article_title(&article.article)}</h1>
{
article.edits.into_iter().rev().map(|edit| {
let path = format!("/article/{}/diff/{}", article.article.title, edit.hash.0);
// TODO: need to return username from backend and show it
let label = format!("{} ({})", edit.summary, edit.created.to_rfc2822());
view! {<li><a href={path}>{label}</a></li> }
let path = format!("/article/{}/diff/{}", article.article.title, edit.edit.hash.0);
let label = format!("{} ({})", edit.edit.summary, edit.edit.created.to_rfc2822());
view! {<li><a href={path}>{label}</a>" by "{user_link(&edit.creator)}</li> }
}).collect::<Vec<_>>()
}
</div>

View File

@ -1,5 +1,6 @@
use crate::frontend::components::article_nav::ArticleNav;
use crate::frontend::pages::article_resource;
use crate::frontend::user_link;
use leptos::*;
use leptos_router::*;
@ -17,12 +18,15 @@ pub fn EditDiff() -> impl IntoView {
.get_untracked()
.get("hash")
.cloned().unwrap();
let edit = article.edits.iter().find(|e| e.hash.0.to_string() == hash).unwrap();
// TODO: need to show username
let edit = article.edits.iter().find(|e| e.edit.hash.0.to_string() == hash).unwrap();
let label = format!("{} ({})", edit.edit.summary, edit.edit.created.to_rfc2822());
view! {
<div class="item-view">
<h1>{article.article.title.replace('_', " ")}</h1>
<pre>{edit.diff.clone()}</pre>
<h2>{label}</h2>
<p>"by "{user_link(&edit.creator)}</p>
<pre>{edit.edit.diff.clone()}</pre>
</div>
}
})

View File

@ -1,6 +1,6 @@
use crate::common::{DbPerson, GetUserData};
use crate::frontend::app::GlobalState;
use crate::frontend::extract_domain;
use crate::frontend::user_title;
use leptos::*;
use leptos_router::use_params_map;
@ -30,14 +30,9 @@ pub fn UserProfile() -> impl IntoView {
}}
<Suspense fallback=|| view! { "Loading..." }> {
move || user_profile.get().map(|person: DbPerson| {
let name =
if person.local {
person.username
} else {
format!("{}@{}", person.username, extract_domain(&person.ap_id))
};
view! {
<h1>{name}</h1>
<h1>{user_title(&person)}</h1>
<p>TODO: create actual user profile</p>
}
})
}</Suspense>

View File

@ -56,7 +56,6 @@ impl TestData {
] {
j.join().unwrap();
}
dbg!(&alpha_db_path);
Self {
alpha: IbisInstance::start(alpha_db_path, port_alpha, "alpha").await,

View File

@ -52,7 +52,7 @@ async fn test_create_read_and_edit_local_article() -> MyResult<()> {
let edit_res = data.alpha.edit_article(&edit_form).await?;
assert_eq!(edit_form.new_text, edit_res.article.text);
assert_eq!(2, edit_res.edits.len());
assert_eq!(edit_form.summary, edit_res.edits[1].summary);
assert_eq!(edit_form.summary, edit_res.edits[1].edit.summary);
let search_form = SearchArticleData {
query: create_form.title.clone(),
@ -215,6 +215,7 @@ async fn test_edit_local_article() -> MyResult<()> {
assert_eq!(edit_res.article.text, edit_form.new_text);
assert_eq!(edit_res.edits.len(), 2);
assert!(edit_res.edits[0]
.edit
.ap_id
.to_string()
.starts_with(&edit_res.article.ap_id.to_string()));
@ -289,6 +290,7 @@ async fn test_edit_remote_article() -> MyResult<()> {
assert_eq!(2, edit_res.edits.len());
assert!(!edit_res.article.local);
assert!(edit_res.edits[0]
.edit
.ap_id
.to_string()
.starts_with(&edit_res.article.ap_id.to_string()));
@ -402,7 +404,7 @@ async fn test_federated_edit_conflict() -> MyResult<()> {
};
let get_res = data.alpha.get_article(get_article_data).await?;
assert_eq!(&create_res.edits.len(), &get_res.edits.len());
assert_eq!(&create_res.edits[0].hash, &get_res.edits[0].hash);
assert_eq!(&create_res.edits[0].edit.hash, &get_res.edits[0].edit.hash);
let edit_form = EditArticleData {
article_id: get_res.article.id,
new_text: "Lorem Ipsum\n".to_string(),
@ -415,6 +417,7 @@ async fn test_federated_edit_conflict() -> MyResult<()> {
assert_eq!(2, edit_res.edits.len());
assert!(!edit_res.article.local);
assert!(edit_res.edits[1]
.edit
.ap_id
.to_string()
.starts_with(&edit_res.article.ap_id.to_string()));
@ -528,9 +531,9 @@ async fn test_fork_article() -> MyResult<()> {
assert_eq!(resolved_article.title, forked_article.title);
assert_eq!(resolved_article.text, forked_article.text);
assert_eq!(resolve_res.edits.len(), fork_res.edits.len());
assert_eq!(resolve_res.edits[0].diff, fork_res.edits[0].diff);
assert_eq!(resolve_res.edits[0].hash, fork_res.edits[0].hash);
assert_ne!(resolve_res.edits[0].id, fork_res.edits[0].id);
assert_eq!(resolve_res.edits[0].edit.diff, fork_res.edits[0].edit.diff);
assert_eq!(resolve_res.edits[0].edit.hash, fork_res.edits[0].edit.hash);
assert_ne!(resolve_res.edits[0].edit.id, fork_res.edits[0].edit.id);
assert_eq!(resolve_res.latest_version, fork_res.latest_version);
assert_ne!(resolved_article.ap_id, forked_article.ap_id);
assert!(forked_article.local);