add: forums ui
This commit is contained in:
parent
2be87c397d
commit
9ec52abfe4
24 changed files with 770 additions and 64 deletions
4
Cargo.lock
generated
4
Cargo.lock
generated
|
@ -3318,7 +3318,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "tetratto"
|
||||
version = "13.0.0"
|
||||
version = "14.0.0"
|
||||
dependencies = [
|
||||
"ammonia",
|
||||
"async-stripe",
|
||||
|
@ -3350,7 +3350,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "tetratto-core"
|
||||
version = "13.0.0"
|
||||
version = "14.0.0"
|
||||
dependencies = [
|
||||
"async-recursion",
|
||||
"base16ct",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "tetratto"
|
||||
version = "13.0.0"
|
||||
version = "14.0.0"
|
||||
edition = "2024"
|
||||
authors.workspace = true
|
||||
repository.workspace = true
|
||||
|
|
|
@ -81,6 +81,8 @@ pub const COMMUNITIES_CREATE_POST: &str =
|
|||
include_str!("./public/html/communities/create_post.lisp");
|
||||
pub const COMMUNITIES_QUESTION: &str = include_str!("./public/html/communities/question.lisp");
|
||||
pub const COMMUNITIES_QUESTIONS: &str = include_str!("./public/html/communities/questions.lisp");
|
||||
pub const COMMUNITIES_TOPICS: &str = include_str!("./public/html/communities/topics.lisp");
|
||||
pub const COMMUNITIES_TOPIC: &str = include_str!("./public/html/communities/topic.lisp");
|
||||
|
||||
pub const POST_POST: &str = include_str!("./public/html/post/post.lisp");
|
||||
pub const POST_REPOSTS: &str = include_str!("./public/html/post/reposts.lisp");
|
||||
|
@ -316,6 +318,8 @@ pub(crate) async fn write_assets(config: &Config) -> PathBufD {
|
|||
write_template!(html_path->"communities/create_post.html"(crate::assets::COMMUNITIES_CREATE_POST) --config=config --lisp plugins);
|
||||
write_template!(html_path->"communities/question.html"(crate::assets::COMMUNITIES_QUESTION) --config=config --lisp plugins);
|
||||
write_template!(html_path->"communities/questions.html"(crate::assets::COMMUNITIES_QUESTIONS) --config=config --lisp plugins);
|
||||
write_template!(html_path->"communities/topics.html"(crate::assets::COMMUNITIES_TOPICS) --config=config --lisp plugins);
|
||||
write_template!(html_path->"communities/topic.html"(crate::assets::COMMUNITIES_TOPIC) --config=config --lisp plugins);
|
||||
|
||||
write_template!(html_path->"post/post.html"(crate::assets::POST_POST) -d "post" --config=config --lisp plugins);
|
||||
write_template!(html_path->"post/reposts.html"(crate::assets::POST_REPOSTS) --config=config --lisp plugins);
|
||||
|
|
|
@ -37,6 +37,7 @@ version = "1.0.0"
|
|||
"general:label.account" = "Account"
|
||||
"general:label.safety" = "Safety"
|
||||
"general:label.share" = "Share"
|
||||
"general:label.edit" = "Edit"
|
||||
"general:action.add_account" = "Add account"
|
||||
"general:action.switch_account" = "Switch account"
|
||||
"general:label.mod" = "Mod"
|
||||
|
@ -102,6 +103,9 @@ version = "1.0.0"
|
|||
"communities:action.select" = "Select"
|
||||
"communities:label.create_new" = "Create new community"
|
||||
"communities:label.name" = "Name"
|
||||
"communities:label.description" = "Description"
|
||||
"communities:label.color" = "Color"
|
||||
"communities:label.position" = "Position"
|
||||
"communities:label.my_communities" = "My communities"
|
||||
"communities:label.popular_communities" = "Popular communities"
|
||||
"communities:action.join" = "Join"
|
||||
|
@ -112,6 +116,7 @@ version = "1.0.0"
|
|||
"communities:label.content" = "Content"
|
||||
"communities:label.title" = "Title"
|
||||
"communities:label.posts" = "Posts"
|
||||
"communities:label.topics" = "Topics"
|
||||
"communities:label.questions" = "Questions"
|
||||
"communities:label.not_allowed_to_read" = "You're not allowed to view this community's posts"
|
||||
"communities:label.might_need_to_join" = "You might need to join this community in order to interact with it!"
|
||||
|
@ -129,6 +134,7 @@ version = "1.0.0"
|
|||
"communities:label.change_title" = "Change title"
|
||||
"communities:label.new_title" = "New title"
|
||||
"communities:label.pinned" = "Pinned"
|
||||
"communities:label.stickied" = "Stickied"
|
||||
"communities:label.edit_content" = "Edit content"
|
||||
"communities:label.repost" = "Repost"
|
||||
"communities:label.quote_post" = "Quote post"
|
||||
|
@ -149,6 +155,8 @@ version = "1.0.0"
|
|||
"communities:label.load" = "Load"
|
||||
"communities:action.draw" = "Draw"
|
||||
"communities:action.remove_drawing" = "Remove drawing"
|
||||
"communities:tab.topics" = "Topics"
|
||||
"communities:action.create_topic" = "Create topic"
|
||||
|
||||
"notifs:action.mark_as_read" = "Mark as read"
|
||||
"notifs:action.mark_as_unread" = "Mark as unread"
|
||||
|
|
|
@ -83,6 +83,10 @@
|
|||
--size-formula: clamp(24px, calc(var(--size) * 0.75), 64px);
|
||||
}
|
||||
|
||||
.smaller_avatar .avatar {
|
||||
--size-formula: clamp(18px, calc(var(--size) * 0.75), 64px);
|
||||
}
|
||||
|
||||
textarea {
|
||||
min-height: 12rem !important;
|
||||
}
|
||||
|
|
|
@ -163,7 +163,8 @@
|
|||
(text "{{ text \"communities:action.create\" }}"))))))
|
||||
(text "{% if not quoting -%}")
|
||||
(script
|
||||
(text "async function create_post_from_form(e) {
|
||||
(text "globalThis.SEARCH_PARAMS = new URLSearchParams(window.location.search);
|
||||
async function create_post_from_form(e) {
|
||||
e.preventDefault();
|
||||
await trigger(\"atto::debounce\", [\"posts::create\"]);
|
||||
|
||||
|
@ -204,6 +205,7 @@
|
|||
content: e.target.content.value,
|
||||
community: !is_selected_stack ? selected_community : \"0\",
|
||||
stack: is_selected_stack ? selected_community : \"0\",
|
||||
topic: !is_selected_stack ? SEARCH_PARAMS.get(\"topic\") || \"0\" : \"0\",
|
||||
poll: poll_data[1],
|
||||
title: e.target.title.value,
|
||||
}),
|
||||
|
@ -457,7 +459,7 @@
|
|||
check_community_supports_title({
|
||||
target: document.getElementById(\"community_to_post_to\"),
|
||||
});
|
||||
}, 150);
|
||||
}, 250);
|
||||
|
||||
window.cancel_create_post = async () => {
|
||||
if (
|
||||
|
|
|
@ -23,5 +23,4 @@
|
|||
(div
|
||||
("class" "card flex flex_col gap_4")
|
||||
(text "{% for post in feed %} {% if post[0].context.repost and post[0].context.repost.reposting -%} {{ components::repost(repost=post[2], post=post[0], owner=post[1], secondary=true, show_community=false, can_manage_post=can_manage_posts) }} {% else %} {{ components::post(post=post[0], owner=post[1], question=post[3], secondary=true, show_community=false, can_manage_post=can_manage_posts, poll=post[4]) }} {%- endif %} {% endfor %} {{ components::pagination(page=page, items=feed|length) }}"))))
|
||||
|
||||
(text "{% endblock %}")
|
||||
|
|
|
@ -6,7 +6,9 @@
|
|||
(main
|
||||
("class" "flex flex_col gap_2")
|
||||
(div
|
||||
("class" "pillmenu")
|
||||
("class" "pillmenu rows w-full")
|
||||
(div
|
||||
("class" "row")
|
||||
(a
|
||||
("href" "#/general")
|
||||
("data-tab-button" "general")
|
||||
|
@ -25,7 +27,9 @@
|
|||
("data-tab-button" "members")
|
||||
(text "{{ icon \"users-round\" }}")
|
||||
(span
|
||||
(text "{{ text \"communities:tab.members\" }}")))
|
||||
(text "{{ text \"communities:tab.members\" }}"))))
|
||||
(div
|
||||
("class" "row")
|
||||
(text "{% if can_manage_channels -%}")
|
||||
(a
|
||||
("href" "#/channels")
|
||||
|
@ -33,6 +37,13 @@
|
|||
(text "{{ icon \"rss\" }}")
|
||||
(span
|
||||
(text "{{ text \"communities:tab.channels\" }}")))
|
||||
(text "{%- endif %} {% if community.is_forum -%}")
|
||||
(a
|
||||
("href" "#/topics")
|
||||
("data-tab-button" "topics")
|
||||
(icon (text "list"))
|
||||
(span
|
||||
(str (text "communities:tab.topics"))))
|
||||
(text "{%- endif %} {% if can_manage_emojis -%}")
|
||||
(a
|
||||
("href" "#/emojis")
|
||||
|
@ -40,7 +51,7 @@
|
|||
(text "{{ icon \"smile\" }}")
|
||||
(span
|
||||
(text "{{ text \"communities:tab.emojis\" }}")))
|
||||
(text "{%- endif %}"))
|
||||
(text "{%- endif %}")))
|
||||
(div
|
||||
("class" "w_full flex flex_col gap_2")
|
||||
("data-tab" "general")
|
||||
|
@ -564,6 +575,235 @@
|
|||
]);
|
||||
});
|
||||
};"))
|
||||
(text "{%- endif %}")
|
||||
|
||||
(text "{% if community.is_forum -%}")
|
||||
(script ("type" "application/json") ("id" "community_topics") (text "{{ community.topics | json_encode() | safe }}"))
|
||||
(div
|
||||
("class" "card lowered w_full hidden flex flex_col gap_2")
|
||||
("data-tab" "topics")
|
||||
(div
|
||||
("class" "card_nest")
|
||||
(div
|
||||
("class" "card small")
|
||||
(b
|
||||
(str (text "communities:action.create_topic"))))
|
||||
(form
|
||||
("class" "card flex flex_col gap_2")
|
||||
("onsubmit" "create_topic_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")))
|
||||
(div
|
||||
("class" "flex flex_col gap_1")
|
||||
(label
|
||||
("for" "description")
|
||||
(str (text "communities:label.description")))
|
||||
(input
|
||||
("type" "text")
|
||||
("name" "description")
|
||||
("id" "description")
|
||||
("placeholder" "description")
|
||||
("required" "")
|
||||
("minlength" "2")
|
||||
("maxlength" "256")))
|
||||
(div
|
||||
("class" "flex flex_col gap_1")
|
||||
(label
|
||||
("for" "color")
|
||||
(str (text "communities:label.color")))
|
||||
(input
|
||||
("type" "color")
|
||||
("name" "color")
|
||||
("id" "color")
|
||||
("placeholder" "color")
|
||||
("required" "")
|
||||
("style" "width: 8rem")))
|
||||
(div
|
||||
("class" "flex flex_col gap_1")
|
||||
(label
|
||||
("for" "position")
|
||||
(str (text "communities:label.position")))
|
||||
(input
|
||||
("type" "number")
|
||||
("name" "position")
|
||||
("id" "position")
|
||||
("placeholder" "position")
|
||||
("required" "")
|
||||
("value" "0")
|
||||
("min" "0")
|
||||
("max" "256")))
|
||||
(button
|
||||
(text "{{ text \"communities:action.create\" }}"))))
|
||||
(text "{% for id, topic in community.topics %}")
|
||||
(div
|
||||
("class" "card_nest")
|
||||
(div
|
||||
("class" "card small flex justify_between gap_2")
|
||||
(div
|
||||
("class" "flex gap_2")
|
||||
(b
|
||||
(text "{{ topic.position }} "))
|
||||
(text "{{ topic.title }}"))
|
||||
(button
|
||||
("class" "red lowered small")
|
||||
("onclick" "delete_topic('{{ id }}')")
|
||||
(icon (text "trash"))
|
||||
(str (text "general:action.delete"))))
|
||||
(div
|
||||
("class" "card flex flex_col gap_2")
|
||||
(details
|
||||
("class" "accordion")
|
||||
(summary ("class" "flex items_center gap_2") (icon (text "pencil")) (str (text "general:label.edit")))
|
||||
(form
|
||||
("class" "inner flex flex_col gap_2")
|
||||
("style" "background: var(--color-super-raised)")
|
||||
("onsubmit" "update_topic_from_form(event, '{{ id }}')")
|
||||
(div
|
||||
("class" "flex flex_col gap_1")
|
||||
(label
|
||||
("for" "title")
|
||||
(text "{{ text \"communities:label.name\" }}"))
|
||||
(input
|
||||
("type" "text")
|
||||
("name" "title")
|
||||
("id" "title")
|
||||
("placeholder" "name")
|
||||
("value" "{{ topic.title }}")
|
||||
("required" "")
|
||||
("minlength" "2")
|
||||
("maxlength" "32")))
|
||||
(div
|
||||
("class" "flex flex_col gap_1")
|
||||
(label
|
||||
("for" "description")
|
||||
(str (text "communities:label.description")))
|
||||
(input
|
||||
("type" "text")
|
||||
("name" "description")
|
||||
("id" "description")
|
||||
("placeholder" "description")
|
||||
("value" "{{ topic.description }}")
|
||||
("required" "")
|
||||
("minlength" "2")
|
||||
("maxlength" "256")))
|
||||
(div
|
||||
("class" "flex flex_col gap_1")
|
||||
(label
|
||||
("for" "color")
|
||||
(str (text "communities:label.color")))
|
||||
(input
|
||||
("type" "color")
|
||||
("name" "color")
|
||||
("id" "color")
|
||||
("placeholder" "color")
|
||||
("required" "")
|
||||
("value" "{{ topic.color }}")
|
||||
("style" "width: 8rem")))
|
||||
(div
|
||||
("class" "flex flex_col gap_1")
|
||||
(label
|
||||
("for" "position")
|
||||
(str (text "communities:label.position")))
|
||||
(input
|
||||
("type" "number")
|
||||
("name" "position")
|
||||
("id" "position")
|
||||
("placeholder" "position")
|
||||
("required" "")
|
||||
("value" "{{ topic.position }}")
|
||||
("min" "0")
|
||||
("max" "256")))
|
||||
(button
|
||||
(icon (text "check"))
|
||||
(str (text "general:action.save")))))))
|
||||
(text "{% endfor %}"))
|
||||
(script
|
||||
(text "globalThis.delete_topic = async (id) => {
|
||||
if (
|
||||
!(await trigger(\"atto::confirm\", [
|
||||
\"Are you sure you would like to do this?\",
|
||||
]))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
fetch(`/api/v1/communities/{{ community.id }}/topics/${id}`, {
|
||||
method: \"DELETE\",
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((res) => {
|
||||
trigger(\"atto::toast\", [
|
||||
res.ok ? \"success\" : \"error\",
|
||||
res.message,
|
||||
]);
|
||||
});
|
||||
};
|
||||
|
||||
async function create_topic_from_form(e) {
|
||||
e.preventDefault();
|
||||
await trigger(\"atto::debounce\", [\"topics::create\"]);
|
||||
|
||||
fetch(\"/api/v1/communities/{{ community.id }}/topics\", {
|
||||
method: \"POST\",
|
||||
headers: {
|
||||
\"Content-Type\": \"application/json\",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
title: e.target.title.value,
|
||||
description: e.target.description.value,
|
||||
color: e.target.color.value,
|
||||
position: Number.parseInt(e.target.position.value),
|
||||
}),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((res) => {
|
||||
trigger(\"atto::toast\", [
|
||||
res.ok ? \"success\" : \"error\",
|
||||
res.message,
|
||||
]);
|
||||
|
||||
if (res.ok) {
|
||||
e.target.reset();
|
||||
window.location.reload();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function update_topic_from_form(e, id) {
|
||||
e.preventDefault();
|
||||
await trigger(\"atto::debounce\", [\"topics::update\"]);
|
||||
|
||||
fetch(`/api/v1/communities/{{ community.id }}/topics/${id}`, {
|
||||
method: \"POST\",
|
||||
headers: {
|
||||
\"Content-Type\": \"application/json\",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
title: e.target.title.value,
|
||||
description: e.target.description.value,
|
||||
color: e.target.color.value,
|
||||
position: Number.parseInt(e.target.position.value),
|
||||
}),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((res) => {
|
||||
trigger(\"atto::toast\", [
|
||||
res.ok ? \"success\" : \"error\",
|
||||
res.message,
|
||||
]);
|
||||
});
|
||||
}"))
|
||||
(text "{%- endif %}"))
|
||||
|
||||
(script
|
||||
|
|
42
crates/app/src/public/html/communities/topic.lisp
Normal file
42
crates/app/src/public/html/communities/topic.lisp
Normal file
|
@ -0,0 +1,42 @@
|
|||
(text "{% import \"components.html\" as components %} {% extends \"communities/base.html\" %} {% block content %}")
|
||||
(div
|
||||
("class" "flex flex_col gap_4 w_full")
|
||||
(text "{{ macros::community_nav(community=community, selected=\"posts\") }}")
|
||||
(div
|
||||
("class" "card_nest")
|
||||
(div
|
||||
("class" "card small flex justify_between gap_2")
|
||||
(text "{{ components::topic_display(id=topic_id, topic=topic, community=community, show_description=false) }}")
|
||||
(div
|
||||
("class" "flex gap_2")
|
||||
(a
|
||||
("href" "/communities/intents/post?community={{ community.id }}&topic={{ topic_id }}")
|
||||
("class" "button small lowered")
|
||||
("data-turbo" "false")
|
||||
(icon (text "plus"))
|
||||
(span
|
||||
(str (text "general:action.post"))))
|
||||
(a
|
||||
("href" "/community/{{ community.title }}")
|
||||
("class" "button lowered small")
|
||||
(icon (text "arrow-left"))
|
||||
(str (text "general:action.back")))))
|
||||
(div
|
||||
("class" "card flex flex_col gap_4")
|
||||
(span ("class" "no_p_margin") (text "{{ topic.description|markdown|safe }}"))
|
||||
(hr)
|
||||
(div
|
||||
("class" "w_full")
|
||||
("style" "overflow: auto")
|
||||
(table
|
||||
("class" "w_full")
|
||||
(thead
|
||||
(th (text "Title"))
|
||||
(th (text "Replies"))
|
||||
(th (text "Score"))
|
||||
(th (text "Created")))
|
||||
(tbody
|
||||
(text "{% for post in pinned %} {{ components::topic_post_display(post=post[0], owner=post[1], is_pinned=true) }} {% endfor %}")
|
||||
(text "{% for post in feed %} {{ components::topic_post_display(post=post[0], owner=post[1]) }} {% endfor %}"))))
|
||||
(text "{{ components::pagination(page=page, items=feed|length) }}"))))
|
||||
(text "{% endblock %}")
|
19
crates/app/src/public/html/communities/topics.lisp
Normal file
19
crates/app/src/public/html/communities/topics.lisp
Normal file
|
@ -0,0 +1,19 @@
|
|||
(text "{% import \"components.html\" as components %} {% extends \"communities/base.html\" %} {% block content %}")
|
||||
(div
|
||||
("class" "flex flex_col gap_4 w_full")
|
||||
(text "{{ macros::community_nav(community=community, selected=\"posts\") }}")
|
||||
(div
|
||||
("class" "card_nest")
|
||||
(div
|
||||
("class" "card small flex gap_2 items_center")
|
||||
(icon (text "list"))
|
||||
(span
|
||||
(str (text "communities:label.topics"))))
|
||||
(div
|
||||
("class" "card flex flex_col gap_4")
|
||||
(text "{% for topic in topics_sorted %}")
|
||||
(div
|
||||
("class" "card lowered w_full flex flex_col gap_2")
|
||||
(text "{{ components::topic_display(id=topic[0], topic=topic[1], community=community) }}"))
|
||||
(text "{% endfor %}"))))
|
||||
(text "{% endblock %}")
|
|
@ -543,8 +543,14 @@
|
|||
(text "{{ self::avatar(username=owner.username, size=\"24px\", selector_type=\"username\") }}"))
|
||||
(span
|
||||
("class" "name")
|
||||
(text "{{ self::full_username(user=owner) }}")))
|
||||
(text "{{ self::post_info(post=post, community=community) }}"))
|
||||
(text "{{ self::full_username(user=owner) }}"))
|
||||
(a
|
||||
("href" "/community/{{ community.title }}/topic/{{ post.topic }}")
|
||||
("class" "flush flex gap_1 items_center smaller_avatar")
|
||||
(text "{{ self::community_avatar(id=post.community, community=community, size=\"18px\") }}")))
|
||||
(div
|
||||
("class" "flex gap_2")
|
||||
(text "{{ self::post_info(post=post, community=community) }}")))
|
||||
(div
|
||||
("class" "card_nest_horizontal")
|
||||
; author info
|
||||
|
@ -2066,6 +2072,7 @@
|
|||
(text "{{ icon \"message-circle\" }}")
|
||||
(span
|
||||
(text "{{ text \"communities:label.chats\" }}")))
|
||||
(text "{% if not community.is_forum -%}")
|
||||
(a
|
||||
("href" "/communities/intents/post?community={{ community.id }}")
|
||||
("class" "button lowered")
|
||||
|
@ -2073,6 +2080,7 @@
|
|||
(text "{{ icon \"plus\" }}")
|
||||
(span
|
||||
(text "{{ text \"general:action.post\" }}")))
|
||||
(text "{%- endif %}")
|
||||
(text "{%- endif %} {% if can_manage_community or is_manager -%}")
|
||||
(a
|
||||
("href" "/community/{{ community.id }}/manage")
|
||||
|
@ -2606,3 +2614,39 @@
|
|||
(icon (text "trash")))
|
||||
(text "{%- endif %}"))))
|
||||
(text "{%- endmacro %}")
|
||||
|
||||
(text "{% macro topic_display(id, topic, community, show_description=true) -%}")
|
||||
(div
|
||||
("class" "flex items_center gap_2")
|
||||
(svg
|
||||
("width" "12")
|
||||
("height" "12")
|
||||
("viewBox" "0 0 12 12")
|
||||
("style" "fill: {% if topic.color == \"#000000\" -%} var(--color-primary) {%- else -%} {{ topic.color }} {%- endif %}; margin-top: 3.5px")
|
||||
(circle
|
||||
("cx" "6")
|
||||
("cy" "6")
|
||||
("r" "6")))
|
||||
(a
|
||||
("href" "/community/{{ community.title }}/topic/{{ id }}")
|
||||
("class" "flush")
|
||||
(b (text "{{ topic.title }}"))))
|
||||
(text "{% if show_description -%}")
|
||||
(span ("class" "no_p_margin") (text "{{ topic.description|markdown|safe }}"))
|
||||
(text "{%- endif %}")
|
||||
(text "{%- endmacro %}")
|
||||
|
||||
(text "{% macro topic_post_display(post, owner, is_pinned=false) -%}")
|
||||
(tr
|
||||
(td
|
||||
("class" "flex gap_1")
|
||||
(a
|
||||
("href" "/post/{{ post.id }}")
|
||||
(text "{% if is_pinned -%}Sticky: {% endif %}")
|
||||
(text "{{ post.title }}"))
|
||||
(span ("class" "fade") (text "by"))
|
||||
(text "{{ self::full_username(user=owner) }}"))
|
||||
(td (text "{{ post.comment_count }}"))
|
||||
(td (text "{{ ((post.likes + 1) / (post.dislikes + 1))|round(method=\"ceil\", precision=2) }}"))
|
||||
(td (span ("class" "date") (text "{{ post.created }}"))))
|
||||
(text "{%- endmacro %}")
|
||||
|
|
|
@ -354,6 +354,7 @@
|
|||
content: e.target.content.value,
|
||||
community: \"{{ community.id }}\",
|
||||
stack: \"{{ post.stack }}\",
|
||||
topic: \"{{ post.topic }}\",
|
||||
replying_to: \"{{ post.id }}\",
|
||||
poll: poll_data[1],
|
||||
}),
|
||||
|
|
|
@ -153,7 +153,8 @@ media_theme_pref();
|
|||
.replaceAll(" months ago", "m")
|
||||
.replaceAll(" month ago", "m")
|
||||
.replaceAll(" years ago", "y")
|
||||
.replaceAll(" year ago", "y");
|
||||
.replaceAll(" year ago", "y")
|
||||
.replaceAll("just now", "now");
|
||||
}
|
||||
|
||||
element.innerText = !pretty ? then.toLocaleDateString() : pretty;
|
||||
|
@ -194,7 +195,8 @@ media_theme_pref();
|
|||
.replaceAll(" month ago", "m")
|
||||
.replaceAll(" years ago", "y")
|
||||
.replaceAll(" year ago", "y")
|
||||
.replaceAll("Yesterday", "1d") || "";
|
||||
.replaceAll("Yesterday", "1d")
|
||||
.replaceAll("just now", "now") || "";
|
||||
|
||||
element.innerText = !pretty ? then.toLocaleDateString() : pretty;
|
||||
element.style.display = "inline-block";
|
||||
|
|
|
@ -538,7 +538,7 @@ pub async fn add_topic_request(
|
|||
None => return Json(Error::NotAllowed.into()),
|
||||
};
|
||||
|
||||
let mut community = match data.get_community_by_id(id).await {
|
||||
let mut community = match data.get_community_by_id_no_void(id).await {
|
||||
Ok(x) => x,
|
||||
Err(e) => return Json(e.into()),
|
||||
};
|
||||
|
@ -547,8 +547,22 @@ pub async fn add_topic_request(
|
|||
return Json(Error::DoesNotSupportField("community".to_string()).into());
|
||||
}
|
||||
|
||||
let (id, topic) = ForumTopic::new(req.title, req.description, req.color);
|
||||
community.topics.insert(id, topic);
|
||||
// check lengths
|
||||
if req.title.len() > 32 {
|
||||
return Json(Error::DataTooLong("title".to_string()).into());
|
||||
}
|
||||
|
||||
if req.title.len() < 2 {
|
||||
return Json(Error::DataTooShort("title".to_string()).into());
|
||||
}
|
||||
|
||||
if req.description.len() > 256 {
|
||||
return Json(Error::DataTooLong("description".to_string()).into());
|
||||
}
|
||||
|
||||
// ...
|
||||
let (topic_id, topic) = ForumTopic::new(req.title, req.description, req.color, req.position);
|
||||
community.topics.insert(topic_id, topic);
|
||||
|
||||
match data
|
||||
.update_community_topics(id, &user, community.topics)
|
||||
|
@ -575,7 +589,7 @@ pub async fn update_topic_request(
|
|||
None => return Json(Error::NotAllowed.into()),
|
||||
};
|
||||
|
||||
let mut community = match data.get_community_by_id(id).await {
|
||||
let mut community = match data.get_community_by_id_no_void(id).await {
|
||||
Ok(x) => x,
|
||||
Err(e) => return Json(e.into()),
|
||||
};
|
||||
|
@ -584,10 +598,25 @@ pub async fn update_topic_request(
|
|||
return Json(Error::DoesNotSupportField("community".to_string()).into());
|
||||
}
|
||||
|
||||
// check lengths
|
||||
if req.title.len() > 32 {
|
||||
return Json(Error::DataTooLong("title".to_string()).into());
|
||||
}
|
||||
|
||||
if req.title.len() < 2 {
|
||||
return Json(Error::DataTooShort("title".to_string()).into());
|
||||
}
|
||||
|
||||
if req.description.len() > 256 {
|
||||
return Json(Error::DataTooLong("description".to_string()).into());
|
||||
}
|
||||
|
||||
// ...
|
||||
let topic = ForumTopic {
|
||||
title: req.title,
|
||||
description: req.description,
|
||||
color: req.color,
|
||||
position: req.position,
|
||||
};
|
||||
|
||||
community.topics.insert(topic_id, topic);
|
||||
|
@ -616,7 +645,7 @@ pub async fn delete_topic_request(
|
|||
None => return Json(Error::NotAllowed.into()),
|
||||
};
|
||||
|
||||
let mut community = match data.get_community_by_id(id).await {
|
||||
let mut community = match data.get_community_by_id_no_void(id).await {
|
||||
Ok(x) => x,
|
||||
Err(e) => return Json(e.into()),
|
||||
};
|
||||
|
@ -631,11 +660,14 @@ pub async fn delete_topic_request(
|
|||
.update_community_topics(id, &user, community.topics)
|
||||
.await
|
||||
{
|
||||
Ok(_) => match data.delete_topic_posts(id, topic_id).await {
|
||||
Ok(_) => Json(ApiReturn {
|
||||
ok: true,
|
||||
message: "Community updated".to_string(),
|
||||
payload: (),
|
||||
}),
|
||||
Err(e) => Json(e.into()),
|
||||
},
|
||||
Err(e) => Json(e.into()),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -129,6 +129,10 @@ pub async fn create_request(
|
|||
Ok(x) => x,
|
||||
Err(e) => return Json(Error::MiscError(e.to_string()).into()),
|
||||
};
|
||||
props.topic = match req.topic.parse::<usize>() {
|
||||
Ok(x) => x,
|
||||
Err(e) => return Json(Error::MiscError(e.to_string()).into()),
|
||||
};
|
||||
}
|
||||
|
||||
// check sizes
|
||||
|
|
|
@ -795,6 +795,8 @@ pub struct AddTopic {
|
|||
pub title: String,
|
||||
pub description: String,
|
||||
pub color: String,
|
||||
#[serde(default)]
|
||||
pub position: i32,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
|
@ -821,6 +823,8 @@ pub struct CreatePost {
|
|||
pub title: String,
|
||||
#[serde(default)]
|
||||
pub stack: String,
|
||||
#[serde(default)]
|
||||
pub topic: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
|
|
|
@ -16,7 +16,7 @@ use tera::Context;
|
|||
use tetratto_core::model::{
|
||||
addr::RemoteAddr,
|
||||
auth::User,
|
||||
communities::Community,
|
||||
communities::{Community, ForumTopic},
|
||||
communities_permissions::CommunityPermission,
|
||||
permissions::FinePermission,
|
||||
stacks::{StackMode, UserStack},
|
||||
|
@ -424,6 +424,47 @@ pub async fn feed_request(
|
|||
// check permissions
|
||||
let (can_read, _) = check_community_permissions!(community, jar, data, user);
|
||||
|
||||
// is this is a forum, just show topics
|
||||
if community.is_forum {
|
||||
// init context
|
||||
let lang = get_lang!(jar, data.0);
|
||||
let mut context = initial_context(&data.0.0.0, lang, &user).await;
|
||||
|
||||
let (
|
||||
is_owner,
|
||||
is_joined,
|
||||
is_pending,
|
||||
can_post,
|
||||
can_manage_posts,
|
||||
can_manage_community,
|
||||
can_manage_roles,
|
||||
can_manage_questions,
|
||||
) = community_context_bools!(data, user, community);
|
||||
|
||||
let mut sorted: Vec<(&usize, &ForumTopic)> = community.topics.iter().collect();
|
||||
sorted.sort_by(|x, y| x.1.position.cmp(&y.1.position));
|
||||
|
||||
context.insert("topics_sorted", &sorted);
|
||||
community_context(
|
||||
&mut context,
|
||||
&community,
|
||||
is_owner,
|
||||
is_joined,
|
||||
is_pending,
|
||||
can_post,
|
||||
can_read,
|
||||
can_manage_posts,
|
||||
can_manage_community,
|
||||
can_manage_roles,
|
||||
can_manage_questions,
|
||||
);
|
||||
|
||||
// return
|
||||
return Ok(Html(
|
||||
data.1.render("communities/topics.html", &context).unwrap(),
|
||||
));
|
||||
}
|
||||
|
||||
// ...
|
||||
let ignore_users = crate::ignore_users_gen!(user, data);
|
||||
|
||||
|
@ -485,6 +526,119 @@ pub async fn feed_request(
|
|||
))
|
||||
}
|
||||
|
||||
/// `/community/{title}/topic/{id}`
|
||||
pub async fn topic_feed_request(
|
||||
jar: CookieJar,
|
||||
Path((title, topic_id)): Path<(String, usize)>,
|
||||
Query(props): Query<PaginatedQuery>,
|
||||
Extension(data): Extension<State>,
|
||||
) -> impl IntoResponse {
|
||||
let data = data.read().await;
|
||||
let user = get_user_from_token!(jar, data.0);
|
||||
|
||||
let community = match data.0.get_community_by_title(&title.to_lowercase()).await {
|
||||
Ok(ua) => ua,
|
||||
Err(e) => return Err(Html(render_error(e, &jar, &data, &user).await)),
|
||||
};
|
||||
|
||||
if community.id == 0 {
|
||||
// don't show page for void community
|
||||
return Err(Html(
|
||||
render_error(
|
||||
Error::GeneralNotFound("community".to_string()),
|
||||
&jar,
|
||||
&data,
|
||||
&user,
|
||||
)
|
||||
.await,
|
||||
));
|
||||
}
|
||||
|
||||
let topic = match community.topics.get(&topic_id) {
|
||||
Some(x) => x,
|
||||
None => {
|
||||
return Err(Html(
|
||||
render_error(
|
||||
Error::GeneralNotFound("topic".to_string()),
|
||||
&jar,
|
||||
&data,
|
||||
&user,
|
||||
)
|
||||
.await,
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
// check permissions
|
||||
let (can_read, _) = check_community_permissions!(community, jar, data, user);
|
||||
|
||||
// ...
|
||||
let ignore_users = crate::ignore_users_gen!(user, data);
|
||||
|
||||
let feed = match data
|
||||
.0
|
||||
.get_posts_by_community_topic(community.id, topic_id, 12, props.page, &user)
|
||||
.await
|
||||
{
|
||||
Ok(p) => match data.0.fill_posts(p, &ignore_users, &user).await {
|
||||
Ok(p) => p,
|
||||
Err(e) => return Err(Html(render_error(e, &jar, &data, &user).await)),
|
||||
},
|
||||
Err(e) => return Err(Html(render_error(e, &jar, &data, &user).await)),
|
||||
};
|
||||
|
||||
let pinned = match data
|
||||
.0
|
||||
.get_pinned_posts_by_community_topic(community.id, topic_id)
|
||||
.await
|
||||
{
|
||||
Ok(p) => match data.0.fill_posts(p, &ignore_users, &user).await {
|
||||
Ok(p) => p,
|
||||
Err(e) => return Err(Html(render_error(e, &jar, &data, &user).await)),
|
||||
},
|
||||
Err(e) => return Err(Html(render_error(e, &jar, &data, &user).await)),
|
||||
};
|
||||
|
||||
// init context
|
||||
let lang = get_lang!(jar, data.0);
|
||||
let mut context = initial_context(&data.0.0.0, lang, &user).await;
|
||||
|
||||
let (
|
||||
is_owner,
|
||||
is_joined,
|
||||
is_pending,
|
||||
can_post,
|
||||
can_manage_posts,
|
||||
can_manage_community,
|
||||
can_manage_roles,
|
||||
can_manage_questions,
|
||||
) = community_context_bools!(data, user, community);
|
||||
|
||||
context.insert("feed", &feed);
|
||||
context.insert("pinned", &pinned);
|
||||
context.insert("page", &props.page);
|
||||
context.insert("topic", &topic);
|
||||
context.insert("topic_id", &topic_id);
|
||||
community_context(
|
||||
&mut context,
|
||||
&community,
|
||||
is_owner,
|
||||
is_joined,
|
||||
is_pending,
|
||||
can_post,
|
||||
can_read,
|
||||
can_manage_posts,
|
||||
can_manage_community,
|
||||
can_manage_roles,
|
||||
can_manage_questions,
|
||||
);
|
||||
|
||||
// return
|
||||
Ok(Html(
|
||||
data.1.render("communities/topic.html", &context).unwrap(),
|
||||
))
|
||||
}
|
||||
|
||||
/// `/community/{title}/questions`
|
||||
pub async fn questions_request(
|
||||
jar: CookieJar,
|
||||
|
|
|
@ -99,6 +99,10 @@ pub fn routes() -> Router {
|
|||
get(communities::create_post_request),
|
||||
)
|
||||
.route("/community/{title}", get(communities::feed_request))
|
||||
.route(
|
||||
"/community/{title}/topic/{id}",
|
||||
get(communities::topic_feed_request),
|
||||
)
|
||||
.route(
|
||||
"/community/{title}/questions",
|
||||
get(communities::questions_request),
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
[package]
|
||||
name = "tetratto-core"
|
||||
description = "The core behind Tetratto"
|
||||
version = "13.0.0"
|
||||
version = "14.0.0"
|
||||
edition = "2024"
|
||||
authors.workspace = true
|
||||
repository.workspace = true
|
||||
|
@ -9,7 +9,13 @@ license.workspace = true
|
|||
homepage.workspace = true
|
||||
|
||||
[features]
|
||||
database = ["dep:oiseau", "dep:base64", "dep:base16ct", "dep:async-recursion", "dep:md-5"]
|
||||
database = [
|
||||
"dep:oiseau",
|
||||
"dep:base64",
|
||||
"dep:base16ct",
|
||||
"dep:async-recursion",
|
||||
"dep:md-5",
|
||||
]
|
||||
types = ["dep:totp-rs", "dep:paste", "dep:bitflags"]
|
||||
sdk = ["types", "dep:reqwest"]
|
||||
default = ["database", "types", "sdk"]
|
||||
|
@ -21,8 +27,14 @@ toml = "0.9.4"
|
|||
tetratto-shared = { version = "12.0.6", path = "../shared" }
|
||||
tetratto-l10n = { version = "12.0.0", path = "../l10n" }
|
||||
serde_json = "1.0.142"
|
||||
totp-rs = { version = "5.7.0", features = ["qr", "gen_secret"], optional = true }
|
||||
reqwest = { version = "0.12.22", features = ["json", "multipart"], optional = true }
|
||||
totp-rs = { version = "5.7.0", features = [
|
||||
"qr",
|
||||
"gen_secret",
|
||||
], optional = true }
|
||||
reqwest = { version = "0.12.22", features = [
|
||||
"json",
|
||||
"multipart",
|
||||
], optional = true }
|
||||
bitflags = { version = "2.9.1", optional = true }
|
||||
async-recursion = { version = "1.1.1", optional = true }
|
||||
md-5 = { version = "0.10.6", optional = true }
|
||||
|
|
|
@ -532,6 +532,25 @@ impl DataManager {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn delete_topic_posts(&self, id: usize, topic: usize) -> Result<()> {
|
||||
let conn = match self.0.connect().await {
|
||||
Ok(c) => c,
|
||||
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
|
||||
};
|
||||
|
||||
let res = execute!(
|
||||
&conn,
|
||||
"DELETE FROM posts WHERE community = $1 AND topic = $2",
|
||||
params![&(id as i64), &(topic as i64)]
|
||||
);
|
||||
|
||||
if let Err(e) = res {
|
||||
return Err(Error::DatabaseError(e.to_string()));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
auto_method!(update_community_context(CommunityContext)@get_community_by_id_no_void:FinePermission::MANAGE_COMMUNITIES; -> "UPDATE communities SET context = $1 WHERE id = $2" --serde --cache-key-tmpl=cache_clear_community);
|
||||
auto_method!(update_community_read_access(CommunityReadAccess)@get_community_by_id_no_void:FinePermission::MANAGE_COMMUNITIES; -> "UPDATE communities SET read_access = $1 WHERE id = $2" --serde --cache-key-tmpl=cache_clear_community);
|
||||
auto_method!(update_community_write_access(CommunityWriteAccess)@get_community_by_id_no_void:FinePermission::MANAGE_COMMUNITIES; -> "UPDATE communities SET write_access = $1 WHERE id = $2" --serde --cache-key-tmpl=cache_clear_community);
|
||||
|
|
|
@ -18,5 +18,6 @@ CREATE TABLE IF NOT EXISTS posts (
|
|||
poll_id BIGINT NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
is_open INT NOT NULL DEFAULT 1,
|
||||
circle BIGINT NOT NULL
|
||||
stack BIGINT NOT NULL,
|
||||
topic BIGINT NOT NULL
|
||||
)
|
||||
|
|
|
@ -21,3 +21,7 @@ ADD COLUMN IF NOT EXISTS is_forum INT DEFAULT 0;
|
|||
-- communities topics
|
||||
ALTER TABLE communities
|
||||
ADD COLUMN IF NOT EXISTS topics TEXT DEFAULT '{}';
|
||||
|
||||
-- posts topic
|
||||
ALTER TABLE posts
|
||||
ADD COLUMN IF NOT EXISTS topic BIGINT DEFAULT 0;
|
||||
|
|
|
@ -116,6 +116,7 @@ impl DataManager {
|
|||
title: get!(x->14(String)),
|
||||
is_open: get!(x->15(i32)) as i8 == 1,
|
||||
stack: get!(x->16(i64)) as usize,
|
||||
topic: get!(x->17(i64)) as usize,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1209,6 +1210,60 @@ impl DataManager {
|
|||
Ok(res.unwrap())
|
||||
}
|
||||
|
||||
/// Get all posts from the given community and topic (from most recent).
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `id` - the ID of the community the requested posts belong to
|
||||
/// * `topic` - the ID of the topic the requested posts belong to
|
||||
/// * `batch` - the limit of posts in each page
|
||||
/// * `page` - the page number
|
||||
pub async fn get_posts_by_community_topic(
|
||||
&self,
|
||||
id: usize,
|
||||
topic: usize,
|
||||
batch: usize,
|
||||
page: usize,
|
||||
user: &Option<User>,
|
||||
) -> Result<Vec<Post>> {
|
||||
let conn = match self.0.connect().await {
|
||||
Ok(c) => c,
|
||||
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
|
||||
};
|
||||
|
||||
// check if we should hide nsfw posts
|
||||
let mut hide_nsfw: bool = true;
|
||||
|
||||
if let Some(ua) = user {
|
||||
hide_nsfw = !ua.settings.show_nsfw;
|
||||
}
|
||||
|
||||
// ...
|
||||
let res = query_rows!(
|
||||
&conn,
|
||||
&format!(
|
||||
"SELECT * FROM posts WHERE community = $1 AND topic = $2 AND replying_to = 0 AND NOT context LIKE '%\"is_pinned\":true%' AND is_deleted = 0 {} ORDER BY created DESC LIMIT $3 OFFSET $4",
|
||||
if hide_nsfw {
|
||||
"AND NOT (context::json->>'is_nsfw')::boolean"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
),
|
||||
&[
|
||||
&(id as i64),
|
||||
&(topic as i64),
|
||||
&(batch as i64),
|
||||
&((page * batch) as i64)
|
||||
],
|
||||
|x| { Self::get_post_from_row(x) }
|
||||
);
|
||||
|
||||
if res.is_err() {
|
||||
return Err(Error::GeneralNotFound("post".to_string()));
|
||||
}
|
||||
|
||||
Ok(res.unwrap())
|
||||
}
|
||||
|
||||
/// Get all posts from the given stack (from most recent).
|
||||
///
|
||||
/// # Arguments
|
||||
|
@ -1264,6 +1319,35 @@ impl DataManager {
|
|||
Ok(res.unwrap())
|
||||
}
|
||||
|
||||
/// Get all pinned posts from the given community (from most recent).
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `id` - the ID of the community the requested posts belong to
|
||||
/// * `topic` - the ID of the topic the requested posts belong to
|
||||
pub async fn get_pinned_posts_by_community_topic(
|
||||
&self,
|
||||
id: usize,
|
||||
topic: usize,
|
||||
) -> Result<Vec<Post>> {
|
||||
let conn = match self.0.connect().await {
|
||||
Ok(c) => c,
|
||||
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
|
||||
};
|
||||
|
||||
let res = query_rows!(
|
||||
&conn,
|
||||
"SELECT * FROM posts WHERE community = $1 AND topic = $2 AND context LIKE '%\"is_pinned\":true%' ORDER BY created DESC",
|
||||
&[&(id as i64), &(topic as i64)],
|
||||
|x| { Self::get_post_from_row(x) }
|
||||
);
|
||||
|
||||
if res.is_err() {
|
||||
return Err(Error::GeneralNotFound("post".to_string()));
|
||||
}
|
||||
|
||||
Ok(res.unwrap())
|
||||
}
|
||||
|
||||
/// Get all pinned posts from the given user (from most recent).
|
||||
///
|
||||
/// # Arguments
|
||||
|
@ -1494,7 +1578,7 @@ impl DataManager {
|
|||
let res = query_rows!(
|
||||
&conn,
|
||||
&format!(
|
||||
"SELECT * FROM posts WHERE replying_to = 0{}{}{} AND NOT context LIKE '%\"full_unlist\":true%' ORDER BY created DESC LIMIT $1 OFFSET $2",
|
||||
"SELECT * FROM posts WHERE replying_to = 0{}{}{} AND NOT context LIKE '%\"full_unlist\":true%' AND topic = 0 ORDER BY created DESC LIMIT $1 OFFSET $2",
|
||||
if before_time > 0 {
|
||||
format!(" AND created < {before_time}")
|
||||
} else {
|
||||
|
@ -1748,6 +1832,20 @@ impl DataManager {
|
|||
self.get_community_by_id(data.community).await?
|
||||
};
|
||||
|
||||
// check is_forum
|
||||
if community.is_forum {
|
||||
if data.topic == 0 {
|
||||
return Err(Error::MiscError(
|
||||
"Topic is required for this community".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
if community.topics.get(&data.topic).is_none() {
|
||||
return Err(Error::GeneralNotFound("topic".to_string()));
|
||||
}
|
||||
}
|
||||
|
||||
// ...
|
||||
let mut owner = self.get_user_by_id(data.owner).await?;
|
||||
|
||||
// check values (if this isn't reposting something else)
|
||||
|
@ -2019,7 +2117,7 @@ impl DataManager {
|
|||
|
||||
let res = execute!(
|
||||
&conn,
|
||||
"INSERT INTO posts VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, DEFAULT, $13, $14, $15, $16)",
|
||||
"INSERT INTO posts VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, DEFAULT, $13, $14, $15, $16, $17)",
|
||||
params![
|
||||
&(data.id as i64),
|
||||
&(data.created as i64),
|
||||
|
@ -2041,6 +2139,7 @@ impl DataManager {
|
|||
&data.title,
|
||||
&{ if data.is_open { 1 } else { 0 } },
|
||||
&(data.stack as i64),
|
||||
&(data.topic as i64),
|
||||
]
|
||||
);
|
||||
|
||||
|
|
|
@ -280,6 +280,11 @@ pub struct Post {
|
|||
///
|
||||
/// If stack is not 0, community should be 0 (and vice versa).
|
||||
pub stack: usize,
|
||||
/// The ID of the topic this post belongs to. 0 means no topic is connected.
|
||||
///
|
||||
/// This can only be set if the post is created in a community with `is_forum: true`,
|
||||
/// where this is also a required field.
|
||||
pub topic: usize,
|
||||
}
|
||||
|
||||
impl Post {
|
||||
|
@ -308,6 +313,7 @@ impl Post {
|
|||
title: String::new(),
|
||||
is_open: true,
|
||||
stack: 0,
|
||||
topic: 0,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -534,6 +540,7 @@ pub struct ForumTopic {
|
|||
pub title: String,
|
||||
pub description: String,
|
||||
pub color: String,
|
||||
pub position: i32,
|
||||
}
|
||||
|
||||
impl ForumTopic {
|
||||
|
@ -542,13 +549,14 @@ impl ForumTopic {
|
|||
/// # Returns
|
||||
/// * ID for [`Community`] hashmap
|
||||
/// * [`ForumTopic`]
|
||||
pub fn new(title: String, description: String, color: String) -> (usize, Self) {
|
||||
pub fn new(title: String, description: String, color: String, position: i32) -> (usize, Self) {
|
||||
(
|
||||
Snowflake::new().to_string().parse::<usize>().unwrap(),
|
||||
Self {
|
||||
title,
|
||||
description,
|
||||
color,
|
||||
position,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue