Compare commits
4 Commits
9a5dc20f83
...
8abf9398e9
| Author | SHA1 | Date | |
|---|---|---|---|
| 8abf9398e9 | |||
| 1b196a2703 | |||
| a24f456136 | |||
| d8fef54606 |
@ -3,7 +3,6 @@ use std::{
|
|||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
fs::File,
|
fs::File,
|
||||||
hash::{DefaultHasher, Hash, Hasher},
|
hash::{DefaultHasher, Hash, Hasher},
|
||||||
str::FromStr,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use async_graphql::{
|
use async_graphql::{
|
||||||
@ -427,6 +426,28 @@ impl Mutation {
|
|||||||
}
|
}
|
||||||
Ok(true)
|
Ok(true)
|
||||||
}
|
}
|
||||||
|
async fn tag_add<'ctx>(
|
||||||
|
&self,
|
||||||
|
ctx: &Context<'ctx>,
|
||||||
|
query: String,
|
||||||
|
tag: String,
|
||||||
|
) -> Result<bool, Error> {
|
||||||
|
let nm = ctx.data_unchecked::<Notmuch>();
|
||||||
|
info!("tag_add({tag}, {query})");
|
||||||
|
nm.tag_add(&tag, &query)?;
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
async fn tag_remove<'ctx>(
|
||||||
|
&self,
|
||||||
|
ctx: &Context<'ctx>,
|
||||||
|
query: String,
|
||||||
|
tag: String,
|
||||||
|
) -> Result<bool, Error> {
|
||||||
|
let nm = ctx.data_unchecked::<Notmuch>();
|
||||||
|
info!("tag_remove({tag}, {query})");
|
||||||
|
nm.tag_remove(&tag, &query)?;
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type GraphqlSchema = Schema<QueryRoot, Mutation, EmptySubscription>;
|
pub type GraphqlSchema = Schema<QueryRoot, Mutation, EmptySubscription>;
|
||||||
|
|||||||
@ -83,6 +83,8 @@ pub fn sanitize_html(html: &str) -> Result<String, SanitizeError> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
// Default's don't allow style, but we want to preserve that.
|
// Default's don't allow style, but we want to preserve that.
|
||||||
|
// TODO: remove 'class' if rendering mails moves to a two phase process where abstract message
|
||||||
|
// types are collected, santized, and then grouped together as one big HTML doc
|
||||||
let attributes = hashset![
|
let attributes = hashset![
|
||||||
"align", "bgcolor", "class", "color", "height", "lang", "title", "width", "style",
|
"align", "bgcolor", "class", "color", "height", "lang", "title", "width", "style",
|
||||||
];
|
];
|
||||||
|
|||||||
3
web/graphql/add_tag.graphql
Normal file
3
web/graphql/add_tag.graphql
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
mutation AddTagMutation($query: String!, $tag: String!) {
|
||||||
|
tagAdd(query:$query, tag:$tag)
|
||||||
|
}
|
||||||
3
web/graphql/remove_tag.graphql
Normal file
3
web/graphql/remove_tag.graphql
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
mutation RemoveTagMutation($query: String!, $tag: String!) {
|
||||||
|
tagRemove(query:$query, tag:$tag)
|
||||||
|
}
|
||||||
@ -65,6 +65,38 @@
|
|||||||
"description": null,
|
"description": null,
|
||||||
"enumValues": null,
|
"enumValues": null,
|
||||||
"fields": [
|
"fields": [
|
||||||
|
{
|
||||||
|
"args": [],
|
||||||
|
"deprecationReason": null,
|
||||||
|
"description": null,
|
||||||
|
"isDeprecated": false,
|
||||||
|
"name": "id",
|
||||||
|
"type": {
|
||||||
|
"kind": "NON_NULL",
|
||||||
|
"name": null,
|
||||||
|
"ofType": {
|
||||||
|
"kind": "SCALAR",
|
||||||
|
"name": "String",
|
||||||
|
"ofType": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"args": [],
|
||||||
|
"deprecationReason": null,
|
||||||
|
"description": null,
|
||||||
|
"isDeprecated": false,
|
||||||
|
"name": "idx",
|
||||||
|
"type": {
|
||||||
|
"kind": "NON_NULL",
|
||||||
|
"name": null,
|
||||||
|
"ofType": {
|
||||||
|
"kind": "SCALAR",
|
||||||
|
"name": "String",
|
||||||
|
"ofType": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"args": [],
|
"args": [],
|
||||||
"deprecationReason": null,
|
"deprecationReason": null,
|
||||||
@ -84,9 +116,13 @@
|
|||||||
"isDeprecated": false,
|
"isDeprecated": false,
|
||||||
"name": "size",
|
"name": "size",
|
||||||
"type": {
|
"type": {
|
||||||
"kind": "SCALAR",
|
"kind": "NON_NULL",
|
||||||
"name": "Int",
|
"name": null,
|
||||||
"ofType": null
|
"ofType": {
|
||||||
|
"kind": "SCALAR",
|
||||||
|
"name": "Int",
|
||||||
|
"ofType": null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -657,6 +693,96 @@
|
|||||||
"ofType": null
|
"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,
|
"inputFields": null,
|
||||||
|
|||||||
@ -35,9 +35,12 @@ query ShowThreadQuery($threadId: String!) {
|
|||||||
}
|
}
|
||||||
path
|
path
|
||||||
attachments {
|
attachments {
|
||||||
|
id
|
||||||
|
idx
|
||||||
filename
|
filename
|
||||||
contentType
|
contentType
|
||||||
contentId
|
contentId
|
||||||
|
size
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
288
web/index.html
288
web/index.html
@ -4,296 +4,24 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||||
<link rel="stylesheet" , href="https://jenil.github.io/bulmaswatch/cyborg/bulmaswatch.min.css">
|
<link rel="stylesheet" href="https://jenil.github.io/bulmaswatch/cyborg/bulmaswatch.min.css">
|
||||||
|
<!--
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css">
|
||||||
|
-->
|
||||||
|
<!--
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@1.0.0/css/bulma.min.css">
|
||||||
|
-->
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.3.0/css/all.min.css"
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.3.0/css/all.min.css"
|
||||||
integrity="sha512-SzlrxWUlpfuzQ+pcUCosxcglQRNAq/DZjVsC0lE40xsADsfeQoEypE+enwcOiGjk/bSuGGKHEyjSoQ1zVisanQ=="
|
integrity="sha512-SzlrxWUlpfuzQ+pcUCosxcglQRNAq/DZjVsC0lE40xsADsfeQoEypE+enwcOiGjk/bSuGGKHEyjSoQ1zVisanQ=="
|
||||||
crossorigin="anonymous" referrerpolicy="no-referrer" />
|
crossorigin="anonymous" referrerpolicy="no-referrer" />
|
||||||
<link rel="icon" href="https://static.xinu.tv/favicon/letterbox.svg" />
|
<link rel="icon" href="https://static.xinu.tv/favicon/letterbox.svg" />
|
||||||
|
<link data-trunk rel="css" href="static/style.css" />
|
||||||
<!-- Pretty checkboxes from https://justboil.github.io/bulma-checkbox/ -->
|
<!-- Pretty checkboxes from https://justboil.github.io/bulma-checkbox/ -->
|
||||||
<link data-trunk rel="css" href="static/main.css" />
|
<link data-trunk rel="css" href="static/main.css" />
|
||||||
<!-- tall thin font for user icon -->
|
<!-- tall thin font for user icon -->
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@700&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@700&display=swap" rel="stylesheet">
|
||||||
<style>
|
|
||||||
<style>.message {
|
|
||||||
display: inline-block;
|
|
||||||
padding: 0.5em;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message .header table td {
|
|
||||||
border: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message .header .media-right {
|
|
||||||
padding: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message .headers {
|
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message .headers .read-status {
|
|
||||||
position: absolute;
|
|
||||||
right: 1em;
|
|
||||||
top: 0em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message .headers .header {
|
|
||||||
overflow: clip;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.body {
|
|
||||||
background: white;
|
|
||||||
color: black;
|
|
||||||
margin-top: 0.5em;
|
|
||||||
padding: 1em;
|
|
||||||
width: 0;
|
|
||||||
min-width: 100%;
|
|
||||||
overflow: auto;
|
|
||||||
overflow-wrap: break-word;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message .body .attachments hr {
|
|
||||||
border: none;
|
|
||||||
border-top: 1px dashed #888;
|
|
||||||
background-color: #f000;
|
|
||||||
margin: 0.5rem 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error {
|
|
||||||
background-color: red;
|
|
||||||
}
|
|
||||||
|
|
||||||
.view-part-text-plain {
|
|
||||||
padding: 0.5em;
|
|
||||||
overflow-wrap: break-word;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
word-break: break-word;
|
|
||||||
word-wrap: break-word;
|
|
||||||
}
|
|
||||||
|
|
||||||
iframe {
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.index {
|
|
||||||
table-layout: fixed;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.index .edit {
|
|
||||||
width: 2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.index .unread {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.index .from {
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
width: 10em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.index .subject {
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.index .date {
|
|
||||||
width: 7em;
|
|
||||||
white-space: nowrap;
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lb-footer {
|
|
||||||
background-color: #eee;
|
|
||||||
color: #222;
|
|
||||||
position: fixed;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
height: 3em;
|
|
||||||
padding: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tag {
|
|
||||||
margin-right: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.debug ul {
|
|
||||||
padding-left: 2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.debug li {}
|
|
||||||
|
|
||||||
.loading {
|
|
||||||
animation-name: spin;
|
|
||||||
animation-duration: 1000ms;
|
|
||||||
animation-iteration-count: infinite;
|
|
||||||
animation-timing-function: linear;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin {
|
|
||||||
from {
|
|
||||||
transform: rotate(0deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
to {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.section {
|
|
||||||
padding: 1.5em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
input,
|
|
||||||
.input {
|
|
||||||
color: #000;
|
|
||||||
}
|
|
||||||
|
|
||||||
input::placeholder,
|
|
||||||
.input::placeholder {
|
|
||||||
color: #555;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile .search-results {
|
|
||||||
padding: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile .thread h3 {
|
|
||||||
overflow-wrap: break-word;
|
|
||||||
padding: 1em 1em 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile .thread .tags {
|
|
||||||
padding: 0 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-results .row {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
flex-wrap: nowrap;
|
|
||||||
|
|
||||||
border-bottom: 1px #444 solid;
|
|
||||||
padding-bottom: .5em;
|
|
||||||
padding-top: .5em;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-results .row .checkbox {}
|
|
||||||
|
|
||||||
.search-results .row .summary {
|
|
||||||
min-width: 0;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-results .row .subject {
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-results td.subject {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-results .subject .tag {}
|
|
||||||
|
|
||||||
.search-results .subject .text {
|
|
||||||
padding-left: 0.5rem;
|
|
||||||
width: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-results .row .from {
|
|
||||||
display: inline-block;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-results .from a {
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-results .from a {
|
|
||||||
display: inline-block;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-results .row .tag {
|
|
||||||
height: 1.5em;
|
|
||||||
padding-left: .5em;
|
|
||||||
padding-right: .5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.float-right {
|
|
||||||
float: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Hide quoted emails */
|
|
||||||
/*
|
|
||||||
div[name="quote"],
|
|
||||||
blockquote[type="cite"],
|
|
||||||
.gmail_quote {
|
|
||||||
background-color: red;
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
.desktop .main-content {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 12rem 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tags-menu {
|
|
||||||
padding: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tags-menu .menu-list a {
|
|
||||||
padding: 0.25em 0.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tags-menu .tag-indent {
|
|
||||||
padding-left: .5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tags-menu .tag-tag {
|
|
||||||
margin-left: -1em;
|
|
||||||
padding-right: .25em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar {
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.desktop nav.pagination,
|
|
||||||
.tablet nav.pagination {
|
|
||||||
margin-left: .5em;
|
|
||||||
margin-bottom: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content-tree {
|
|
||||||
white-space: pre-wrap;
|
|
||||||
word-break: break-word;
|
|
||||||
word-wrap: break-word;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@ -28,6 +28,22 @@ pub struct ShowThreadQuery;
|
|||||||
)]
|
)]
|
||||||
pub struct MarkReadMutation;
|
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, Resp>(body: Body) -> Result<graphql_client::Response<Resp>, Error>
|
pub async fn send_graphql<Body, Resp>(body: Body) -> Result<graphql_client::Response<Resp>, Error>
|
||||||
where
|
where
|
||||||
Body: Serialize,
|
Body: Serialize,
|
||||||
|
|||||||
@ -184,6 +184,52 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
|||||||
Msg::Noop
|
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<graphql::add_tag_mutation::ResponseData>,
|
||||||
|
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<graphql::remove_tag_mutation::ResponseData>,
|
||||||
|
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 {
|
Msg::FrontPageRequest {
|
||||||
query,
|
query,
|
||||||
@ -311,6 +357,36 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
|||||||
*selected_threads = results.iter().map(|node| node.thread.clone()).collect();
|
*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::<Vec<_>>()
|
||||||
|
.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::<Vec<_>>()
|
||||||
|
.join(" ");
|
||||||
|
orders
|
||||||
|
.skip()
|
||||||
|
.perform_cmd(async move { Msg::RemoveTag(threads, tag) });
|
||||||
|
}
|
||||||
|
}
|
||||||
Msg::SelectionMarkAsRead => {
|
Msg::SelectionMarkAsRead => {
|
||||||
if let Context::SearchResult {
|
if let Context::SearchResult {
|
||||||
selected_threads, ..
|
selected_threads, ..
|
||||||
@ -435,6 +511,8 @@ pub enum Msg {
|
|||||||
SearchQuery(String),
|
SearchQuery(String),
|
||||||
|
|
||||||
SetUnread(String, bool),
|
SetUnread(String, bool),
|
||||||
|
AddTag(String, String),
|
||||||
|
RemoveTag(String, String),
|
||||||
|
|
||||||
FrontPageRequest {
|
FrontPageRequest {
|
||||||
query: String,
|
query: String,
|
||||||
@ -455,6 +533,8 @@ pub enum Msg {
|
|||||||
|
|
||||||
SelectionSetNone,
|
SelectionSetNone,
|
||||||
SelectionSetAll,
|
SelectionSetAll,
|
||||||
|
SelectionAddTag(String),
|
||||||
|
SelectionRemoveTag(String),
|
||||||
SelectionMarkAsRead,
|
SelectionMarkAsRead,
|
||||||
SelectionMarkAsUnread,
|
SelectionMarkAsUnread,
|
||||||
SelectionAddThread(String),
|
SelectionAddThread(String),
|
||||||
|
|||||||
@ -261,6 +261,13 @@ fn search_toolbar(
|
|||||||
span![
|
span![
|
||||||
// TODO(wathiede): add "Mark as spam"
|
// TODO(wathiede): add "Mark as spam"
|
||||||
C!["level-item", "buttons", "has-addons"],
|
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![
|
button![
|
||||||
C!["button"],
|
C!["button"],
|
||||||
attrs!{At::Title => "Mark as read"},
|
attrs!{At::Title => "Mark as read"},
|
||||||
@ -642,14 +649,53 @@ fn message_render(msg: &ShowThreadQueryThreadMessages, open: bool) -> Node<Msg>
|
|||||||
C!["view-part-text-html"],
|
C!["view-part-text-html"],
|
||||||
raw![contents],
|
raw![contents],
|
||||||
IF!(!msg.attachments.is_empty() =>
|
IF!(!msg.attachments.is_empty() =>
|
||||||
div![
|
div![
|
||||||
C!["attachments"],
|
C!["attachments"],
|
||||||
hr![],
|
hr![],
|
||||||
h2!["Attachments"],
|
h2!["Attachments"],
|
||||||
|
div![C!["grid","is-col-min-6"],
|
||||||
msg.attachments
|
msg.attachments
|
||||||
.iter()
|
.iter()
|
||||||
.map(|a| div!["Filename: ", &a.filename, " ", &a.content_type])
|
.map(|a| {
|
||||||
]),
|
let default = "UNKNOWN_FILE".to_string();
|
||||||
|
let filename = a.filename.as_ref().unwrap_or(&default);
|
||||||
|
let host = seed::window().location().host().expect("couldn't get host");
|
||||||
|
let url = format!("//{host}/download/attachment/{}/{}/{}", a.id,a.idx, filename);
|
||||||
|
|
||||||
|
div![
|
||||||
|
C!["attachment", "card"],
|
||||||
|
a.content_type.as_ref().map(|content_type|
|
||||||
|
IF!(content_type.starts_with("image/") =>
|
||||||
|
div![C!["card-image","is-1by1"],
|
||||||
|
div![
|
||||||
|
C!["image","is-1by1"],
|
||||||
|
style!{
|
||||||
|
St::BackgroundImage=>format!(r#"url("{url}");"#),
|
||||||
|
St::BackgroundSize=>"cover",
|
||||||
|
St::BackgroundPosition=>"center",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
)),
|
||||||
|
div![C!["card-content"],
|
||||||
|
div![C!["content"],
|
||||||
|
&a.filename, br![],
|
||||||
|
small![&a.size, " bytes"]
|
||||||
|
]
|
||||||
|
],
|
||||||
|
footer![
|
||||||
|
C!["card-footer"],
|
||||||
|
a![C!["card-footer-item"],span![C!["icon"], i![C!["fas", "fa-download"]]],
|
||||||
|
ev(Ev::Click, move |_| {
|
||||||
|
seed::window().location().set_href(&url
|
||||||
|
).expect("failed to set URL");
|
||||||
|
})
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
})
|
||||||
|
]
|
||||||
|
]),
|
||||||
view_content_tree(&content_tree),
|
view_content_tree(&content_tree),
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
@ -682,13 +728,23 @@ fn thread(thread: &ShowThreadQueryThread, open_messages: &HashSet<String>) -> No
|
|||||||
});
|
});
|
||||||
let read_thread_id = thread.thread_id.clone();
|
let read_thread_id = thread.thread_id.clone();
|
||||||
let unread_thread_id = thread.thread_id.clone();
|
let unread_thread_id = thread.thread_id.clone();
|
||||||
|
let spam_thread_id = thread.thread_id.clone();
|
||||||
div![
|
div![
|
||||||
C!["thread"],
|
C!["thread"],
|
||||||
h3![C!["is-size-5"], subject],
|
h3![C!["is-size-5"], subject],
|
||||||
span![C!["tags"], tags_chiclet(&tags, false)],
|
span![C!["tags"], tags_chiclet(&tags, false)],
|
||||||
span![
|
span![
|
||||||
// TODO(wathiede): add "Mark as spam"
|
|
||||||
C!["level-item", "buttons", "has-addons"],
|
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![
|
button![
|
||||||
C!["button"],
|
C!["button"],
|
||||||
attrs! {At::Title => "Mark as read"},
|
attrs! {At::Title => "Mark as read"},
|
||||||
|
|||||||
284
web/static/style.css
Normal file
284
web/static/style.css
Normal file
@ -0,0 +1,284 @@
|
|||||||
|
.message {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.5em;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message .header table td {
|
||||||
|
border: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message .header .media-right {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message .headers {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message .headers .read-status {
|
||||||
|
position: absolute;
|
||||||
|
right: 1em;
|
||||||
|
top: 0em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message .headers .header {
|
||||||
|
overflow: clip;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body {
|
||||||
|
background: white;
|
||||||
|
color: black;
|
||||||
|
margin-top: 0.5em;
|
||||||
|
padding: 1em;
|
||||||
|
width: 0;
|
||||||
|
min-width: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message .body .attachments hr {
|
||||||
|
border: none;
|
||||||
|
border-top: 1px dashed #888;
|
||||||
|
background-color: #f000;
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
background-color: red;
|
||||||
|
}
|
||||||
|
|
||||||
|
.view-part-text-plain {
|
||||||
|
padding: 0.5em;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
iframe {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.index {
|
||||||
|
table-layout: fixed;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.index .edit {
|
||||||
|
width: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.index .unread {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.index .from {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
width: 10em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.index .subject {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.index .date {
|
||||||
|
width: 7em;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lb-footer {
|
||||||
|
background-color: #eee;
|
||||||
|
color: #222;
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 3em;
|
||||||
|
padding: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag {
|
||||||
|
margin-right: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.debug ul {
|
||||||
|
padding-left: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.debug li {}
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
animation-name: spin;
|
||||||
|
animation-duration: 1000ms;
|
||||||
|
animation-iteration-count: infinite;
|
||||||
|
animation-timing-function: linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
from {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.section {
|
||||||
|
padding: 1.5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
input,
|
||||||
|
.input {
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
input::placeholder,
|
||||||
|
.input::placeholder {
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile .search-results {
|
||||||
|
padding: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile .thread h3 {
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
padding: 1em 1em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile .thread .tags {
|
||||||
|
padding: 0 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-results .row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
|
||||||
|
border-bottom: 1px #444 solid;
|
||||||
|
padding-bottom: .5em;
|
||||||
|
padding-top: .5em;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-results .row .checkbox {}
|
||||||
|
|
||||||
|
.search-results .row .summary {
|
||||||
|
min-width: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-results .row .subject {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-results td.subject {
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-results .subject .tag {}
|
||||||
|
|
||||||
|
.search-results .subject .text {
|
||||||
|
padding-left: 0.5rem;
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-results .row .from {
|
||||||
|
display: inline-block;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-results .from a {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-results .from a {
|
||||||
|
display: inline-block;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-results .row .tag {
|
||||||
|
height: 1.5em;
|
||||||
|
padding-left: .5em;
|
||||||
|
padding-right: .5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.float-right {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide quoted emails */
|
||||||
|
/*
|
||||||
|
div[name="quote"],
|
||||||
|
blockquote[type="cite"],
|
||||||
|
.gmail_quote {
|
||||||
|
background-color: red;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
.desktop .main-content {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 12rem 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tags-menu {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tags-menu .menu-list a {
|
||||||
|
padding: 0.25em 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tags-menu .tag-indent {
|
||||||
|
padding-left: .5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tags-menu .tag-tag {
|
||||||
|
margin-left: -1em;
|
||||||
|
padding-right: .25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.desktop nav.pagination,
|
||||||
|
.tablet nav.pagination {
|
||||||
|
margin-left: .5em;
|
||||||
|
margin-bottom: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-tree {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attachment {
|
||||||
|
margin: .25em;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
.attachment .card-content {
|
||||||
|
padding: 0.5rem 1.5rem;
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user