mirror of
https://github.com/Nutomic/ibis.git
synced 2024-11-22 08:11:08 +00:00
Use single api endpoint for notifications (conflicts and new articles)
This commit is contained in:
parent
7531476066
commit
21c77c2f24
18 changed files with 231 additions and 158 deletions
|
@ -42,7 +42,7 @@ use diffy::create_patch;
|
|||
/// Create a new article with empty text, and federate it to followers.
|
||||
#[debug_handler]
|
||||
pub(in crate::backend::api) async fn create_article(
|
||||
Extension(user): Extension<LocalUserView>,
|
||||
Extension(mut user): Extension<LocalUserView>,
|
||||
data: Data<IbisData>,
|
||||
Form(create_article): Form<CreateArticleForm>,
|
||||
) -> MyResult<Json<ArticleView>> {
|
||||
|
@ -79,9 +79,13 @@ pub(in crate::backend::api) async fn create_article(
|
|||
previous_version_id: article.latest_edit_version(&data)?,
|
||||
resolve_conflict_id: None,
|
||||
};
|
||||
|
||||
// workaround so the edit goes through
|
||||
user.local_user.admin = true;
|
||||
let _ = edit_article(Extension(user), data.reset_request_count(), Form(edit_data)).await?;
|
||||
|
||||
let article_view = DbArticle::read_view(article.id, &data)?;
|
||||
// allow reading unapproved article here
|
||||
let article_view = DbArticle::read_view(article.id, true, &data)?;
|
||||
CreateArticle::send_to_followers(article_view.article.clone(), &data).await?;
|
||||
|
||||
Ok(Json(article_view))
|
||||
|
@ -102,11 +106,12 @@ pub(in crate::backend::api) async fn edit_article(
|
|||
data: Data<IbisData>,
|
||||
Form(mut edit_form): Form<EditArticleForm>,
|
||||
) -> MyResult<Json<Option<ApiConflict>>> {
|
||||
let is_admin = check_is_admin(&user).is_ok();
|
||||
// resolve conflict if any
|
||||
if let Some(resolve_conflict_id) = edit_form.resolve_conflict_id {
|
||||
DbConflict::delete(resolve_conflict_id, &data)?;
|
||||
}
|
||||
let original_article = DbArticle::read_view(edit_form.article_id, &data)?;
|
||||
let original_article = DbArticle::read_view(edit_form.article_id, is_admin, &data)?;
|
||||
if edit_form.new_text == original_article.article.text {
|
||||
return Err(anyhow!("Edit contains no changes").into());
|
||||
}
|
||||
|
@ -148,7 +153,7 @@ pub(in crate::backend::api) async fn edit_article(
|
|||
previous_version_id: previous_version.hash,
|
||||
};
|
||||
let conflict = DbConflict::create(&form, &data)?;
|
||||
Ok(Json(conflict.to_api_conflict(&data).await?))
|
||||
Ok(Json(conflict.to_api_conflict(is_admin, &data).await?))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -156,19 +161,23 @@ pub(in crate::backend::api) async fn edit_article(
|
|||
#[debug_handler]
|
||||
pub(in crate::backend::api) async fn get_article(
|
||||
Query(query): Query<GetArticleForm>,
|
||||
Extension(user): Extension<LocalUserView>,
|
||||
data: Data<IbisData>,
|
||||
) -> MyResult<Json<ArticleView>> {
|
||||
let is_admin = check_is_admin(&user).is_ok();
|
||||
match (query.title, query.id) {
|
||||
(Some(title), None) => Ok(Json(DbArticle::read_view_title(
|
||||
&title,
|
||||
query.domain,
|
||||
is_admin,
|
||||
&data,
|
||||
)?)),
|
||||
(None, Some(id)) => {
|
||||
if query.domain.is_some() {
|
||||
return Err(anyhow!("Cant combine id and instance_domain").into());
|
||||
}
|
||||
Ok(Json(DbArticle::read_view(id, &data)?))
|
||||
let article = DbArticle::read_view(id, is_admin, &data)?;
|
||||
Ok(Json(article))
|
||||
}
|
||||
_ => Err(anyhow!("Must pass exactly one of title, id").into()),
|
||||
}
|
||||
|
@ -190,12 +199,13 @@ pub(in crate::backend::api) async fn list_articles(
|
|||
/// how an article should be edited.
|
||||
#[debug_handler]
|
||||
pub(in crate::backend::api) async fn fork_article(
|
||||
Extension(_user): Extension<LocalUserView>,
|
||||
Extension(user): Extension<LocalUserView>,
|
||||
data: Data<IbisData>,
|
||||
Form(fork_form): Form<ForkArticleForm>,
|
||||
) -> MyResult<Json<ArticleView>> {
|
||||
let is_admin = check_is_admin(&user).is_ok();
|
||||
// TODO: lots of code duplicated from create_article(), can move it into helper
|
||||
let original_article = DbArticle::read(fork_form.article_id, &data)?;
|
||||
let original_article = DbArticle::read_view(fork_form.article_id, is_admin, &data)?;
|
||||
|
||||
let local_instance = DbInstance::read_local_instance(&data)?;
|
||||
let ap_id = ObjectId::parse(&format!(
|
||||
|
@ -206,18 +216,19 @@ pub(in crate::backend::api) async fn fork_article(
|
|||
))?;
|
||||
let form = DbArticleForm {
|
||||
title: fork_form.new_title,
|
||||
text: original_article.text.clone(),
|
||||
text: original_article.article.text.clone(),
|
||||
ap_id,
|
||||
instance_id: local_instance.id,
|
||||
local: true,
|
||||
protected: false,
|
||||
approved: data.config.article_approval,
|
||||
approved: !data.config.article_approval,
|
||||
};
|
||||
let article = DbArticle::create(form, &data)?;
|
||||
|
||||
// copy edits to new article
|
||||
// this could also be done in sql
|
||||
let edits = DbEdit::read_for_article(&original_article, &data)?
|
||||
let edits = original_article
|
||||
.edits
|
||||
.into_iter()
|
||||
.map(|e| e.edit)
|
||||
.collect::<Vec<_>>();
|
||||
|
@ -238,7 +249,7 @@ pub(in crate::backend::api) async fn fork_article(
|
|||
|
||||
CreateArticle::send_to_followers(article.clone(), &data).await?;
|
||||
|
||||
Ok(Json(DbArticle::read_view(article.id, &data)?))
|
||||
Ok(Json(DbArticle::read_view(article.id, is_admin, &data)?))
|
||||
}
|
||||
|
||||
/// Fetch a remote article, including edits collection. Allows viewing and editing. Note that new
|
||||
|
@ -288,17 +299,6 @@ pub(in crate::backend::api) async fn protect_article(
|
|||
Ok(Json(article))
|
||||
}
|
||||
|
||||
/// Get a list of all unresolved edit conflicts.
|
||||
#[debug_handler]
|
||||
pub async fn list_approval_required(
|
||||
Extension(user): Extension<LocalUserView>,
|
||||
data: Data<IbisData>,
|
||||
) -> MyResult<Json<Vec<DbArticle>>> {
|
||||
check_is_admin(&user)?;
|
||||
let articles = DbArticle::list_approval_required(&data)?;
|
||||
Ok(Json(articles))
|
||||
}
|
||||
|
||||
/// Get a list of all unresolved edit conflicts.
|
||||
#[debug_handler]
|
||||
pub async fn approve_article(
|
||||
|
|
|
@ -22,28 +22,25 @@ use crate::{
|
|||
AUTH_COOKIE,
|
||||
},
|
||||
},
|
||||
database::{conflict::DbConflict, IbisData},
|
||||
database::IbisData,
|
||||
error::MyResult,
|
||||
},
|
||||
common::{ApiConflict, LocalUserView},
|
||||
common::LocalUserView,
|
||||
};
|
||||
use activitypub_federation::config::Data;
|
||||
use anyhow::anyhow;
|
||||
use article::{approve_article, list_approval_required};
|
||||
use article::approve_article;
|
||||
use axum::{
|
||||
body::Body,
|
||||
http::{Request, StatusCode},
|
||||
middleware::{self, Next},
|
||||
response::Response,
|
||||
routing::{get, post},
|
||||
Extension,
|
||||
Json,
|
||||
Router,
|
||||
};
|
||||
use axum_extra::extract::CookieJar;
|
||||
use axum_macros::debug_handler;
|
||||
use futures::future::try_join_all;
|
||||
use instance::list_remote_instances;
|
||||
use user::{count_notifications, list_notifications};
|
||||
|
||||
pub mod article;
|
||||
pub mod instance;
|
||||
|
@ -59,18 +56,15 @@ pub fn api_routes() -> Router<()> {
|
|||
.route("/article/fork", post(fork_article))
|
||||
.route("/article/resolve", get(resolve_article))
|
||||
.route("/article/protect", post(protect_article))
|
||||
.route(
|
||||
"/article/list/approval_required",
|
||||
get(list_approval_required),
|
||||
)
|
||||
.route("/article/approve", post(approve_article))
|
||||
.route("/edit_conflicts", get(edit_conflicts))
|
||||
.route("/instance", get(get_instance))
|
||||
.route("/instance/follow", post(follow_instance))
|
||||
.route("/instance/resolve", get(resolve_instance))
|
||||
.route("/instance/list", get(list_remote_instances))
|
||||
.route("/search", get(search_article))
|
||||
.route("/user", get(get_user))
|
||||
.route("/user/notifications/list", get(list_notifications))
|
||||
.route("/user/notifications/count", get(count_notifications))
|
||||
.route("/account/register", post(register_user))
|
||||
.route("/account/login", post(login_user))
|
||||
.route("/account/my_profile", get(my_profile))
|
||||
|
@ -99,21 +93,3 @@ fn check_is_admin(user: &LocalUserView) -> MyResult<()> {
|
|||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get a list of all unresolved edit conflicts.
|
||||
#[debug_handler]
|
||||
async fn edit_conflicts(
|
||||
Extension(user): Extension<LocalUserView>,
|
||||
data: Data<IbisData>,
|
||||
) -> MyResult<Json<Vec<ApiConflict>>> {
|
||||
let conflicts = DbConflict::list(&user.person, &data)?;
|
||||
let conflicts: Vec<ApiConflict> = try_join_all(conflicts.into_iter().map(|c| {
|
||||
let data = data.reset_request_count();
|
||||
async move { c.to_api_conflict(&data).await }
|
||||
}))
|
||||
.await?
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.collect();
|
||||
Ok(Json(conflicts))
|
||||
}
|
||||
|
|
|
@ -1,17 +1,27 @@
|
|||
use super::check_is_admin;
|
||||
use crate::{
|
||||
backend::{
|
||||
database::{read_jwt_secret, IbisData},
|
||||
database::{conflict::DbConflict, read_jwt_secret, IbisData},
|
||||
error::MyResult,
|
||||
},
|
||||
common::{DbPerson, GetUserForm, LocalUserView, LoginUserForm, RegisterUserForm},
|
||||
common::{
|
||||
DbArticle,
|
||||
DbPerson,
|
||||
GetUserForm,
|
||||
LocalUserView,
|
||||
LoginUserForm,
|
||||
Notification,
|
||||
RegisterUserForm,
|
||||
},
|
||||
};
|
||||
use activitypub_federation::config::Data;
|
||||
use anyhow::anyhow;
|
||||
use axum::{extract::Query, Form, Json};
|
||||
use axum::{extract::Query, Extension, Form, Json};
|
||||
use axum_extra::extract::cookie::{Cookie, CookieJar, Expiration, SameSite};
|
||||
use axum_macros::debug_handler;
|
||||
use bcrypt::verify;
|
||||
use chrono::Utc;
|
||||
use futures::future::try_join_all;
|
||||
use jsonwebtoken::{
|
||||
decode,
|
||||
encode,
|
||||
|
@ -145,3 +155,49 @@ pub(in crate::backend::api) async fn get_user(
|
|||
&data,
|
||||
)?))
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
pub(crate) async fn list_notifications(
|
||||
Extension(user): Extension<LocalUserView>,
|
||||
data: Data<IbisData>,
|
||||
) -> MyResult<Json<Vec<Notification>>> {
|
||||
let is_admin = check_is_admin(&user).is_ok();
|
||||
let conflicts = DbConflict::list(&user.person, &data)?;
|
||||
let conflicts: Vec<_> = try_join_all(conflicts.into_iter().map(|c| {
|
||||
let data = data.reset_request_count();
|
||||
async move { c.to_api_conflict(is_admin, &data).await }
|
||||
}))
|
||||
.await?;
|
||||
let mut notifications: Vec<_> = conflicts
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.map(Notification::EditConflict)
|
||||
.collect();
|
||||
|
||||
if check_is_admin(&user).is_ok() {
|
||||
let articles = DbArticle::list_approval_required(&data)?;
|
||||
notifications.extend(
|
||||
articles
|
||||
.into_iter()
|
||||
.map(Notification::ArticleApprovalRequired),
|
||||
)
|
||||
}
|
||||
|
||||
Ok(Json(notifications))
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
pub(crate) async fn count_notifications(
|
||||
Extension(user): Extension<LocalUserView>,
|
||||
data: Data<IbisData>,
|
||||
) -> MyResult<Json<usize>> {
|
||||
let mut count = 0;
|
||||
let conflicts = DbConflict::list(&user.person, &data)?;
|
||||
count += conflicts.len();
|
||||
if check_is_admin(&user).is_ok() {
|
||||
let articles = DbArticle::list_approval_required(&data)?;
|
||||
count += articles.len();
|
||||
}
|
||||
|
||||
Ok(Json(count))
|
||||
}
|
||||
|
|
|
@ -87,14 +87,13 @@ impl DbArticle {
|
|||
.get_result::<Self>(conn.deref_mut())?)
|
||||
}
|
||||
|
||||
pub fn read(id: ArticleId, data: &IbisData) -> MyResult<Self> {
|
||||
pub fn read_view(id: ArticleId, is_admin: bool, data: &IbisData) -> MyResult<ArticleView> {
|
||||
let mut conn = data.db_pool.get()?;
|
||||
Ok(article::table.find(id).get_result(conn.deref_mut())?)
|
||||
let mut query = article::table.find(id).into_boxed();
|
||||
if !is_admin {
|
||||
query = query.filter(article::dsl::approved.eq(true));
|
||||
}
|
||||
|
||||
pub fn read_view(id: ArticleId, data: &IbisData) -> MyResult<ArticleView> {
|
||||
let mut conn = data.db_pool.get()?;
|
||||
let article: DbArticle = { article::table.find(id).get_result(conn.deref_mut())? };
|
||||
let article: DbArticle = query.get_result(conn.deref_mut())?;
|
||||
let latest_version = article.latest_edit_version(data)?;
|
||||
let edits = DbEdit::read_for_article(&article, data)?;
|
||||
Ok(ArticleView {
|
||||
|
@ -107,6 +106,7 @@ impl DbArticle {
|
|||
pub fn read_view_title(
|
||||
title: &str,
|
||||
domain: Option<String>,
|
||||
admin: bool,
|
||||
data: &IbisData,
|
||||
) -> MyResult<ArticleView> {
|
||||
let mut conn = data.db_pool.get()?;
|
||||
|
@ -115,11 +115,14 @@ impl DbArticle {
|
|||
.inner_join(instance::table)
|
||||
.filter(article::dsl::title.eq(title))
|
||||
.into_boxed();
|
||||
let query = if let Some(domain) = domain {
|
||||
let mut query = if let Some(domain) = domain {
|
||||
query.filter(instance::dsl::domain.eq(domain))
|
||||
} else {
|
||||
query.filter(article::dsl::local.eq(true))
|
||||
};
|
||||
if !admin {
|
||||
query = query.filter(article::dsl::approved.eq(true));
|
||||
}
|
||||
query
|
||||
.select(article::all_columns)
|
||||
.get_result(conn.deref_mut())?
|
||||
|
@ -140,14 +143,6 @@ impl DbArticle {
|
|||
.get_result(conn.deref_mut())?)
|
||||
}
|
||||
|
||||
pub fn read_local_title(title: &str, data: &IbisData) -> MyResult<Self> {
|
||||
let mut conn = data.db_pool.get()?;
|
||||
Ok(article::table
|
||||
.filter(article::dsl::title.eq(title))
|
||||
.filter(article::dsl::local.eq(true))
|
||||
.get_result(conn.deref_mut())?)
|
||||
}
|
||||
|
||||
/// Read all articles, ordered by most recently edited first.
|
||||
///
|
||||
/// TODO: Should get rid of only_local param and rely on instance_id
|
||||
|
|
|
@ -76,10 +76,14 @@ impl DbConflict {
|
|||
Ok(delete(conflict::table.find(id)).get_result(conn.deref_mut())?)
|
||||
}
|
||||
|
||||
pub async fn to_api_conflict(&self, data: &Data<IbisData>) -> MyResult<Option<ApiConflict>> {
|
||||
let article = DbArticle::read(self.article_id, data)?;
|
||||
pub async fn to_api_conflict(
|
||||
&self,
|
||||
is_admin: bool,
|
||||
data: &Data<IbisData>,
|
||||
) -> MyResult<Option<ApiConflict>> {
|
||||
let article = DbArticle::read_view(self.article_id, is_admin, data)?;
|
||||
// Make sure to get latest version from origin so that all conflicts can be resolved
|
||||
let original_article = article.ap_id.dereference_forced(data).await?;
|
||||
let original_article = article.article.ap_id.dereference_forced(data).await?;
|
||||
|
||||
// create common ancestor version
|
||||
let edits = DbEdit::read_for_article(&original_article, data)?;
|
||||
|
|
|
@ -52,7 +52,7 @@ impl Object for DbEdit {
|
|||
}
|
||||
|
||||
async fn into_json(self, data: &Data<Self::DataType>) -> Result<Self::Kind, Self::Error> {
|
||||
let article = DbArticle::read(self.article_id, data)?;
|
||||
let article = DbArticle::read_view(self.article_id, false, data)?;
|
||||
let creator = DbPerson::read(self.creator_id, data)?;
|
||||
Ok(ApubEdit {
|
||||
kind: PatchType::Patch,
|
||||
|
@ -61,7 +61,7 @@ impl Object for DbEdit {
|
|||
summary: self.summary,
|
||||
version: self.hash,
|
||||
previous_version: self.previous_version_id,
|
||||
object: article.ap_id,
|
||||
object: article.article.ap_id,
|
||||
attributed_to: creator.ap_id,
|
||||
published: self.created,
|
||||
})
|
||||
|
|
|
@ -35,7 +35,7 @@ impl Collection for DbEditCollection {
|
|||
owner: &Self::Owner,
|
||||
data: &Data<Self::DataType>,
|
||||
) -> Result<Self::Kind, Self::Error> {
|
||||
let article = DbArticle::read_view(owner.id, data)?;
|
||||
let article = DbArticle::read_view(owner.id, false, data)?;
|
||||
|
||||
let edits = future::try_join_all(
|
||||
article
|
||||
|
|
|
@ -94,8 +94,8 @@ async fn http_get_article(
|
|||
Path(title): Path<String>,
|
||||
data: Data<IbisData>,
|
||||
) -> MyResult<FederationJson<WithContext<ApubArticle>>> {
|
||||
let article = DbArticle::read_local_title(&title, &data)?;
|
||||
let json = article.into_json(&data).await?;
|
||||
let article = DbArticle::read_view_title(&title, None, false, &data)?;
|
||||
let json = article.article.into_json(&data).await?;
|
||||
Ok(FederationJson(WithContext::new_default(json)))
|
||||
}
|
||||
|
||||
|
@ -104,8 +104,8 @@ async fn http_get_article_edits(
|
|||
Path(title): Path<String>,
|
||||
data: Data<IbisData>,
|
||||
) -> MyResult<FederationJson<WithContext<ApubEditCollection>>> {
|
||||
let article = DbArticle::read_local_title(&title, &data)?;
|
||||
let json = DbEditCollection::read_local(&article, &data).await?;
|
||||
let article = DbArticle::read_view_title(&title, None, false, &data)?;
|
||||
let json = DbEditCollection::read_local(&article.article, &data).await?;
|
||||
Ok(FederationJson(WithContext::new_default(json)))
|
||||
}
|
||||
|
||||
|
|
|
@ -250,6 +250,12 @@ pub struct ApiConflict {
|
|||
pub previous_version_id: EditVersion,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub enum Notification {
|
||||
EditConflict(ApiConflict),
|
||||
ArticleApprovalRequired(DbArticle),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[cfg_attr(feature = "ssr", derive(Queryable, Selectable, Identifiable))]
|
||||
#[cfg_attr(feature = "ssr", diesel(table_name = instance, check_for_backend(diesel::pg::Pg)))]
|
||||
|
|
|
@ -19,6 +19,7 @@ use crate::{
|
|||
ListArticlesForm,
|
||||
LocalUserView,
|
||||
LoginUserForm,
|
||||
Notification,
|
||||
ProtectArticleForm,
|
||||
RegisterUserForm,
|
||||
ResolveObject,
|
||||
|
@ -132,10 +133,17 @@ impl ApiClient {
|
|||
.await
|
||||
}
|
||||
|
||||
pub async fn list_articles_approval_required(&self) -> MyResult<Vec<DbArticle>> {
|
||||
pub async fn notifications_list(&self) -> MyResult<Vec<Notification>> {
|
||||
let req = self
|
||||
.client
|
||||
.get(self.request_endpoint("/api/v1/article/list/approval_required"));
|
||||
.get(self.request_endpoint("/api/v1/user/notifications/list"));
|
||||
handle_json_res(req).await
|
||||
}
|
||||
|
||||
pub async fn notifications_count(&self) -> MyResult<usize> {
|
||||
let req = self
|
||||
.client
|
||||
.get(self.request_endpoint("/api/v1/user/notifications/count"));
|
||||
handle_json_res(req).await
|
||||
}
|
||||
|
||||
|
@ -230,13 +238,6 @@ impl ApiClient {
|
|||
handle_json_res(req).await
|
||||
}
|
||||
|
||||
pub async fn get_conflicts(&self) -> MyResult<Vec<ApiConflict>> {
|
||||
let req = self
|
||||
.client
|
||||
.get(self.request_endpoint("/api/v1/edit_conflicts"));
|
||||
Ok(handle_json_res(req).await.unwrap())
|
||||
}
|
||||
|
||||
pub async fn resolve_article(&self, id: Url) -> MyResult<ArticleView> {
|
||||
let resolve_object = ResolveObject { id };
|
||||
self.get_query("/api/v1/article/resolve", Some(resolve_object))
|
||||
|
|
|
@ -13,10 +13,10 @@ use crate::{
|
|||
list::ListArticles,
|
||||
read::ReadArticle,
|
||||
},
|
||||
conflicts::Conflicts,
|
||||
diff::EditDiff,
|
||||
instance::{details::InstanceDetails, list::ListInstances},
|
||||
login::Login,
|
||||
notifications::Notifications,
|
||||
register::Register,
|
||||
search::Search,
|
||||
user_profile::UserProfile,
|
||||
|
@ -125,7 +125,7 @@ pub fn App() -> impl IntoView {
|
|||
<Route path="/login" view=Login />
|
||||
<Route path="/register" view=Register />
|
||||
<Route path="/search" view=Search />
|
||||
<Route path="/conflicts" view=Conflicts />
|
||||
<Route path="/notifications" view=Notifications />
|
||||
</Routes>
|
||||
</main>
|
||||
</Router>
|
||||
|
|
|
@ -19,6 +19,15 @@ pub fn Nav() -> impl IntoView {
|
|||
.unwrap_or_default()
|
||||
},
|
||||
);
|
||||
let notification_count = create_resource(
|
||||
|| (),
|
||||
move |_| async move {
|
||||
GlobalState::api_client()
|
||||
.notifications_count()
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
},
|
||||
);
|
||||
|
||||
let (search_query, set_search_query) = create_signal(String::new());
|
||||
let mut dark_mode = expect_context::<DarkMode>();
|
||||
|
@ -57,7 +66,7 @@ pub fn Nav() -> impl IntoView {
|
|||
<A href="/article/create">"Create Article"</A>
|
||||
</li>
|
||||
<li>
|
||||
<A href="/conflicts">"Edit Conflicts"</A>
|
||||
<A href="/notifications">"Notifications "<span class="indicator-item indicator-end badge badge-neutral">{notification_count}</span></A>
|
||||
</li>
|
||||
</Show>
|
||||
<li>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use crate::{
|
||||
common::{newtypes::ConflictId, ApiConflict, ArticleView, EditArticleForm},
|
||||
common::{newtypes::ConflictId, ApiConflict, ArticleView, EditArticleForm, Notification},
|
||||
frontend::{
|
||||
app::GlobalState,
|
||||
components::{
|
||||
|
@ -35,10 +35,14 @@ pub fn EditArticle() -> impl IntoView {
|
|||
let conflict_id = ConflictId(conflict_id.parse().unwrap());
|
||||
async move {
|
||||
let conflict = GlobalState::api_client()
|
||||
.get_conflicts()
|
||||
.notifications_list()
|
||||
.await
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.filter_map(|n| match n {
|
||||
Notification::EditConflict(c) => Some(c),
|
||||
_ => None,
|
||||
})
|
||||
.find(|c| c.id == conflict_id)
|
||||
.unwrap();
|
||||
set_edit_response.set(EditResponse::Conflict(conflict));
|
||||
|
|
|
@ -1,41 +0,0 @@
|
|||
use crate::frontend::{app::GlobalState, article_link, article_title};
|
||||
use leptos::*;
|
||||
|
||||
#[component]
|
||||
pub fn Conflicts() -> impl IntoView {
|
||||
let conflicts = create_resource(
|
||||
move || {},
|
||||
|_| async move { GlobalState::api_client().get_conflicts().await.unwrap() },
|
||||
);
|
||||
|
||||
view! {
|
||||
<h1>Your unresolved edit conflicts</h1>
|
||||
<Suspense fallback=|| view! { "Loading..." }>
|
||||
<ul>
|
||||
{move || {
|
||||
conflicts
|
||||
.get()
|
||||
.map(|c| {
|
||||
c.into_iter()
|
||||
.map(|c| {
|
||||
let link = format!(
|
||||
"{}/edit/{}",
|
||||
article_link(&c.article),
|
||||
c.id.0,
|
||||
);
|
||||
view! {
|
||||
<li>
|
||||
<a href=link>
|
||||
{article_title(&c.article)} " - " {c.summary}
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
}}
|
||||
|
||||
</ul>
|
||||
</Suspense>
|
||||
}
|
||||
}
|
|
@ -6,10 +6,10 @@ use leptos::{create_resource, Resource, SignalGet};
|
|||
use leptos_router::use_params_map;
|
||||
|
||||
pub(crate) mod article;
|
||||
pub(crate) mod conflicts;
|
||||
pub(crate) mod diff;
|
||||
pub(crate) mod instance;
|
||||
pub(crate) mod login;
|
||||
pub(crate) mod notifications;
|
||||
pub(crate) mod register;
|
||||
pub(crate) mod search;
|
||||
pub(crate) mod user_profile;
|
||||
|
|
55
src/frontend/pages/notifications.rs
Normal file
55
src/frontend/pages/notifications.rs
Normal file
|
@ -0,0 +1,55 @@
|
|||
use crate::{
|
||||
common::Notification,
|
||||
frontend::{app::GlobalState, article_link, article_title},
|
||||
};
|
||||
use leptos::*;
|
||||
|
||||
#[component]
|
||||
pub fn Notifications() -> impl IntoView {
|
||||
let notifications = create_local_resource(
|
||||
move || {},
|
||||
|_| async move {
|
||||
GlobalState::api_client()
|
||||
.notifications_list()
|
||||
.await
|
||||
.unwrap()
|
||||
},
|
||||
);
|
||||
|
||||
view! {
|
||||
<h1 class="text-4xl font-bold font-serif my-6 grow flex-auto">Notifications</h1>
|
||||
<Suspense fallback=|| view! { "Loading..." }>
|
||||
<ul>
|
||||
{move || {
|
||||
notifications
|
||||
.get()
|
||||
.map(|n| {
|
||||
n.into_iter()
|
||||
.map(|n| {
|
||||
use Notification::*;
|
||||
let (link, title) = match n {
|
||||
EditConflict(c) => (format!(
|
||||
"{}/edit/{}",
|
||||
article_link(&c.article),
|
||||
c.id.0)
|
||||
, format!("Conflict: {} - {}", article_title(&c.article), c.summary)),
|
||||
ArticleApprovalRequired(a) => (article_link(&a), format!("Approval required: {}", a.title)),
|
||||
|
||||
};
|
||||
// TODO: need buttons to approve/reject new article, also makes sense to discard edit conflict
|
||||
view! {
|
||||
<li>
|
||||
<a class="link text-lg" href=link>
|
||||
{title}
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
}}
|
||||
|
||||
</ul>
|
||||
</Suspense>
|
||||
}
|
||||
}
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
use ibis::{
|
||||
backend::{
|
||||
config::{IbisConfig, IbisConfigDatabase, IbisConfigFederation, IbisConfigSetup},
|
||||
config::{IbisConfig, IbisConfigDatabase, IbisConfigFederation},
|
||||
start,
|
||||
},
|
||||
common::RegisterUserForm,
|
||||
|
|
|
@ -14,6 +14,7 @@ use ibis::{
|
|||
GetUserForm,
|
||||
ListArticlesForm,
|
||||
LoginUserForm,
|
||||
Notification,
|
||||
ProtectArticleForm,
|
||||
RegisterUserForm,
|
||||
SearchArticleForm,
|
||||
|
@ -377,9 +378,12 @@ async fn test_local_edit_conflict() -> MyResult<()> {
|
|||
.unwrap();
|
||||
assert_eq!("<<<<<<< ours\nIpsum Lorem\n||||||| original\nsome\nexample\ntext\n=======\nLorem Ipsum\n>>>>>>> theirs\n", edit_res.three_way_merge);
|
||||
|
||||
let conflicts = data.alpha.get_conflicts().await?;
|
||||
assert_eq!(1, conflicts.len());
|
||||
assert_eq!(conflicts[0], edit_res);
|
||||
let notifications = data.alpha.notifications_list().await?;
|
||||
assert_eq!(1, notifications.len());
|
||||
let Notification::EditConflict(conflict) = ¬ifications[0] else {
|
||||
panic!()
|
||||
};
|
||||
assert_eq!(conflict, &edit_res);
|
||||
|
||||
let edit_form = EditArticleForm {
|
||||
article_id: create_res.article.id,
|
||||
|
@ -391,8 +395,7 @@ async fn test_local_edit_conflict() -> MyResult<()> {
|
|||
let edit_res = data.alpha.edit_article(&edit_form).await?;
|
||||
assert_eq!(edit_form.new_text, edit_res.article.text);
|
||||
|
||||
let conflicts = data.alpha.get_conflicts().await?;
|
||||
assert_eq!(0, conflicts.len());
|
||||
assert_eq!(0, data.alpha.notifications_count().await?);
|
||||
|
||||
data.stop()
|
||||
}
|
||||
|
@ -463,23 +466,28 @@ async fn test_federated_edit_conflict() -> MyResult<()> {
|
|||
assert_eq!(1, edit_res.edits.len());
|
||||
assert!(!edit_res.article.local);
|
||||
|
||||
let conflicts = data.gamma.get_conflicts().await?;
|
||||
assert_eq!(1, conflicts.len());
|
||||
assert_eq!(1, data.gamma.notifications_count().await?);
|
||||
let notifications = data.gamma.notifications_list().await?;
|
||||
assert_eq!(1, notifications.len());
|
||||
let Notification::EditConflict(conflict) = ¬ifications[0] else {
|
||||
panic!()
|
||||
};
|
||||
|
||||
// resolve the conflict
|
||||
let edit_form = EditArticleForm {
|
||||
article_id: resolve_res.article.id,
|
||||
new_text: "aaaa\n".to_string(),
|
||||
summary: "summary".to_string(),
|
||||
previous_version_id: conflicts[0].previous_version_id.clone(),
|
||||
resolve_conflict_id: Some(conflicts[0].id),
|
||||
previous_version_id: conflict.previous_version_id.clone(),
|
||||
resolve_conflict_id: Some(conflict.id),
|
||||
};
|
||||
let edit_res = data.gamma.edit_article(&edit_form).await?;
|
||||
assert_eq!(edit_form.new_text, edit_res.article.text);
|
||||
assert_eq!(3, edit_res.edits.len());
|
||||
|
||||
let conflicts = data.gamma.get_conflicts().await?;
|
||||
assert_eq!(0, conflicts.len());
|
||||
assert_eq!(0, data.gamma.notifications_count().await?);
|
||||
let notifications = data.gamma.notifications_list().await?;
|
||||
assert_eq!(0, notifications.len());
|
||||
|
||||
data.stop()
|
||||
}
|
||||
|
@ -519,8 +527,7 @@ async fn test_overlapping_edits_no_conflict() -> MyResult<()> {
|
|||
resolve_conflict_id: None,
|
||||
};
|
||||
let edit_res = data.alpha.edit_article(&edit_form).await?;
|
||||
let conflicts = data.alpha.get_conflicts().await?;
|
||||
assert_eq!(0, conflicts.len());
|
||||
assert_eq!(0, data.alpha.notifications_count().await?);
|
||||
assert_eq!(3, edit_res.edits.len());
|
||||
assert_eq!("my\nexample\narticle\n", edit_res.article.text);
|
||||
|
||||
|
@ -752,14 +759,15 @@ async fn test_article_approval_required() -> MyResult<()> {
|
|||
};
|
||||
data.alpha.login(form).await?;
|
||||
|
||||
let list_approval_required = data.alpha.list_articles_approval_required().await?;
|
||||
assert_eq!(1, list_approval_required.len());
|
||||
assert_eq!(create_res.article.id, list_approval_required[0].id);
|
||||
assert_eq!(1, data.alpha.notifications_count().await?);
|
||||
let notifications = data.alpha.notifications_list().await?;
|
||||
assert_eq!(1, notifications.len());
|
||||
let Notification::ArticleApprovalRequired(notif) = ¬ifications[0] else {
|
||||
panic!()
|
||||
};
|
||||
assert_eq!(create_res.article.id, notif.id);
|
||||
|
||||
let approve = data
|
||||
.alpha
|
||||
.approve_article(list_approval_required[0].id)
|
||||
.await?;
|
||||
let approve = data.alpha.approve_article(notif.id).await?;
|
||||
assert_eq!(create_res.article.id, approve.id);
|
||||
assert!(approve.approved);
|
||||
|
||||
|
|
Loading…
Reference in a new issue