tetratto/crates/app/src/public/html/communities/settings.lisp

953 lines
35 KiB
Common Lisp

(text "{% extends \"root.html\" %} {% block head %}")
(title
(text "Community settings - {{ config.name }}"))
(text "{% endblock %} {% block body %} {{ macros::nav() }}")
(main
("class" "flex flex-col gap-2")
(div
("class" "pillmenu")
(a
("href" "#/general")
("data-tab-button" "general")
("class" "active")
(text "{{ icon \"settings\" }}")
(span
(text "{{ text \"settings:tab.general\" }}")))
(a
("href" "#/images")
("data-tab-button" "images")
(text "{{ icon \"image\" }}")
(span
(text "{{ text \"settings:tab.images\" }}")))
(a
("href" "#/members")
("data-tab-button" "members")
(text "{{ icon \"users-round\" }}")
(span
(text "{{ text \"communities:tab.members\" }}")))
(text "{% if can_manage_channels -%}")
(a
("href" "#/channels")
("data-tab-button" "channels")
(text "{{ icon \"rss\" }}")
(span
(text "{{ text \"communities:tab.channels\" }}")))
(text "{%- endif %} {% if can_manage_emojis -%}")
(a
("href" "#/emojis")
("data-tab-button" "emojis")
(text "{{ icon \"smile\" }}")
(span
(text "{{ text \"communities:tab.emojis\" }}")))
(text "{%- endif %}"))
(div
("class" "w-full flex flex-col gap-2")
("data-tab" "general")
(div
("id" "manage_fields")
("class" "card lowered flex flex-col gap-2")
(div
("class" "card-nest")
("ui_ident" "read_access")
(div
("class" "card small")
(b
(text "Read access")))
(div
("class" "card")
(select
("onchange" "save_access(event, 'read')")
(option
("value" "Everybody")
("selected" "{% if community.read_access == 'Everybody' -%}true{% else %}false{%- endif %}")
(text "Everybody"))
(option
("value" "Joined")
("selected" "{% if community.read_access == 'Joined' -%}true{% else %}false{%- endif %}")
(text "Joined")))))
(div
("class" "card-nest")
("ui_ident" "join_access")
(div
("class" "card small")
(b
(text "Join access")))
(div
("class" "card")
(select
("onchange" "save_access(event, 'join')")
(option
("value" "Everybody")
("selected" "{% if community.join_access == 'Everybody' -%}true{% else %}false{%- endif %}")
(text "Everybody"))
(option
("value" "Request")
("selected" "{% if community.join_access == 'Request' -%}true{% else %}false{%- endif %}")
(text "Request"))
(option
("value" "Nobody")
("selected" "{% if community.join_access == 'Nobody' -%}true{% else %}false{%- endif %}")
(text "Nobody")))))
(div
("class" "card-nest")
("ui_ident" "write_access")
(div
("class" "card small")
(b
(text "Post permission")))
(div
("class" "card")
(select
("onchange" "save_access(event, 'write')")
(option
("value" "Everybody")
("selected" "{% if community.write_access == 'Everybody' -%}true{% else %}false{%- endif %}")
(text "Everybody"))
(option
("value" "Joined")
("selected" "{% if community.write_access == 'Joined' -%}true{% else %}false{%- endif %}")
(text "Joined"))
(option
("value" "Owner")
("selected" "{% if community.write_access == 'Owner' -%}true{% else %}false{%- endif %}")
(text "Owner only")))))
(div
("class" "card-nest")
("ui_ident" "change_title")
(div
("class" "card small")
(b
(text "{{ text \"communities:label.change_title\" }}")))
(form
("class" "card flex flex-col gap-2")
("onsubmit" "change_title(event)")
(div
("class" "flex flex-col gap-1")
(label
("for" "new_title")
(text "{{ text \"communities:label.new_title\" }}"))
(input
("type" "text")
("name" "new_title")
("id" "new_title")
("placeholder" "new_title")
("required" "")
("minlength" "2")))
(button
("class" "primary")
(text "{{ icon \"check\" }}")
(span
(text "{{ text \"general:action.save\" }}"))))))
(div
("class" "card-nest")
("ui_ident" "danger_zone")
(div
("class" "card small flex gap-1 items-center red")
(text "{{ icon \"skull\" }}")
(b
(text "{{ text \"communities:label.danger_zone\" }}")))
(div
("class" "card flex flex-wrap gap-2")
(button
("class" "red lowered")
("onclick" "delete_community()")
(text "{{ icon \"trash\" }}")
(span
(text "{{ text \"communities:label.delete_community\" }}")))))
(div
("class" "flex gap-2 flex-wrap")
(button
("onclick" "save_context()")
(text "{{ icon \"check\" }}")
(span
(text "{{ text \"general:action.save\" }}")))
(a
("href" "/community/{{ community.title }}")
("class" "button secondary")
(text "{{ icon \"arrow-left\" }}")
(span
(text "{{ text \"general:action.back\" }}")))))
(div
("class" "card lowered w-full hidden flex flex-col gap-2")
("data-tab" "images")
(div
("class" "card-nest")
("ui_ident" "change_avatar")
(div
("class" "card small")
(b
(text "{{ text \"settings:label.change_avatar\" }}")))
(form
("class" "card flex gap-2 flex-row flex-wrap items-center")
("method" "post")
("enctype" "multipart/form-data")
("onsubmit" "upload_avatar(event)")
(input
("id" "avatar_file")
("name" "file")
("type" "file")
("accept" "image/png,image/jpeg,image/avif,image/webp,image/gif")
("class" "w-content"))
(button
("class" "primary")
(text "{{ icon \"check\" }}"))))
(div
("class" "card-nest")
("ui_ident" "change_banner")
(div
("class" "card small")
(b
(text "{{ text \"settings:label.change_banner\" }}")))
(form
("class" "card flex flex-col gap-2")
("method" "post")
("enctype" "multipart/form-data")
("onsubmit" "upload_banner(event)")
(div
("class" "flex gap-2 flex-row flex-wrap items-center")
(input
("id" "banner_file")
("name" "file")
("type" "file")
("accept" "image/png,image/jpeg,image/avif,image/webp")
("class" "w-content"))
(button
("class" "primary")
(text "{{ icon \"check\" }}")))
(span
("class" "fade")
(text "Use an image of 1100x350px for the best results.")))))
(div
("class" "card lowered w-full hidden flex flex-col gap-2")
("data-tab" "members")
(div
("class" "card-nest")
(div
("class" "card small")
(b
(text "{{ text \"communities:label.select_member\" }}")))
(form
("class" "card flex-col gap-2")
("onsubmit" "select_user_from_form(event)")
(div
("class" "flex flex-col gap-1")
(div
("class" "flex flex-col gap-1")
(label
("for" "uid")
(text "{{ text \"communities:label.user_id\" }}"))
(input
("type" "number")
("name" "uid")
("id" "uid")
("placeholder" "user id")
("required" "")
("minlength" "18")))
(button
("class" "primary")
(text "{{ text \"communities:action.select\" }}")))))
(div
("class" "card flex flex-col gap-2 w-full")
("id" "membership_info"))
(div
("class" "card-nest w-full")
(div
("class" "card small flex items-center justify-between gap-2")
(div
("class" "flex items-center gap-2")
(text "{{ icon \"blocks\" }}")
(span
(text "{{ text \"mod_panel:label.permissions_level_builder\" }}")))
(button
("class" "small lowered")
("onclick" "update_user_role(document.getElementById('uid').value, document.getElementById('role').value)")
(text "{{ icon \"check\" }}")
(span
(text "{{ text \"general:action.save\" }}"))))
(div
("class" "card flex flex-col gap-2")
("id" "permission_builder"))))
(text "{% if can_manage_channels -%}")
(div
("class" "card lowered w-full hidden flex flex-col gap-2")
("data-tab" "channels")
(div
("class" "card-nest")
(div
("class" "card small")
(b
(text "{{ text \"communities:action.create_channel\" }}")))
(form
("class" "card flex flex-col gap-2")
("onsubmit" "create_channel_from_form(event)")
(div
("class" "flex flex-col gap-1")
(label
("for" "title")
(text "{{ text \"communities:label.name\" }}"))
(input
("type" "text")
("name" "title")
("id" "title")
("placeholder" "name")
("required" "")
("minlength" "2")
("maxlength" "32")))
(button
("class" "primary")
(text "{{ text \"communities:action.create\" }}"))))
(text "{% for channel in channels %}")
(div
("class" "card-nest")
(div
("class" "card small")
(b
(text "{{ channel.position }} "))
(text "{{ channel.title }}"))
(div
("class" "card flex gap-2")
(button
("class" "red lowered small")
("onclick" "delete_channel('{{ channel.id }}')")
(text "{{ text \"general:action.delete\" }}"))
(button
("class" "lowered small")
("onclick" "update_channel_position('{{ channel.id }}')")
(text "{{ text \"chats:action.move\" }}"))
(button
("class" "lowered small")
("onclick" "update_channel_title('{{ channel.id }}')")
(text "{{ text \"chats:action.rename\" }}"))))
(text "{% endfor %}"))
(script
(text "globalThis.delete_channel = async (id) => {
if (
!(await trigger(\"atto::confirm\", [
\"Are you sure you would like to do this?\",
]))
) {
return;
}
fetch(`/api/v1/channels/${id}`, {
method: \"DELETE\",
})
.then((res) => res.json())
.then((res) => {
trigger(\"atto::toast\", [
res.ok ? \"success\" : \"error\",
res.message,
]);
});
};
globalThis.update_channel_position = async (id) => {
await trigger(\"atto::debounce\", [\"channels::move\"]);
const position = Number.parseInt(
await trigger(\"atto::prompt\", [
\"New channel position (number):\",
]),
);
if (!position && position !== 0) {
return alert(\"Must be a number!\");
}
fetch(`/api/v1/channels/${id}/move`, {
method: \"POST\",
headers: {
\"Content-Type\": \"application/json\",
},
body: JSON.stringify({
position,
}),
})
.then((res) => res.json())
.then((res) => {
trigger(\"atto::toast\", [
res.ok ? \"success\" : \"error\",
res.message,
]);
});
};
globalThis.update_channel_title = async (id) => {
await trigger(\"atto::debounce\", [\"channels::update_title\"]);
const title = await trigger(\"atto::prompt\", [\"New channel title:\"]);
if (!title) {
return;
}
fetch(`/api/v1/channels/${id}/title`, {
method: \"POST\",
headers: {
\"Content-Type\": \"application/json\",
},
body: JSON.stringify({
title,
}),
})
.then((res) => res.json())
.then((res) => {
trigger(\"atto::toast\", [
res.ok ? \"success\" : \"error\",
res.message,
]);
});
};
async function create_channel_from_form(e) {
e.preventDefault();
await trigger(\"atto::debounce\", [\"channels::create\"]);
fetch(\"/api/v1/channels\", {
method: \"POST\",
headers: {
\"Content-Type\": \"application/json\",
},
body: JSON.stringify({
title: e.target.title.value,
community: \"{{ community.id }}\",
}),
})
.then((res) => res.json())
.then((res) => {
trigger(\"atto::toast\", [
res.ok ? \"success\" : \"error\",
res.message,
]);
if (res.ok) {
window.location.reload();
}
});
}"))
(text "{%- endif %} {% if can_manage_emojis -%}")
(div
("class" "card lowered w-full hidden flex flex-col gap-2")
("data-tab" "emojis")
(text "{{ components::supporter_ad(body=\"Become a supporter to upload GIF animated emojis!\") }}")
(div
("class" "card-nest")
("ui_ident" "change_banner")
(div
("class" "card small flex items-center gap-2")
(text "{{ icon \"upload\" }}")
(b
(text "{{ text \"communities:label.upload\" }}")))
(form
("class" "card flex flex-col gap-2")
("onsubmit" "upload_emoji(event)")
(div
("class" "flex flex-col gap-1")
(label
("for" "name")
(text "{{ text \"communities:label.name\" }}"))
(input
("type" "text")
("name" "name")
("id" "name")
("placeholder" "name")
("required" "")
("minlength" "2")
("maxlength" "32")))
(div
("class" "flex flex-col gap-1")
(label
("for" "file")
(text "{{ text \"communities:label.file\" }}"))
(input
("id" "banner_file")
("name" "file")
("type" "file")
("accept" "image/png,image/jpeg,image/avif,image/webp")
("class" "w-full")))
(button
(text "{{ text \"communities:action.create\" }}"))
(span
("class" "fade")
(text "Emojis can be a maximum of 256 KiB, or 512x512px (width x
height)."))))
(text "{% for emoji in emojis %}")
(div
("class" "card secondary flex flex-wrap gap-2 items-center justify-between")
(div
("class" "flex gap-2 items-center")
(img
("src" "/api/v1/communities/{{ community.id }}/emojis/{{ emoji.name }}")
("alt" "{{ emoji.name }}")
("class" "emoji")
("loading" "lazy"))
(b
(text "{{ emoji.name }}")))
(div
("class" "flex gap-2")
(button
("class" "lowered small")
("onclick" "rename_emoji('{{ emoji.id }}')")
(text "{{ icon \"pencil\" }}")
(span
(text "{{ text \"chats:action.rename\" }}")))
(button
("class" "lowered small red")
("onclick" "remove_emoji('{{ emoji.id }}')")
(text "{{ icon \"x\" }}")
(span
(text "{{ text \"stacks:label.remove\" }}")))))
(text "{% endfor %}"))
(script
(text "globalThis.upload_emoji = (e) => {
e.preventDefault();
e.target.querySelector(\"button\").style.display = \"none\";
fetch(
`/api/v1/communities/{{ community.id }}/emojis/${e.target.name.value}`,
{
method: \"POST\",
body: e.target.file.files[0],
},
)
.then((res) => res.json())
.then((res) => {
trigger(\"atto::toast\", [
res.ok ? \"success\" : \"error\",
res.message,
]);
e.target.querySelector(\"button\").removeAttribute(\"style\");
});
alert(\"Emoji upload in progress. Please wait!\");
};
globalThis.rename_emoji = async (id) => {
const name = await trigger(\"atto::prompt\", [\"New emoji name:\"]);
if (!name) {
return;
}
fetch(`/api/v1/emojis_id/${id}/name`, {
method: \"POST\",
headers: {
\"Content-Type\": \"application/json\",
},
body: JSON.stringify({
name,
}),
})
.then((res) => res.json())
.then((res) => {
trigger(\"atto::toast\", [
res.ok ? \"success\" : \"error\",
res.message,
]);
});
};
globalThis.remove_emoji = async (id) => {
if (
!(await trigger(\"atto::confirm\", [
\"Are you sure you would like to do this? This action is permanent.\",
]))
) {
return;
}
fetch(`/api/v1/emojis_id/${id}`, {
method: \"DELETE\",
})
.then((res) => res.json())
.then((res) => {
trigger(\"atto::toast\", [
res.ok ? \"success\" : \"error\",
res.message,
]);
});
};"))
(text "{%- endif %}"))
(script
(text "setTimeout(async () => {
const element = document.getElementById(\"membership_info\");
const ui = await ns(\"ui\");
const uid = new URLSearchParams(window.location.search).get(\"uid\");
if (uid) {
document.getElementById(\"uid\").value = uid;
}
globalThis.update_user_role = async (uid, new_role) => {
if (
!(await trigger(\"atto::confirm\", [
\"Are you sure you would like to do this?\",
]))
) {
return;
}
fetch(
`/api/v1/communities/{{ community.id }}/memberships/${uid}/role`,
{
method: \"POST\",
headers: {
\"Content-Type\": \"application/json\",
},
body: JSON.stringify({
role: Number.parseInt(new_role),
}),
},
)
.then((res) => res.json())
.then((res) => {
trigger(\"atto::toast\", [
res.ok ? \"success\" : \"error\",
res.message,
]);
});
};
globalThis.kick_user = async (uid, new_role) => {
if (
!(await trigger(\"atto::confirm\", [
\"Are you sure you would like to do this?\",
]))
) {
return;
}
fetch(`/api/v1/communities/{{ community.id }}/memberships/${uid}`, {
method: \"DELETE\",
})
.then((res) => res.json())
.then((res) => {
trigger(\"atto::toast\", [
res.ok ? \"success\" : \"error\",
res.message,
]);
});
};
globalThis.transfer_ownership = async (uid) => {
if (
!(await trigger(\"atto::confirm\", [
\"Are you sure you would like to do this?\\n\\nThis action is PERMANENT!\",
]))
) {
return;
}
fetch(`/api/v1/communities/{{ community.id }}/transfer_ownership`, {
method: \"POST\",
headers: {
\"Content-Type\": \"application/json\",
},
body: JSON.stringify({
user: uid,
}),
})
.then((res) => res.json())
.then((res) => {
trigger(\"atto::toast\", [
res.ok ? \"success\" : \"error\",
res.message,
]);
});
};
globalThis.select_user_from_form = (e) => {
e.preventDefault();
fetch(
`/api/v1/communities/{{ community.id }}/memberships/${e.target.uid.value}`,
)
.then((res) => res.json())
.then(async (res) => {
trigger(\"atto::toast\", [
res.ok ? \"success\" : \"error\",
res.message,
]);
if (!res.ok) {
return;
}
// permissions manager
const get_permissions_html = await trigger(
\"ui::generate_permissions_ui\",
[
{
// https://trisuaso.github.io/tetratto/tetratto/model/communities_permissions/struct.CommunityPermission.html
DEFAULT: 1 << 0,
ADMINISTRATOR: 1 << 1,
MEMBER: 1 << 2,
MANAGE_POSTS: 1 << 3,
MANAGE_ROLES: 1 << 4,
BANNED: 1 << 5,
REQUESTED: 1 << 6,
MANAGE_PINS: 1 << 7,
MANAGE_COMMUNITY: 1 << 8,
MANAGE_QUESTIONS: 1 << 9,
MANAGE_CHANNELS: 1 << 10,
MANAGE_MESSAGES: 1 << 11,
MANAGE_EMOJIS: 1 << 12,
},
],
);
// ...
element.innerHTML = `<div class=\"flex gap-2 flex-wrap\" ui_ident=\"actions\">
<a target=\"_blank\" class=\"button\" href=\"/api/v1/auth/user/find/${e.target.uid.value}\">Open user profile</a>
${res.payload.role !== 33 ? `<button class=\"red lowered\" onclick=\"update_user_role('${e.target.uid.value}', 33)\">Ban</button>` : `<button class=\"lowered\" onclick=\"update_user_role('${e.target.uid.value}', 5)\">Unban</button>`}
${res.payload.role !== 65 ? `<button class=\"red lowered\" onclick=\"update_user_role('${e.target.uid.value}', 65)\">Send to review</button>` : `<button class=\"green lowered\" onclick=\"update_user_role('${e.target.uid.value}', 5)\">Accept join request</button>`}
<button class=\"red lowered\" onclick=\"kick_user('${e.target.uid.value}')\">Kick</button>
<button class=\"red lowered\" onclick=\"transfer_ownership('${e.target.uid.value}')\">Transfer ownership</button>
</div>`;
document.getElementById(\"permission_builder\").innerHTML =
get_permissions_html(res.payload.role, \"permission_builder\");
ui.refresh_container(element, [\"actions\", \"permissions\"]);
ui.generate_settings_ui(
element,
[
[
[\"role\", \"Permission level\"],
res.payload.role,
\"input\",
],
],
null,
{
role: (new_role) => {
const [matching, _] =
all_matching_permissions(new_role);
document.getElementById(
\"permissions\",
).innerHTML = get_permissions_html(
rebuild_role(matching),
\"permissions\",
);
return update_user_role(
e.target.uid.value,
new_role,
);
},
},
);
});
};
}, 250);"))
(script
("type" "application/json")
("id" "settings_json")
(text "{{ community.context|json_encode()|safe }}"))
(script
(text "setTimeout(async () => {
const ui = await ns(\"ui\");
const settings = JSON.parse(
document.getElementById(\"settings_json\").innerHTML,
);
globalThis.upload_avatar = (e) => {
e.preventDefault();
e.target.querySelector(\"button\").style.display = \"none\";
fetch(\"/api/v1/communities/{{ community.id }}/upload/avatar\", {
method: \"POST\",
body: e.target.file.files[0],
})
.then((res) => res.json())
.then((res) => {
trigger(\"atto::toast\", [
res.ok ? \"success\" : \"error\",
res.message,
]);
e.target.querySelector(\"button\").removeAttribute(\"style\");
});
alert(\"Avatar upload in progress. Please wait!\");
};
globalThis.upload_banner = (e) => {
e.preventDefault();
e.target.querySelector(\"button\").style.display = \"none\";
fetch(\"/api/v1/communities/{{ community.id }}/upload/banner\", {
method: \"POST\",
body: e.target.file.files[0],
})
.then((res) => res.json())
.then((res) => {
trigger(\"atto::toast\", [
res.ok ? \"success\" : \"error\",
res.message,
]);
e.target.querySelector(\"button\").removeAttribute(\"style\");
});
alert(\"Banner upload in progress. Please wait!\");
};
globalThis.save_context = () => {
fetch(\"/api/v1/communities/{{ community.id }}/context\", {
method: \"POST\",
headers: {
\"Content-Type\": \"application/json\",
},
body: JSON.stringify({
context: settings,
}),
})
.then((res) => res.json())
.then((res) => {
trigger(\"atto::toast\", [
res.ok ? \"success\" : \"error\",
res.message,
]);
});
};
globalThis.save_access = (event, mode) => {
const selected = event.target.selectedOptions[0];
fetch(`/api/v1/communities/{{ community.id }}/access/${mode}`, {
method: \"POST\",
headers: {
\"Content-Type\": \"application/json\",
},
body: JSON.stringify({
access: selected.value,
}),
})
.then((res) => res.json())
.then((res) => {
trigger(\"atto::toast\", [
res.ok ? \"success\" : \"error\",
res.message,
]);
});
};
globalThis.change_title = async (e) => {
e.preventDefault();
if (
!(await trigger(\"atto::confirm\", [
\"Are you sure you would like to do this?\",
]))
) {
return;
}
fetch(\"/api/v1/communities/{{ community.id }}/title\", {
method: \"POST\",
headers: {
\"Content-Type\": \"application/json\",
},
body: JSON.stringify({
title: e.target.new_title.value,
}),
})
.then((res) => res.json())
.then((res) => {
trigger(\"atto::toast\", [
res.ok ? \"success\" : \"error\",
res.message,
]);
});
};
globalThis.delete_community = async () => {
if (
!(await trigger(\"atto::confirm\", [
\"Are you sure you would like to do this? This action is permanent.\",
]))
) {
return;
}
fetch(`/api/v1/communities/{{ community.id }}`, {
method: \"DELETE\",
})
.then((res) => res.json())
.then((res) => {
trigger(\"atto::toast\", [
res.ok ? \"success\" : \"error\",
res.message,
]);
});
};
ui.refresh_container(document.getElementById(\"manage_fields\"), [
\"read_access\",
\"join_access\",
\"write_access\",
\"change_title\",
\"change_avatar\",
\"change_banner\",
]);
const settings_fields = [
[
[\"display_name\", \"Display title\"],
\"{{ community.context.display_name }}\",
\"input\",
],
[
[\"description\", \"Description\"],
settings.description,
\"textarea\",
],
[
[\"is_nsfw\", \"Mark as NSFW\"],
\"{{ community.context.is_nsfw }}\",
\"checkbox\",
]
];
// {% if not community.is_forge %}
settings_fields.push([
[
\"enable_questions\",
\"Allow users to ask questions in this community\",
],
\"{{ community.context.enable_questions }}\",
\"checkbox\",
]);
settings_fields.push([
[
\"enable_titles\",
\"Allow users to attach a title to their posts\",
],
\"{{ community.context.enable_titles }}\",
\"checkbox\",
]);
settings_fields.push([
[
\"require_titles\",
\"Require users to attach a title to their posts\",
],
\"{{ community.context.require_titles }}\",
\"checkbox\",
]);
// {% endif %}
ui.generate_settings_ui(
document.getElementById(\"manage_fields\"),
settings_fields,
settings,
);
}, 250);"))
(text "{% endblock %}")