mirror of
https://github.com/Nutomic/ibis.git
synced 2024-11-22 14:11:10 +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;
|
flex-grow: 1;
|
||||||
padding: 20px;
|
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
|
// copy edits to new article
|
||||||
// this could also be done in sql
|
// 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 {
|
for e in edits {
|
||||||
let ap_id = DbEditForm::generate_ap_id(&article, &e.hash)?;
|
let ap_id = DbEditForm::generate_ap_id(&article, &e.hash)?;
|
||||||
let form = DbEditForm {
|
let form = DbEditForm {
|
||||||
|
@ -221,7 +224,7 @@ pub(super) async fn resolve_article(
|
||||||
) -> MyResult<Json<ArticleView>> {
|
) -> MyResult<Json<ArticleView>> {
|
||||||
let article: DbArticle = ObjectId::from(query.id).dereference(&data).await?;
|
let article: DbArticle = ObjectId::from(query.id).dereference(&data).await?;
|
||||||
let edits = DbEdit::read_for_article(&article, &data.db_connection)?;
|
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 {
|
Ok(Json(ArticleView {
|
||||||
article,
|
article,
|
||||||
edits,
|
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::backend::error::MyResult;
|
||||||
use crate::common::EditVersion;
|
|
||||||
use crate::common::{DbArticle, DbEdit};
|
use crate::common::{DbArticle, DbEdit};
|
||||||
|
use crate::common::{EditVersion, EditView};
|
||||||
use activitypub_federation::fetch::object_id::ObjectId;
|
use activitypub_federation::fetch::object_id::ObjectId;
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use diesel::ExpressionMethods;
|
use diesel::ExpressionMethods;
|
||||||
|
@ -83,13 +83,15 @@ impl DbEdit {
|
||||||
.get_result(conn.deref_mut())?)
|
.get_result(conn.deref_mut())?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: create internal variant which doesnt return person?
|
||||||
pub fn read_for_article(
|
pub fn read_for_article(
|
||||||
article: &DbArticle,
|
article: &DbArticle,
|
||||||
conn: &Mutex<PgConnection>,
|
conn: &Mutex<PgConnection>,
|
||||||
) -> MyResult<Vec<Self>> {
|
) -> MyResult<Vec<EditView>> {
|
||||||
let mut conn = conn.lock().unwrap();
|
let mut conn = conn.lock().unwrap();
|
||||||
Ok(edit::table
|
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())?)
|
.get_results(conn.deref_mut())?)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,7 +44,7 @@ impl Collection for DbEditCollection {
|
||||||
article
|
article
|
||||||
.edits
|
.edits
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|a| a.into_json(data))
|
.map(|a| a.edit.into_json(data))
|
||||||
.collect::<Vec<_>>(),
|
.collect::<Vec<_>>(),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use crate::backend::error::MyResult;
|
use crate::backend::error::MyResult;
|
||||||
use crate::common::DbEdit;
|
|
||||||
use crate::common::EditVersion;
|
use crate::common::EditVersion;
|
||||||
|
use crate::common::EditView;
|
||||||
use activitypub_federation::fetch::object_id::ObjectId;
|
use activitypub_federation::fetch::object_id::ObjectId;
|
||||||
use activitypub_federation::traits::Object;
|
use activitypub_federation::traits::Object;
|
||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
|
@ -30,15 +30,15 @@ where
|
||||||
///
|
///
|
||||||
/// TODO: testing
|
/// TODO: testing
|
||||||
/// TODO: should cache all these generated versions
|
/// 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();
|
let mut generated = String::new();
|
||||||
if version == &EditVersion::default() {
|
if version == &EditVersion::default() {
|
||||||
return Ok(generated);
|
return Ok(generated);
|
||||||
}
|
}
|
||||||
for e in edits {
|
for e in edits {
|
||||||
let patch = Patch::from_str(&e.diff)?;
|
let patch = Patch::from_str(&e.edit.diff)?;
|
||||||
generated = apply(&generated, &patch)?;
|
generated = apply(&generated, &patch)?;
|
||||||
if &e.hash == version {
|
if &e.edit.hash == version {
|
||||||
return Ok(generated);
|
return Ok(generated);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -48,14 +48,16 @@ pub fn generate_article_version(edits: &Vec<DbEdit>, version: &EditVersion) -> M
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::common::{DbEdit, DbPerson};
|
||||||
use activitypub_federation::fetch::object_id::ObjectId;
|
use activitypub_federation::fetch::object_id::ObjectId;
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
use diffy::create_patch;
|
use diffy::create_patch;
|
||||||
|
|
||||||
fn create_edits() -> MyResult<Vec<DbEdit>> {
|
fn create_edits() -> MyResult<Vec<EditView>> {
|
||||||
let generate_edit = |a, b| -> MyResult<DbEdit> {
|
let generate_edit = |a, b| -> MyResult<EditView> {
|
||||||
let diff = create_patch(a, b).to_string();
|
let diff = create_patch(a, b).to_string();
|
||||||
Ok(DbEdit {
|
Ok(EditView {
|
||||||
|
edit: DbEdit {
|
||||||
id: 0,
|
id: 0,
|
||||||
creator_id: 0,
|
creator_id: 0,
|
||||||
hash: EditVersion::new(&diff),
|
hash: EditVersion::new(&diff),
|
||||||
|
@ -65,6 +67,17 @@ mod test {
|
||||||
article_id: 0,
|
article_id: 0,
|
||||||
previous_version_id: Default::default(),
|
previous_version_id: Default::default(),
|
||||||
created: Utc::now(),
|
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([
|
Ok([
|
||||||
|
@ -78,7 +91,7 @@ mod test {
|
||||||
#[test]
|
#[test]
|
||||||
fn test_generate_article_version() -> MyResult<()> {
|
fn test_generate_article_version() -> MyResult<()> {
|
||||||
let edits = create_edits()?;
|
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);
|
assert_eq!("sda\n", generated);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,7 +37,7 @@ pub struct ListArticlesData {
|
||||||
pub struct ArticleView {
|
pub struct ArticleView {
|
||||||
pub article: DbArticle,
|
pub article: DbArticle,
|
||||||
pub latest_version: EditVersion,
|
pub latest_version: EditVersion,
|
||||||
pub edits: Vec<DbEdit>,
|
pub edits: Vec<EditView>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
#[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
|
// TODO: we could use hash as primary key, but that gives errors on forking because
|
||||||
// the same edit is used for multiple articles
|
// the same edit is used for multiple articles
|
||||||
pub id: i32,
|
pub id: i32,
|
||||||
|
#[serde(skip)]
|
||||||
pub creator_id: i32,
|
pub creator_id: i32,
|
||||||
/// UUID built from sha224 hash of diff
|
/// UUID built from sha224 hash of diff
|
||||||
pub hash: EditVersion,
|
pub hash: EditVersion,
|
||||||
|
@ -78,6 +79,14 @@ pub struct DbEdit {
|
||||||
pub created: DateTime<Utc>,
|
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
|
/// 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.
|
/// and using the first 16 bytes so that it fits into UUID.
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
|
#[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());
|
let (search_query, set_search_query) = create_signal(String::new());
|
||||||
view! {
|
view! {
|
||||||
<nav class="inner">
|
<nav class="inner" style="min-width: 250px;">
|
||||||
<li>
|
<li>
|
||||||
<A href="/">"Main Page"</A>
|
<A href="/">"Main Page"</A>
|
||||||
</li>
|
</li>
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
use crate::common::utils::extract_domain;
|
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 api;
|
||||||
pub mod app;
|
pub mod app;
|
||||||
|
@ -31,3 +33,18 @@ fn article_title(article: &DbArticle) -> String {
|
||||||
format!("{}@{}", title, extract_domain(&article.ap_id))
|
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::components::article_nav::ArticleNav;
|
||||||
use crate::frontend::pages::article_resource;
|
use crate::frontend::pages::article_resource;
|
||||||
|
use crate::frontend::{article_title, user_link};
|
||||||
use leptos::*;
|
use leptos::*;
|
||||||
use leptos_router::*;
|
use leptos_router::*;
|
||||||
|
|
||||||
|
@ -19,10 +19,9 @@ pub fn ArticleHistory() -> impl IntoView {
|
||||||
<h1>{article_title(&article.article)}</h1>
|
<h1>{article_title(&article.article)}</h1>
|
||||||
{
|
{
|
||||||
article.edits.into_iter().rev().map(|edit| {
|
article.edits.into_iter().rev().map(|edit| {
|
||||||
let path = format!("/article/{}/diff/{}", article.article.title, edit.hash.0);
|
let path = format!("/article/{}/diff/{}", article.article.title, edit.edit.hash.0);
|
||||||
// TODO: need to return username from backend and show it
|
let label = format!("{} ({})", edit.edit.summary, edit.edit.created.to_rfc2822());
|
||||||
let label = format!("{} ({})", edit.summary, edit.created.to_rfc2822());
|
view! {<li><a href={path}>{label}</a>" by "{user_link(&edit.creator)}</li> }
|
||||||
view! {<li><a href={path}>{label}</a></li> }
|
|
||||||
}).collect::<Vec<_>>()
|
}).collect::<Vec<_>>()
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
use crate::frontend::components::article_nav::ArticleNav;
|
use crate::frontend::components::article_nav::ArticleNav;
|
||||||
use crate::frontend::pages::article_resource;
|
use crate::frontend::pages::article_resource;
|
||||||
|
use crate::frontend::user_link;
|
||||||
use leptos::*;
|
use leptos::*;
|
||||||
use leptos_router::*;
|
use leptos_router::*;
|
||||||
|
|
||||||
|
@ -17,12 +18,15 @@ pub fn EditDiff() -> impl IntoView {
|
||||||
.get_untracked()
|
.get_untracked()
|
||||||
.get("hash")
|
.get("hash")
|
||||||
.cloned().unwrap();
|
.cloned().unwrap();
|
||||||
let edit = article.edits.iter().find(|e| e.hash.0.to_string() == hash).unwrap();
|
let edit = article.edits.iter().find(|e| e.edit.hash.0.to_string() == hash).unwrap();
|
||||||
// TODO: need to show username
|
let label = format!("{} ({})", edit.edit.summary, edit.edit.created.to_rfc2822());
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
<div class="item-view">
|
<div class="item-view">
|
||||||
<h1>{article.article.title.replace('_', " ")}</h1>
|
<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>
|
</div>
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use crate::common::{DbPerson, GetUserData};
|
use crate::common::{DbPerson, GetUserData};
|
||||||
use crate::frontend::app::GlobalState;
|
use crate::frontend::app::GlobalState;
|
||||||
use crate::frontend::extract_domain;
|
use crate::frontend::user_title;
|
||||||
use leptos::*;
|
use leptos::*;
|
||||||
use leptos_router::use_params_map;
|
use leptos_router::use_params_map;
|
||||||
|
|
||||||
|
@ -30,14 +30,9 @@ pub fn UserProfile() -> impl IntoView {
|
||||||
}}
|
}}
|
||||||
<Suspense fallback=|| view! { "Loading..." }> {
|
<Suspense fallback=|| view! { "Loading..." }> {
|
||||||
move || user_profile.get().map(|person: DbPerson| {
|
move || user_profile.get().map(|person: DbPerson| {
|
||||||
let name =
|
|
||||||
if person.local {
|
|
||||||
person.username
|
|
||||||
} else {
|
|
||||||
format!("{}@{}", person.username, extract_domain(&person.ap_id))
|
|
||||||
};
|
|
||||||
view! {
|
view! {
|
||||||
<h1>{name}</h1>
|
<h1>{user_title(&person)}</h1>
|
||||||
|
<p>TODO: create actual user profile</p>
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}</Suspense>
|
}</Suspense>
|
||||||
|
|
|
@ -56,7 +56,6 @@ impl TestData {
|
||||||
] {
|
] {
|
||||||
j.join().unwrap();
|
j.join().unwrap();
|
||||||
}
|
}
|
||||||
dbg!(&alpha_db_path);
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
alpha: IbisInstance::start(alpha_db_path, port_alpha, "alpha").await,
|
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?;
|
let edit_res = data.alpha.edit_article(&edit_form).await?;
|
||||||
assert_eq!(edit_form.new_text, edit_res.article.text);
|
assert_eq!(edit_form.new_text, edit_res.article.text);
|
||||||
assert_eq!(2, edit_res.edits.len());
|
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 {
|
let search_form = SearchArticleData {
|
||||||
query: create_form.title.clone(),
|
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.article.text, edit_form.new_text);
|
||||||
assert_eq!(edit_res.edits.len(), 2);
|
assert_eq!(edit_res.edits.len(), 2);
|
||||||
assert!(edit_res.edits[0]
|
assert!(edit_res.edits[0]
|
||||||
|
.edit
|
||||||
.ap_id
|
.ap_id
|
||||||
.to_string()
|
.to_string()
|
||||||
.starts_with(&edit_res.article.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_eq!(2, edit_res.edits.len());
|
||||||
assert!(!edit_res.article.local);
|
assert!(!edit_res.article.local);
|
||||||
assert!(edit_res.edits[0]
|
assert!(edit_res.edits[0]
|
||||||
|
.edit
|
||||||
.ap_id
|
.ap_id
|
||||||
.to_string()
|
.to_string()
|
||||||
.starts_with(&edit_res.article.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?;
|
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.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 {
|
let edit_form = EditArticleData {
|
||||||
article_id: get_res.article.id,
|
article_id: get_res.article.id,
|
||||||
new_text: "Lorem Ipsum\n".to_string(),
|
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_eq!(2, edit_res.edits.len());
|
||||||
assert!(!edit_res.article.local);
|
assert!(!edit_res.article.local);
|
||||||
assert!(edit_res.edits[1]
|
assert!(edit_res.edits[1]
|
||||||
|
.edit
|
||||||
.ap_id
|
.ap_id
|
||||||
.to_string()
|
.to_string()
|
||||||
.starts_with(&edit_res.article.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.title, forked_article.title);
|
||||||
assert_eq!(resolved_article.text, forked_article.text);
|
assert_eq!(resolved_article.text, forked_article.text);
|
||||||
assert_eq!(resolve_res.edits.len(), fork_res.edits.len());
|
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].edit.diff, fork_res.edits[0].edit.diff);
|
||||||
assert_eq!(resolve_res.edits[0].hash, fork_res.edits[0].hash);
|
assert_eq!(resolve_res.edits[0].edit.hash, fork_res.edits[0].edit.hash);
|
||||||
assert_ne!(resolve_res.edits[0].id, fork_res.edits[0].id);
|
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_eq!(resolve_res.latest_version, fork_res.latest_version);
|
||||||
assert_ne!(resolved_article.ap_id, forked_article.ap_id);
|
assert_ne!(resolved_article.ap_id, forked_article.ap_id);
|
||||||
assert!(forked_article.local);
|
assert!(forked_article.local);
|
||||||
|
|
Loading…
Reference in a new issue