diff --git a/web/graphql/add_tag.graphql b/web/graphql/add_tag.graphql
new file mode 100644
index 0000000..0dda9da
--- /dev/null
+++ b/web/graphql/add_tag.graphql
@@ -0,0 +1,3 @@
+mutation AddTagMutation($query: String!, $tag: String!) {
+ tagAdd(query:$query, tag:$tag)
+}
diff --git a/web/graphql/remove_tag.graphql b/web/graphql/remove_tag.graphql
new file mode 100644
index 0000000..fcdd9ff
--- /dev/null
+++ b/web/graphql/remove_tag.graphql
@@ -0,0 +1,3 @@
+mutation RemoveTagMutation($query: String!, $tag: String!) {
+ tagRemove(query:$query, tag:$tag)
+}
diff --git a/web/graphql/schema.json b/web/graphql/schema.json
index a6f2c07..524bcac 100644
--- a/web/graphql/schema.json
+++ b/web/graphql/schema.json
@@ -693,6 +693,96 @@
"ofType": null
}
}
+ },
+ {
+ "args": [
+ {
+ "defaultValue": null,
+ "description": null,
+ "name": "query",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ }
+ },
+ {
+ "defaultValue": null,
+ "description": null,
+ "name": "tag",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ }
+ }
+ ],
+ "deprecationReason": null,
+ "description": null,
+ "isDeprecated": false,
+ "name": "tagAdd",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ }
+ }
+ },
+ {
+ "args": [
+ {
+ "defaultValue": null,
+ "description": null,
+ "name": "query",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ }
+ },
+ {
+ "defaultValue": null,
+ "description": null,
+ "name": "tag",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ }
+ }
+ ],
+ "deprecationReason": null,
+ "description": null,
+ "isDeprecated": false,
+ "name": "tagRemove",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ }
+ }
}
],
"inputFields": null,
diff --git a/web/src/graphql.rs b/web/src/graphql.rs
index e46baee..3f0ae16 100644
--- a/web/src/graphql.rs
+++ b/web/src/graphql.rs
@@ -28,6 +28,22 @@ pub struct ShowThreadQuery;
)]
pub struct MarkReadMutation;
+#[derive(GraphQLQuery)]
+#[graphql(
+ schema_path = "graphql/schema.json",
+ query_path = "graphql/add_tag.graphql",
+ response_derives = "Debug"
+)]
+pub struct AddTagMutation;
+
+#[derive(GraphQLQuery)]
+#[graphql(
+ schema_path = "graphql/schema.json",
+ query_path = "graphql/remove_tag.graphql",
+ response_derives = "Debug"
+)]
+pub struct RemoveTagMutation;
+
pub async fn send_graphql
(body: Body) -> Result, Error>
where
Body: Serialize,
diff --git a/web/src/state.rs b/web/src/state.rs
index 5352b91..8620fb4 100644
--- a/web/src/state.rs
+++ b/web/src/state.rs
@@ -184,6 +184,52 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders) {
Msg::Noop
});
}
+ Msg::AddTag(query, tag) => {
+ let search_url = urls::search(&model.query, 0).to_string();
+ orders.skip().perform_cmd(async move {
+ let res: Result<
+ graphql_client::Response,
+ gloo_net::Error,
+ > = send_graphql(graphql::AddTagMutation::build_query(
+ graphql::add_tag_mutation::Variables {
+ query: query.clone(),
+ tag: tag.clone(),
+ },
+ ))
+ .await;
+ if let Err(e) = res {
+ error!("Failed to add tag {tag} to {query}: {e}");
+ }
+ seed::window()
+ .location()
+ .set_href(&search_url)
+ .expect("failed to change location");
+ Msg::Noop
+ });
+ }
+ Msg::RemoveTag(query, tag) => {
+ let search_url = urls::search(&model.query, 0).to_string();
+ orders.skip().perform_cmd(async move {
+ let res: Result<
+ graphql_client::Response,
+ gloo_net::Error,
+ > = send_graphql(graphql::RemoveTagMutation::build_query(
+ graphql::remove_tag_mutation::Variables {
+ query: query.clone(),
+ tag: tag.clone(),
+ },
+ ))
+ .await;
+ if let Err(e) = res {
+ error!("Failed to remove tag {tag} to {query}: {e}");
+ }
+ seed::window()
+ .location()
+ .set_href(&search_url)
+ .expect("failed to change location");
+ Msg::Noop
+ });
+ }
Msg::FrontPageRequest {
query,
@@ -311,6 +357,36 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders) {
*selected_threads = results.iter().map(|node| node.thread.clone()).collect();
}
}
+ Msg::SelectionAddTag(tag) => {
+ if let Context::SearchResult {
+ selected_threads, ..
+ } = &mut model.context
+ {
+ let threads = selected_threads
+ .iter()
+ .map(|tid| format!("thread:{tid}"))
+ .collect::>()
+ .join(" ");
+ orders
+ .skip()
+ .perform_cmd(async move { Msg::AddTag(threads, tag) });
+ }
+ }
+ Msg::SelectionRemoveTag(tag) => {
+ if let Context::SearchResult {
+ selected_threads, ..
+ } = &mut model.context
+ {
+ let threads = selected_threads
+ .iter()
+ .map(|tid| format!("thread:{tid}"))
+ .collect::>()
+ .join(" ");
+ orders
+ .skip()
+ .perform_cmd(async move { Msg::RemoveTag(threads, tag) });
+ }
+ }
Msg::SelectionMarkAsRead => {
if let Context::SearchResult {
selected_threads, ..
@@ -435,6 +511,8 @@ pub enum Msg {
SearchQuery(String),
SetUnread(String, bool),
+ AddTag(String, String),
+ RemoveTag(String, String),
FrontPageRequest {
query: String,
@@ -455,6 +533,8 @@ pub enum Msg {
SelectionSetNone,
SelectionSetAll,
+ SelectionAddTag(String),
+ SelectionRemoveTag(String),
SelectionMarkAsRead,
SelectionMarkAsUnread,
SelectionAddThread(String),
diff --git a/web/src/view/mod.rs b/web/src/view/mod.rs
index cbea814..70a8a80 100644
--- a/web/src/view/mod.rs
+++ b/web/src/view/mod.rs
@@ -261,6 +261,13 @@ fn search_toolbar(
span![
// TODO(wathiede): add "Mark as spam"
C!["level-item", "buttons", "has-addons"],
+ button![
+ C!["button"],
+ attrs!{At::Title => "Mark as spam"},
+ span![C!["icon", "is-small"], i![C!["far", "fa-hand"]]],
+ span!["Spam"],
+ ev(Ev::Click, |_| Msg::SelectionAddTag("Spam".to_string()))
+ ],
button![
C!["button"],
attrs!{At::Title => "Mark as read"},
@@ -721,13 +728,23 @@ fn thread(thread: &ShowThreadQueryThread, open_messages: &HashSet) -> No
});
let read_thread_id = thread.thread_id.clone();
let unread_thread_id = thread.thread_id.clone();
+ let spam_thread_id = thread.thread_id.clone();
div![
C!["thread"],
h3![C!["is-size-5"], subject],
span![C!["tags"], tags_chiclet(&tags, false)],
span![
- // TODO(wathiede): add "Mark as spam"
C!["level-item", "buttons", "has-addons"],
+ button![
+ C!["button"],
+ attrs! {At::Title => "Spam"},
+ span![C!["icon", "is-small"], i![C!["far", "fa-hand"]]],
+ span!["Spam"],
+ ev(Ev::Click, move |_| Msg::AddTag(
+ format!("thread:{spam_thread_id}"),
+ "Spam".to_string()
+ )),
+ ],
button![
C!["button"],
attrs! {At::Title => "Mark as read"},