mirror of
https://github.com/Nutomic/ibis.git
synced 2024-11-22 08:11:08 +00:00
Link user profile from article history and diff view
This commit is contained in:
parent
47362b52da
commit
911fadb94b
13 changed files with 97 additions and 49 deletions
|
@ -22,3 +22,7 @@ main {
|
|||
flex-grow: 1;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
pre {
|
||||
white-space: pre-wrap;
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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())?)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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?;
|
||||
|
|
|
@ -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,14 +48,16 @@ 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 {
|
||||
Ok(EditView {
|
||||
edit: DbEdit {
|
||||
id: 0,
|
||||
creator_id: 0,
|
||||
hash: EditVersion::new(&diff),
|
||||
|
@ -65,6 +67,17 @@ mod test {
|
|||
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(())
|
||||
}
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
}
|
||||
})
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -56,7 +56,6 @@ impl TestData {
|
|||
] {
|
||||
j.join().unwrap();
|
||||
}
|
||||
dbg!(&alpha_db_path);
|
||||
|
||||
Self {
|
||||
alpha: IbisInstance::start(alpha_db_path, port_alpha, "alpha").await,
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Reference in a new issue