add: circle stacks
This commit is contained in:
parent
50704d27a9
commit
56cea83933
27 changed files with 419 additions and 107 deletions
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "tetratto"
|
||||
version = "7.0.0"
|
||||
version = "8.0.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
@ -19,7 +19,7 @@ tetratto-core = { path = "../core" }
|
|||
tetratto-l10n = { path = "../l10n" }
|
||||
|
||||
image = "0.25.6"
|
||||
reqwest = { version = "0.12.19", features = ["json", "stream"] }
|
||||
reqwest = { version = "0.12.20", features = ["json", "stream"] }
|
||||
regex = "1.11.1"
|
||||
serde_json = "1.0.140"
|
||||
mime_guess = "2.0.5"
|
||||
|
|
|
@ -127,11 +127,8 @@ where
|
|||
Err(_) => return Err((StatusCode::BAD_REQUEST, "json data isn't utf8".to_string())),
|
||||
}) {
|
||||
Ok(s) => s,
|
||||
Err(_) => {
|
||||
return Err((
|
||||
StatusCode::BAD_REQUEST,
|
||||
"could not parse json data as json".to_string(),
|
||||
));
|
||||
Err(e) => {
|
||||
return Err((StatusCode::BAD_REQUEST, e.to_string()));
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -97,6 +97,13 @@
|
|||
("value" "{{ community.id }}")
|
||||
("selected" "{% if selected_community == community.id -%}true{% else %}false{%- endif %}")
|
||||
(text "{% if community.context.display_name -%} {{ community.context.display_name }} {% else %} {{ community.title }} {%- endif %}"))
|
||||
(text "{% endfor %}")
|
||||
(text "{% for stack in stacks %}")
|
||||
(option
|
||||
("value" "{{ stack.id }}")
|
||||
("selected" "{% if selected_stack == stack.id -%}true{% else %}false{%- endif %}")
|
||||
("is_stack" "true")
|
||||
(text "{{ stack.name }} (circle)"))
|
||||
(text "{% endfor %}")))
|
||||
(form
|
||||
("class" "card flex flex-col gap-2")
|
||||
|
@ -184,13 +191,19 @@
|
|||
}
|
||||
}
|
||||
|
||||
const is_selected_stack = document.getElementById(
|
||||
\"community_to_post_to\",
|
||||
).selectedOptions[0].getAttribute(\"is_stack\") === \"true\";
|
||||
const selected_community = document.getElementById(
|
||||
\"community_to_post_to\",
|
||||
).selectedOptions[0].value;
|
||||
|
||||
body.append(
|
||||
\"body\",
|
||||
JSON.stringify({
|
||||
content: e.target.content.value,
|
||||
community: document.getElementById(
|
||||
\"community_to_post_to\",
|
||||
).selectedOptions[0].value,
|
||||
community: !is_selected_stack ? selected_community : \"0\",
|
||||
stack: is_selected_stack ? selected_community : \"0\",
|
||||
poll: poll_data[1],
|
||||
title: e.target.title.value,
|
||||
}),
|
||||
|
@ -316,12 +329,15 @@
|
|||
(text "{% else %}")
|
||||
(script
|
||||
(text "async function create_post_from_form(e) {
|
||||
e.preventDefault();
|
||||
const id = await trigger(\"me::repost\", [
|
||||
\"{{ quoting[1].id }}\",
|
||||
e.target.content.value,
|
||||
document.getElementById(\"community_to_post_to\")
|
||||
.selectedOptions[0].value,
|
||||
false,
|
||||
document.getElementById(\"community_to_post_to\")
|
||||
.selectedOptions[0].getAttribute(\"is_stack\") === \"true\",
|
||||
]);
|
||||
|
||||
// update settings
|
||||
|
@ -394,27 +410,34 @@
|
|||
(text "{%- endif %}"))
|
||||
|
||||
(script
|
||||
(text "const town_square = \"{{ config.town_square }}\";
|
||||
(text "(() => {const town_square = \"{{ config.town_square }}\";
|
||||
const user_id = \"{{ user.id }}\";
|
||||
|
||||
function update_community_avatar(e) {
|
||||
window.update_community_avatar = (e) => {
|
||||
const element = e.target.parentElement.querySelector(\".avatar\");
|
||||
const is_stack = e.target.selectedOptions[0].getAttribute(\"is_stack\") === \"true\";
|
||||
const id = e.target.selectedOptions[0].value;
|
||||
|
||||
element.setAttribute(\"title\", id);
|
||||
element.setAttribute(\"alt\", `${id}'s avatar`);
|
||||
|
||||
if (id === town_square) {
|
||||
if (id === town_square || is_stack) {
|
||||
element.src = `/api/v1/auth/user/${user_id}/avatar?selector_type=id`;
|
||||
} else {
|
||||
element.src = `/api/v1/communities/${id}/avatar`;
|
||||
}
|
||||
}
|
||||
|
||||
function check_community_supports_title(e) {
|
||||
window.check_community_supports_title = async (e) => {
|
||||
const element = document.getElementById(\"title_field\");
|
||||
const is_stack = e.target.selectedOptions[0].getAttribute(\"is_stack\") === \"true\";
|
||||
const id = e.target.selectedOptions[0].value;
|
||||
|
||||
if (is_stack) {
|
||||
element.classList.add(\"hidden\");
|
||||
return;
|
||||
}
|
||||
|
||||
fetch(`/api/v1/communities/${id}/supports_titles`)
|
||||
.then((res) => res.json())
|
||||
.then((res) => {
|
||||
|
@ -436,7 +459,7 @@
|
|||
});
|
||||
}, 150);
|
||||
|
||||
async function cancel_create_post() {
|
||||
window.cancel_create_post = async () => {
|
||||
if (
|
||||
!(await trigger(\"atto::confirm\", [
|
||||
\"Are you sure you would like to do this? Your post content will be lost.\",
|
||||
|
@ -446,6 +469,6 @@
|
|||
}
|
||||
|
||||
window.history.back();
|
||||
}"))
|
||||
}})();"))
|
||||
|
||||
(text "{% endblock %}")
|
||||
|
|
|
@ -173,8 +173,13 @@
|
|||
("class" "flex items-center")
|
||||
("style" "color: var(--color-primary)")
|
||||
(text "{{ icon \"square-asterisk\" }}"))
|
||||
(text "{%- endif %}")
|
||||
(text "{% if community and community.is_forge -%} {% if post.is_open -%}")
|
||||
(text "{%- endif %} {% if post.stack -%}")
|
||||
(span
|
||||
("title" "Posted to a stack you're in")
|
||||
("class" "flex items-center")
|
||||
("style" "color: var(--color-primary)")
|
||||
(text "{{ icon \"layers\" }}"))
|
||||
(text "{%- endif %} {% if community and community.is_forge -%} {% if post.is_open -%}")
|
||||
(span
|
||||
("title" "Open")
|
||||
("class" "flex items-center green")
|
||||
|
|
|
@ -298,6 +298,7 @@
|
|||
JSON.stringify({
|
||||
content: e.target.content.value,
|
||||
community: \"{{ community.id }}\",
|
||||
stack: \"{{ post.stack }}\",
|
||||
replying_to: \"{{ post.id }}\",
|
||||
poll: poll_data[1],
|
||||
}),
|
||||
|
|
|
@ -581,7 +581,9 @@
|
|||
(li
|
||||
(text "Ability to create more than 1 app"))
|
||||
(li
|
||||
(text "Create up to 10 stack blocks")))
|
||||
(text "Create up to 10 stack blocks"))
|
||||
(li
|
||||
(text "Add unlimited users to stacks")))
|
||||
(a
|
||||
("href" "{{ config.stripe.payment_link }}?client_reference_id={{ user.id }}")
|
||||
("class" "button")
|
||||
|
|
|
@ -17,14 +17,27 @@
|
|||
(text "{{ components::avatar(username=stack.owner, selector_type=\"id\") }}"))
|
||||
(span
|
||||
(text "{{ stack.name }}")))
|
||||
(text "{% if user and user.id == stack.owner -%}")
|
||||
(a
|
||||
("href" "/stacks/{{ stack.id }}/manage")
|
||||
("class" "button lowered small")
|
||||
(text "{{ icon \"pencil\" }}")
|
||||
(span
|
||||
(text "{{ text \"general:action.manage\" }}")))
|
||||
(text "{%- endif %}"))
|
||||
(div
|
||||
("class" "flex gap-2")
|
||||
(text "{% if stack.mode == 'Circle' -%}")
|
||||
; post button for circle stacks
|
||||
(a
|
||||
("href" "/communities/intents/post?stack={{ stack.id }}")
|
||||
("class" "button lowered small")
|
||||
(text "{{ icon \"plus\" }}")
|
||||
(span
|
||||
(text "{{ text \"general:action.post\" }}")))
|
||||
(text "{%- endif %}")
|
||||
|
||||
(text "{% if user and user.id == stack.owner -%}")
|
||||
; manage button for stack owner only
|
||||
(a
|
||||
("href" "/stacks/{{ stack.id }}/manage")
|
||||
("class" "button lowered small")
|
||||
(text "{{ icon \"pencil\" }}")
|
||||
(span
|
||||
(text "{{ text \"general:action.manage\" }}")))
|
||||
(text "{%- endif %}")))
|
||||
(div
|
||||
("class" "card w-full flex flex-col gap-2")
|
||||
(text "{% if list|length == 0 -%}")
|
||||
|
@ -37,6 +50,7 @@
|
|||
(text "{%- endif %}")
|
||||
|
||||
(text "{% if stack.mode == 'BlockList' -%}")
|
||||
; block button + user list for blocklist only
|
||||
(text "{% if not is_blocked -%}")
|
||||
(button
|
||||
("onclick" "block_all()")
|
||||
|
@ -50,6 +64,12 @@
|
|||
("class" "flex gap-2 flex-wrap w-full")
|
||||
(text "{% for user in list %} {{ components::user_plate(user=user, secondary=true) }} {% endfor %}"))
|
||||
(text "{% else %}")
|
||||
; user icons for circle stack
|
||||
(text "{% if stack.mode == 'Circle' -%} {% for user in stack.users %}")
|
||||
(text "{{ components::avatar(username=user, selector_type=\"id\", size=\"24px\") }}")
|
||||
(text "{% endfor %} {%- endif %}")
|
||||
|
||||
; posts for all stacks except blocklist
|
||||
(text "{% for post in list %}
|
||||
{% if post[2].read_access == \"Everybody\" -%}
|
||||
{% if post[0].context.repost and post[0].context.repost.reposting -%}
|
||||
|
|
|
@ -67,7 +67,11 @@
|
|||
(option
|
||||
("value" "BlockList")
|
||||
("selected" "{% if stack.mode == 'BlockList' -%}true{% else %}false{%- endif %}")
|
||||
(text "Block list")))))
|
||||
(text "Block list"))
|
||||
(option
|
||||
("value" "Circle")
|
||||
("selected" "{% if stack.mode == 'Circle' -%}true{% else %}false{%- endif %}")
|
||||
(text "Circle")))))
|
||||
(div
|
||||
("class" "card-nest")
|
||||
("ui_ident" "sort")
|
||||
|
|
|
@ -259,7 +259,14 @@
|
|||
|
||||
self.define(
|
||||
"repost",
|
||||
(_, id, content, community, do_not_redirect = false) => {
|
||||
(
|
||||
_,
|
||||
id,
|
||||
content,
|
||||
community,
|
||||
do_not_redirect = false,
|
||||
is_stack = false,
|
||||
) => {
|
||||
return new Promise((resolve, _) => {
|
||||
fetch(`/api/v1/posts/${id}/repost`, {
|
||||
method: "POST",
|
||||
|
@ -268,7 +275,8 @@
|
|||
},
|
||||
body: JSON.stringify({
|
||||
content,
|
||||
community,
|
||||
community: !is_stack ? community : "0",
|
||||
stack: is_stack ? community : "0",
|
||||
}),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
|
|
|
@ -124,6 +124,10 @@ pub async fn create_request(
|
|||
};
|
||||
} else {
|
||||
props.title = req.title;
|
||||
props.stack = match req.stack.parse::<usize>() {
|
||||
Ok(x) => x,
|
||||
Err(e) => return Json(Error::MiscError(e.to_string()).into()),
|
||||
};
|
||||
}
|
||||
|
||||
// check sizes
|
||||
|
@ -197,18 +201,23 @@ pub async fn create_repost_request(
|
|||
None => return Json(Error::NotAllowed.into()),
|
||||
};
|
||||
|
||||
match data
|
||||
.create_post(Post::repost(
|
||||
req.content,
|
||||
match req.community.parse::<usize>() {
|
||||
Ok(x) => x,
|
||||
Err(e) => return Json(Error::MiscError(e.to_string()).into()),
|
||||
},
|
||||
user.id,
|
||||
id,
|
||||
))
|
||||
.await
|
||||
{
|
||||
let mut props = Post::repost(
|
||||
req.content,
|
||||
match req.community.parse::<usize>() {
|
||||
Ok(x) => x,
|
||||
Err(e) => return Json(Error::MiscError(e.to_string()).into()),
|
||||
},
|
||||
user.id,
|
||||
id,
|
||||
);
|
||||
|
||||
props.stack = match req.stack.parse::<usize>() {
|
||||
Ok(x) => x,
|
||||
Err(e) => return Json(Error::MiscError(e.to_string()).into()),
|
||||
};
|
||||
|
||||
// ...
|
||||
match data.create_post(props).await {
|
||||
Ok(id) => Json(ApiReturn {
|
||||
ok: true,
|
||||
message: "Post reposted".to_string(),
|
||||
|
|
|
@ -616,12 +616,15 @@ pub struct CreatePost {
|
|||
pub poll: Option<CreatePostPoll>,
|
||||
#[serde(default)]
|
||||
pub title: String,
|
||||
#[serde(default)]
|
||||
pub stack: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct CreateRepost {
|
||||
pub content: String,
|
||||
pub community: String,
|
||||
pub stack: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
|
|
|
@ -5,11 +5,14 @@ use axum::{
|
|||
Extension, Json,
|
||||
};
|
||||
use axum_extra::extract::CookieJar;
|
||||
use tetratto_core::model::{
|
||||
oauth,
|
||||
permissions::FinePermission,
|
||||
stacks::{StackBlock, StackPrivacy, UserStack},
|
||||
ApiReturn, Error,
|
||||
use tetratto_core::{
|
||||
model::{
|
||||
oauth,
|
||||
permissions::FinePermission,
|
||||
stacks::{StackBlock, StackMode, StackPrivacy, UserStack},
|
||||
ApiReturn, Error,
|
||||
},
|
||||
DataManager,
|
||||
};
|
||||
use super::{
|
||||
AddOrRemoveStackUser, CreateStack, UpdateStackMode, UpdateStackName, UpdateStackPrivacy,
|
||||
|
@ -161,6 +164,25 @@ pub async fn add_user_request(
|
|||
};
|
||||
|
||||
stack.users.push(other_user.id);
|
||||
|
||||
// check number of stacks
|
||||
let owner = match data.get_user_by_id(stack.owner).await {
|
||||
Ok(ua) => ua,
|
||||
Err(e) => return Json(e.into()),
|
||||
};
|
||||
|
||||
if !owner.permissions.check(FinePermission::SUPPORTER) {
|
||||
if stack.users.len() >= DataManager::MAXIMUM_FREE_STACK_USERS {
|
||||
return Json(
|
||||
Error::MiscError(
|
||||
"This stack already has the maximum users it can have".to_string(),
|
||||
)
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ...
|
||||
match data.update_stack_users(id, &user, stack.users).await {
|
||||
Ok(_) => Json(ApiReturn {
|
||||
ok: true,
|
||||
|
@ -250,6 +272,7 @@ pub async fn get_users_request(
|
|||
|
||||
if stack.privacy == StackPrivacy::Private
|
||||
&& user.id != stack.owner
|
||||
&& !(stack.mode == StackMode::Circle && stack.users.contains(&user.id))
|
||||
&& !user.permissions.check(FinePermission::MANAGE_STACKS)
|
||||
{
|
||||
return Json(Error::NotAllowed.into());
|
||||
|
|
|
@ -11,8 +11,12 @@ use axum_extra::extract::CookieJar;
|
|||
use serde::Deserialize;
|
||||
use tera::Context;
|
||||
use tetratto_core::model::{
|
||||
auth::User, communities::Community, communities_permissions::CommunityPermission,
|
||||
permissions::FinePermission, Error,
|
||||
auth::User,
|
||||
communities::Community,
|
||||
communities_permissions::CommunityPermission,
|
||||
permissions::FinePermission,
|
||||
stacks::{StackMode, UserStack},
|
||||
Error,
|
||||
};
|
||||
|
||||
#[macro_export]
|
||||
|
@ -245,6 +249,8 @@ pub struct CreatePostProps {
|
|||
#[serde(default)]
|
||||
pub community: usize,
|
||||
#[serde(default)]
|
||||
pub stack: usize,
|
||||
#[serde(default)]
|
||||
pub from_draft: usize,
|
||||
#[serde(default)]
|
||||
pub quote: usize,
|
||||
|
@ -286,6 +292,16 @@ pub async fn create_post_request(
|
|||
communities.push(community)
|
||||
}
|
||||
|
||||
let stacks = match data.0.get_stacks_by_user(user.id).await {
|
||||
Ok(s) => s,
|
||||
Err(e) => return Err(Html(render_error(e, &jar, &data, &Some(user)).await)),
|
||||
};
|
||||
|
||||
let stacks: Vec<&UserStack> = stacks
|
||||
.iter()
|
||||
.filter(|x| x.mode == StackMode::Circle)
|
||||
.collect();
|
||||
|
||||
// get draft
|
||||
let draft = if props.from_draft != 0 {
|
||||
match data.0.get_draft_by_id(props.from_draft).await {
|
||||
|
@ -326,8 +342,10 @@ pub async fn create_post_request(
|
|||
|
||||
context.insert("draft", &draft);
|
||||
context.insert("drafts", &drafts);
|
||||
context.insert("stacks", &stacks);
|
||||
context.insert("quoting", "ing);
|
||||
context.insert("communities", &communities);
|
||||
context.insert("selected_stack", &props.stack);
|
||||
context.insert("selected_community", &props.community);
|
||||
|
||||
// return
|
||||
|
@ -663,6 +681,28 @@ pub async fn post_request(
|
|||
}
|
||||
}
|
||||
|
||||
// check stack
|
||||
if post.stack != 0 {
|
||||
let stack = match data.0.get_stack_by_id(post.stack).await {
|
||||
Ok(c) => c,
|
||||
Err(e) => return Err(Html(render_error(e, &jar, &data, &user).await)),
|
||||
};
|
||||
|
||||
if let Some(ref ua) = user {
|
||||
if (stack.owner != ua.id) && !stack.users.contains(&ua.id) {
|
||||
return Err(Html(
|
||||
render_error(Error::NotAllowed, &jar, &data, &user).await,
|
||||
));
|
||||
}
|
||||
} else {
|
||||
// we MUST be authenticated to view posts in a stack
|
||||
return Err(Html(
|
||||
render_error(Error::NotAllowed, &jar, &data, &user).await,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// ...
|
||||
let community = match data.0.get_community_by_id(post.community).await {
|
||||
Ok(c) => c,
|
||||
Err(e) => return Err(Html(render_error(e, &jar, &data, &user).await)),
|
||||
|
|
|
@ -50,7 +50,7 @@ pub async fn settings_request(
|
|||
}
|
||||
};
|
||||
|
||||
let stacks = match data.0.get_stacks_by_owner(profile.id).await {
|
||||
let stacks = match data.0.get_stacks_by_user(profile.id).await {
|
||||
Ok(ua) => ua,
|
||||
Err(e) => {
|
||||
return Err(Html(render_error(e, &jar, &data, &None).await));
|
||||
|
|
|
@ -25,7 +25,7 @@ pub async fn list_request(jar: CookieJar, Extension(data): Extension<State>) ->
|
|||
}
|
||||
};
|
||||
|
||||
let list = match data.0.get_stacks_by_owner(user.id).await {
|
||||
let list = match data.0.get_stacks_by_user(user.id).await {
|
||||
Ok(p) => p,
|
||||
Err(e) => return Err(Html(render_error(e, &jar, &data, &Some(user)).await)),
|
||||
};
|
||||
|
@ -63,6 +63,7 @@ pub async fn feed_request(
|
|||
|
||||
if stack.privacy == StackPrivacy::Private
|
||||
&& user.id != stack.owner
|
||||
&& !(stack.mode == StackMode::Circle && stack.users.contains(&user.id))
|
||||
&& !user.permissions.check(FinePermission::MANAGE_STACKS)
|
||||
{
|
||||
return Err(Html(
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue