add: mail ui
This commit is contained in:
parent
2e60cbc464
commit
b2a73d286b
24 changed files with 993 additions and 259 deletions
|
@ -113,6 +113,12 @@
|
|||
("style" "color: var(--color-primary)")
|
||||
("class" "flex items-center")
|
||||
(text "{{ icon \"badge-check\" }}"))
|
||||
(text "{%- endif %} {% if user.permissions|has_supporter -%}")
|
||||
(span
|
||||
("title" "Supporter")
|
||||
("style" "color: var(--color-primary);")
|
||||
("class" "flex items-center")
|
||||
(text "{{ icon \"star\" }}"))
|
||||
(text "{%- endif %} {% if user.permissions|has_staff_badge -%}")
|
||||
(span
|
||||
("title" "Staff")
|
||||
|
@ -456,21 +462,25 @@
|
|||
(div
|
||||
("class" "w-full card-nest")
|
||||
(div
|
||||
("class" "card small notif_title flex items-center")
|
||||
(text "{% if not notification.read -%}")
|
||||
(svg
|
||||
("width" "24")
|
||||
("height" "24")
|
||||
("viewBox" "0 0 24 24")
|
||||
("style" "fill: var(--color-link)")
|
||||
(circle
|
||||
("cx" "12")
|
||||
("cy" "12")
|
||||
("r" "6")))
|
||||
(text "{%- endif %}")
|
||||
(b
|
||||
("class" "no_p_margin")
|
||||
(text "{{ notification.title|markdown|safe }}")))
|
||||
("class" "card small notif_title flex gap-2 justify-between items-center")
|
||||
(div
|
||||
("class" "flex items-center")
|
||||
(text "{% if not notification.read -%}")
|
||||
(svg
|
||||
("width" "24")
|
||||
("height" "24")
|
||||
("viewBox" "0 0 24 24")
|
||||
("style" "fill: var(--color-link)")
|
||||
(circle
|
||||
("cx" "12")
|
||||
("cy" "12")
|
||||
("r" "6")))
|
||||
(text "{%- endif %}")
|
||||
(b
|
||||
("class" "no_p_margin")
|
||||
(text "{{ notification.title|markdown|safe }}")))
|
||||
|
||||
(span ("class" "date") (text "{{ notification.created }}")))
|
||||
(div
|
||||
("class" "card notif_content flex flex-col gap-2")
|
||||
(span
|
||||
|
@ -2451,3 +2461,85 @@
|
|||
(span
|
||||
(str (text "dialog:action.continue"))))))
|
||||
(text "{%- endif %} {%- endmacro %}")
|
||||
|
||||
(text "{% macro letter_listing(letter, owner) -%}")
|
||||
(div
|
||||
("class" "card lowered flex gap-2 flex-row")
|
||||
(a
|
||||
("href" "/@{{ owner.username }}")
|
||||
(text "{{ self::avatar(username=owner.username, size=\"32px\") }}"))
|
||||
(div
|
||||
("class" "flex flex-col")
|
||||
(text "{{ self::full_username(user=owner) }}")
|
||||
(div
|
||||
("class" "flex items-center gap-2")
|
||||
; read status
|
||||
(text "{% if user.id in letter.read_by -%}")
|
||||
(div ("class" "flex items-center green") (icon (text "mail-check")))
|
||||
(text "{% else %}")
|
||||
(div ("class" "flex items-center") (icon (text "mail")))
|
||||
(text "{%- endif %}")
|
||||
|
||||
; subject
|
||||
(a ("class" "flush") ("href" "/mail/letter/{{ letter.id }}") (b (text "{{ letter.subject }}"))))))
|
||||
(text "{%- endmacro %}")
|
||||
|
||||
(text "{% macro letter(letter, owner, show_subject=true) -%}")
|
||||
(div
|
||||
("class" "card-nest")
|
||||
(text "{% if show_subject -%}")
|
||||
(div
|
||||
("class" "card flex gap-2 flex-row")
|
||||
(a
|
||||
("href" "/@{{ owner.username }}")
|
||||
(text "{{ self::avatar(username=owner.username, size=\"32px\") }}"))
|
||||
(div
|
||||
("class" "flex flex-col")
|
||||
(text "{{ self::full_username(user=owner) }}")
|
||||
(span
|
||||
(b (text "{{ letter.subject }}"))
|
||||
(text "{% if letter.replying_to -%}")
|
||||
(a
|
||||
("href" "/mail/letter/{{ letter.replying_to }}")
|
||||
(text " (up)"))
|
||||
(text "{%- endif %}"))
|
||||
(div
|
||||
("class" "flex flex-wrap gap-2")
|
||||
(text "{% for receiver in letter.receivers %}")
|
||||
(a
|
||||
("href" "/api/v1/auth/user/find/{{ receiver }}")
|
||||
(text "{{ components::avatar(username=receiver, selector_type=\"id\", size=\"18px\") }}"))
|
||||
(text "{%- endfor %}"))))
|
||||
(text "{% else %}")
|
||||
(div
|
||||
("class" "card small flex gap-2 flex-row")
|
||||
(a
|
||||
("href" "/@{{ owner.username }}")
|
||||
(text "{{ self::avatar(username=owner.username, size=\"24px\") }}"))
|
||||
(text "{{ self::full_username(user=owner) }}"))
|
||||
(text "{%- endif %}")
|
||||
|
||||
(div
|
||||
("class" "card flex flex-col gap-2")
|
||||
(text "{{ letter.content|markdown|safe }}")
|
||||
(hr)
|
||||
(div
|
||||
("class" "flex gap-2 items-center")
|
||||
(a
|
||||
("class" "button small lowered")
|
||||
("href" "/mail/compose?receivers={{ owner.username }}&subject=Re%3A%20{{ letter.subject }}&replying_to={{ letter.id }}")
|
||||
("title" "Reply")
|
||||
(icon (text "reply")))
|
||||
(a
|
||||
("class" "button small lowered")
|
||||
("href" "/mail/compose?receivers={% for receiver in letter.receivers %},id%3A{{ receiver }}{% endfor %}&subject=Re%3A%20{{ letter.subject }}&replying_to={{ letter.id }}")
|
||||
("title" "Reply all")
|
||||
(icon (text "reply-all")))
|
||||
(text "{% if user and letter.owner == user.id -%}")
|
||||
(button
|
||||
("class" "small lowered red")
|
||||
("onclick" "delete_letter('{{ letter.id }}')")
|
||||
("title" "Delete")
|
||||
(icon (text "trash")))
|
||||
(text "{%- endif %}"))))
|
||||
(text "{%- endmacro %}")
|
||||
|
|
|
@ -76,6 +76,11 @@
|
|||
("title" "Chats")
|
||||
(icon (text "message-circle"))
|
||||
(str (text "communities:label.chats")))
|
||||
(a
|
||||
("href" "/mail")
|
||||
("title" "Mail")
|
||||
(icon (text "mail"))
|
||||
(str (text "general:link.mail")))
|
||||
(a
|
||||
("href" "/journals/0/0")
|
||||
(icon (text "notebook"))
|
||||
|
|
139
crates/app/src/public/html/mail/compose.lisp
Normal file
139
crates/app/src/public/html/mail/compose.lisp
Normal file
|
@ -0,0 +1,139 @@
|
|||
(text "{% extends \"root.html\" %} {% block head %}")
|
||||
(title
|
||||
(text "Compose letter - {{ config.name }}"))
|
||||
|
||||
(text "{% endblock %} {% block body %} {{ macros::nav() }}")
|
||||
(main
|
||||
(div
|
||||
("class" "card-nest")
|
||||
(div
|
||||
("class" "card small items-center gap-2 flex justify-between")
|
||||
(div
|
||||
("class" "flex gap-2 items-center")
|
||||
(icon (text "mail-plus"))
|
||||
(str (text "mail:label.compose")))
|
||||
|
||||
(button
|
||||
("onclick" "window.history.back()")
|
||||
("class" "lowered small")
|
||||
(icon (text "arrow-left"))
|
||||
(str (text "general:action.back"))))
|
||||
|
||||
(form
|
||||
("class" "card flex flex-col gap-2")
|
||||
("onsubmit" "create_letter_from_form(event)")
|
||||
(div
|
||||
("class" "flex flex-col gap-1")
|
||||
(span
|
||||
(b (str (text "mail:label.receivers"))))
|
||||
(div
|
||||
("class" "flex flex-wrap gap-2 small card lowered")
|
||||
(div ("id" "receivers") ("class" "flex flex-wrap gap-2"))
|
||||
(button
|
||||
("class" "small tiny big_icon square raised")
|
||||
("onclick" "add_receiver()")
|
||||
("type" "button")
|
||||
(icon (text "plus")))))
|
||||
|
||||
(div
|
||||
("class" "flex flex-col gap-1")
|
||||
(label
|
||||
("for" "subject")
|
||||
(b (str (text "mail:label.subject"))))
|
||||
(input
|
||||
("type" "text")
|
||||
("placeholder" "subject")
|
||||
("required" "")
|
||||
("name" "subject")
|
||||
("id" "subject")))
|
||||
|
||||
(div
|
||||
("class" "flex flex-col gap-1")
|
||||
(label
|
||||
("for" "content")
|
||||
(b (str (text "mail:label.content"))))
|
||||
(textarea
|
||||
("placeholder" "content")
|
||||
("required" "")
|
||||
("name" "content")
|
||||
("id" "content")))
|
||||
|
||||
(button
|
||||
(icon (text "send-horizontal"))
|
||||
(str (text "mail:action.send"))))))
|
||||
|
||||
(script
|
||||
(text "globalThis.RECEIVERS = [];
|
||||
globalThis.SEARCH_PARAMS = new URLSearchParams(window.location.search);
|
||||
|
||||
globalThis.add_receiver = async () => {
|
||||
const username = await trigger(\"atto::prompt\", [\"Username:\"]);
|
||||
if (!username) {
|
||||
return;
|
||||
}
|
||||
|
||||
RECEIVERS.push(username);
|
||||
render_receivers();
|
||||
}
|
||||
|
||||
globalThis.remove_receiver = (username) => {
|
||||
RECEIVERS.splice(RECEIVERS.indexOf(username), 1);
|
||||
render_receivers();
|
||||
}
|
||||
|
||||
globalThis.render_receivers = () => {
|
||||
const element = document.getElementById(\"receivers\");
|
||||
element.innerHTML = \"\";
|
||||
|
||||
for (let receiver of RECEIVERS) {
|
||||
const is_id = receiver.startsWith(\"id:\");
|
||||
receiver = receiver.replaceAll(\"<\", \"<\").replaceAll(\">\", \">\").replace(\"id:\", \"\");
|
||||
element.innerHTML += `<button class=\"small lowered\" onclick=\"remove_receiver('${receiver}')\" type=\"button\">
|
||||
<img class=\"avatar\" style=\"--size: 18px\" src=\"/api/v1/auth/user/${receiver}/avatar?selector_type=${is_id ? \"id\" : \"username\"}\" />
|
||||
<span>${is_id ? \"...\" : receiver}</span>
|
||||
</button>`;
|
||||
}
|
||||
}
|
||||
|
||||
globalThis.create_letter_from_form = async (e) => {
|
||||
e.preventDefault();
|
||||
await trigger(\"atto::debounce\", [\"letters::create\"]);
|
||||
|
||||
fetch(\"/api/v1/letters\", {
|
||||
method: \"POST\",
|
||||
headers: {
|
||||
\"Content-Type\": \"application/json\",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
content: e.target.content.value.trim(),
|
||||
subject: e.target.subject.value.trim(),
|
||||
receivers: RECEIVERS,
|
||||
replying_to: SEARCH_PARAMS.get(\"replying_to\") || \"0\",
|
||||
}),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((res) => {
|
||||
if (!res.ok) {
|
||||
trigger(\"atto::toast\", [\"error\", res.message]);
|
||||
} else {
|
||||
e.target.reset();
|
||||
window.location.href = `/mail/letter/${res.payload}`;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
if (SEARCH_PARAMS.get(\"receivers\")) {
|
||||
let r = SEARCH_PARAMS.get(\"receivers\");
|
||||
|
||||
if (r.startsWith(\",\")) {
|
||||
r = r.replace(\",\", \"\");
|
||||
}
|
||||
|
||||
RECEIVERS = r.split(\",\");
|
||||
render_receivers();
|
||||
}
|
||||
|
||||
if (SEARCH_PARAMS.get(\"subject\")) {
|
||||
document.getElementById(\"subject\").value = SEARCH_PARAMS.get(\"subject\");
|
||||
}"))
|
||||
(text "{% endblock %}")
|
49
crates/app/src/public/html/mail/letter.lisp
Normal file
49
crates/app/src/public/html/mail/letter.lisp
Normal file
|
@ -0,0 +1,49 @@
|
|||
(text "{% extends \"root.html\" %} {% block head %}")
|
||||
(title
|
||||
(text "Letter - {{ config.name }}"))
|
||||
(text "{% endblock %} {% block body %} {{ macros::nav() }}")
|
||||
(main
|
||||
("class" "flex flex-col gap-2")
|
||||
(text "{{ components::letter(letter=letter, owner=owner) }}")
|
||||
|
||||
(text "{% for letter in replies %}")
|
||||
(text "{{ components::letter(letter=letter[1], owner=letter[0], show_subject=false) }}")
|
||||
(text "{%- endfor %}")
|
||||
|
||||
(text "{{ components::pagination(page=page, items=replies|length) }}"))
|
||||
|
||||
(script
|
||||
(text "globalThis.delete_letter = async (id) => {
|
||||
if (
|
||||
!(await trigger(\"atto::confirm\", [
|
||||
\"Are you sure you would like to do this?\",
|
||||
]))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
fetch(`/api/v1/letters/${id}`, {
|
||||
method: \"DELETE\",
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((res) => {
|
||||
trigger(\"atto::toast\", [
|
||||
res.ok ? \"success\" : \"error\",
|
||||
res.message,
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
fetch(\"/api/v1/letters/{{ letter.id }}/read\", {
|
||||
method: \"POST\",
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((res) => {
|
||||
if (!res.ok) {
|
||||
trigger(\"atto::toast\", [
|
||||
res.ok ? \"success\" : \"error\",
|
||||
res.message,
|
||||
]);
|
||||
}
|
||||
});"))
|
||||
(text "{% endblock %}")
|
43
crates/app/src/public/html/mail/received.lisp
Normal file
43
crates/app/src/public/html/mail/received.lisp
Normal file
|
@ -0,0 +1,43 @@
|
|||
(text "{% extends \"root.html\" %} {% block head %}")
|
||||
(title
|
||||
(text "Received mail - {{ config.name }}"))
|
||||
(text "{% endblock %} {% block body %} {{ macros::nav() }}")
|
||||
(main
|
||||
("class" "flex flex-col gap-2")
|
||||
(div
|
||||
("class" "pillmenu")
|
||||
(a
|
||||
("href" "/mail")
|
||||
("class" "active")
|
||||
(str (text "mail:label.received")))
|
||||
(a
|
||||
("href" "/mail/sent")
|
||||
(str (text "mail:label.sent"))))
|
||||
|
||||
; letters
|
||||
(div
|
||||
("class" "card-nest")
|
||||
(div
|
||||
("class" "card small flex items-center justify-between gap-2")
|
||||
(div
|
||||
("class" "flex items-center gap-2")
|
||||
(icon (text "mailbox"))
|
||||
(str (text "mail:label.received")))
|
||||
(a
|
||||
("href" "/mail/compose")
|
||||
("class" "button small lowered")
|
||||
(icon (text "plus"))
|
||||
(str (text "mail:label.compose"))))
|
||||
(div
|
||||
("class" "card flex flex-col gap-2")
|
||||
(text "{% for letter in list %}")
|
||||
(text "{{ components::letter_listing(letter=letter[1], owner=letter[0]) }}")
|
||||
(text "{% endfor %}")
|
||||
|
||||
; pagination
|
||||
(text "{% if list|length == 0 -%}")
|
||||
(i ("class" "fade") (text "Nothing yet!"))
|
||||
(text "{% else %}")
|
||||
(text "{{ components::pagination(page=page, items=list|length) }}")
|
||||
(text "{%- endif %}"))))
|
||||
(text "{% endblock %}")
|
43
crates/app/src/public/html/mail/sent.lisp
Normal file
43
crates/app/src/public/html/mail/sent.lisp
Normal file
|
@ -0,0 +1,43 @@
|
|||
(text "{% extends \"root.html\" %} {% block head %}")
|
||||
(title
|
||||
(text "Sent mail - {{ config.name }}"))
|
||||
(text "{% endblock %} {% block body %} {{ macros::nav() }}")
|
||||
(main
|
||||
("class" "flex flex-col gap-2")
|
||||
(div
|
||||
("class" "pillmenu")
|
||||
(a
|
||||
("href" "/mail")
|
||||
(str (text "mail:label.received")))
|
||||
(a
|
||||
("href" "/mail/sent")
|
||||
("class" "active")
|
||||
(str (text "mail:label.sent"))))
|
||||
|
||||
; letters
|
||||
(div
|
||||
("class" "card-nest")
|
||||
(div
|
||||
("class" "card small flex items-center justify-between gap-2")
|
||||
(div
|
||||
("class" "flex items-center gap-2")
|
||||
(icon (text "mailbox"))
|
||||
(str (text "mail:label.sent")))
|
||||
(a
|
||||
("href" "/mail/compose")
|
||||
("class" "button small lowered")
|
||||
(icon (text "plus"))
|
||||
(str (text "mail:label.compose"))))
|
||||
(div
|
||||
("class" "card flex flex-col gap-2")
|
||||
(text "{% for letter in list %}")
|
||||
(text "{{ components::letter_listing(letter=letter[1], owner=letter[0]) }}")
|
||||
(text "{% endfor %}")
|
||||
|
||||
; pagination
|
||||
(text "{% if list|length == 0 -%}")
|
||||
(i ("class" "fade") (text "Nothing yet!"))
|
||||
(text "{% else %}")
|
||||
(text "{{ components::pagination(page=page, items=list|length) }}")
|
||||
(text "{%- endif %}"))))
|
||||
(text "{% endblock %}")
|
|
@ -7,7 +7,7 @@
|
|||
(div
|
||||
("class" "card-nest")
|
||||
(div
|
||||
("class" "card")
|
||||
("class" "card small")
|
||||
(b (text "Error 😦")))
|
||||
|
||||
(div
|
||||
|
|
|
@ -252,13 +252,20 @@
|
|||
(text "{{ icon \"shield-off\" }}")
|
||||
(span
|
||||
(text "{{ text \"auth:action.unblock\" }}")))
|
||||
(text "{%- endif %} {% if not user.settings.private_chats or is_following_you %}")
|
||||
(text "{%- endif %} {% if not profile.settings.private_chats or is_following_you %}")
|
||||
(button
|
||||
("onclick" "create_group_chat()")
|
||||
("class" "lowered")
|
||||
(text "{{ icon \"message-circle\" }}")
|
||||
(span
|
||||
(text "{{ text \"auth:action.message\" }}")))
|
||||
(text "{%- endif %} {% if not profile.settings.private_mails or is_following_you %}")
|
||||
(a
|
||||
("href" "/mail/compose?receivers={{ profile.username }}")
|
||||
("class" "button lowered")
|
||||
(icon (text "mail-plus"))
|
||||
(span
|
||||
(str (text "mail:action.send_mail"))))
|
||||
(text "{%- endif %} {% if is_helper -%}")
|
||||
(a
|
||||
("href" "/mod_panel/profile/{{ profile.id }}")
|
||||
|
|
|
@ -1700,6 +1700,7 @@
|
|||
[\"private_last_seen\", true],
|
||||
[\"private_communities\", true],
|
||||
[\"private_chats\", true],
|
||||
[\"private_mails\", true],
|
||||
[\"require_account\", true],
|
||||
];
|
||||
|
||||
|
@ -1830,6 +1831,14 @@
|
|||
\"{{ profile.settings.private_chats }}\",
|
||||
\"checkbox\",
|
||||
],
|
||||
[
|
||||
[
|
||||
\"private_mails\",
|
||||
\"Only allow users I'm following to add send me mail\",
|
||||
],
|
||||
\"{{ profile.settings.private_mails }}\",
|
||||
\"checkbox\",
|
||||
],
|
||||
[
|
||||
[
|
||||
\"private_communities\",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue