add: group channels, group channel members list, group channel renaming
This commit is contained in:
parent
a62905a8c4
commit
b91e9a62fb
16 changed files with 341 additions and 77 deletions
8
Cargo.lock
generated
8
Cargo.lock
generated
|
@ -3744,7 +3744,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tetratto"
|
name = "tetratto"
|
||||||
version = "2.1.0"
|
version = "2.2.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ammonia",
|
"ammonia",
|
||||||
"async-stripe",
|
"async-stripe",
|
||||||
|
@ -3775,7 +3775,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tetratto-core"
|
name = "tetratto-core"
|
||||||
version = "2.1.0"
|
version = "2.2.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-recursion",
|
"async-recursion",
|
||||||
"base16ct",
|
"base16ct",
|
||||||
|
@ -3799,7 +3799,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tetratto-l10n"
|
name = "tetratto-l10n"
|
||||||
version = "2.1.0"
|
version = "2.2.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"pathbufd",
|
"pathbufd",
|
||||||
"serde",
|
"serde",
|
||||||
|
@ -3808,7 +3808,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tetratto-shared"
|
name = "tetratto-shared"
|
||||||
version = "2.1.0"
|
version = "2.2.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ammonia",
|
"ammonia",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "tetratto"
|
name = "tetratto"
|
||||||
version = "2.1.0"
|
version = "2.2.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
|
|
|
@ -160,3 +160,6 @@ version = "1.0.0"
|
||||||
"chats:label.go_back" = "Go back"
|
"chats:label.go_back" = "Go back"
|
||||||
"chats:action.leave" = "Leave"
|
"chats:action.leave" = "Leave"
|
||||||
"chats:label.viewing_single_message" = "You're viewing a single message!"
|
"chats:label.viewing_single_message" = "You're viewing a single message!"
|
||||||
|
"chats:action.add_someone" = "Add someone"
|
||||||
|
"chats:action.kick_member" = "Kick member"
|
||||||
|
"chats:action.mention_user" = "Mention user"
|
||||||
|
|
|
@ -47,7 +47,7 @@ hide_user_menu=true) }}
|
||||||
|
|
||||||
<div class="sidebar flex flex-col gap-2 justify-between" id="channels_list">
|
<div class="sidebar flex flex-col gap-2 justify-between" id="channels_list">
|
||||||
<div class="flex flex-col gap-2 w-full">
|
<div class="flex flex-col gap-2 w-full">
|
||||||
<div class="title flex justify-between">
|
<div class="title flex items-center justify-between channel_header">
|
||||||
{% if community %}
|
{% if community %}
|
||||||
<b class="name shorter">
|
<b class="name shorter">
|
||||||
{% if community.context.display_name %} {{
|
{% if community.context.display_name %} {{
|
||||||
|
@ -56,7 +56,7 @@ hide_user_menu=true) }}
|
||||||
</b>
|
</b>
|
||||||
{% else %}
|
{% else %}
|
||||||
<b>{{ text "chats:label.my_chats" }}</b>
|
<b>{{ text "chats:label.my_chats" }}</b>
|
||||||
{% endif %}
|
{% endif %} {% if selected_community != 0 %}
|
||||||
<div class="dropdown">
|
<div class="dropdown">
|
||||||
<button
|
<button
|
||||||
class="camo small"
|
class="camo small"
|
||||||
|
@ -67,7 +67,6 @@ hide_user_menu=true) }}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div class="inner">
|
<div class="inner">
|
||||||
{% if selected_community != 0 %}
|
|
||||||
<a href="/community/{{ selected_community }}">
|
<a href="/community/{{ selected_community }}">
|
||||||
{{ icon "book-heart" }}
|
{{ icon "book-heart" }}
|
||||||
<span
|
<span
|
||||||
|
@ -75,7 +74,7 @@ hide_user_menu=true) }}
|
||||||
}}</span
|
}}</span
|
||||||
>
|
>
|
||||||
</a>
|
</a>
|
||||||
{% endif %} {% if can_manage_channels %}
|
{% if can_manage_channels %}
|
||||||
<a href="/community/{{ selected_community }}/manage">
|
<a href="/community/{{ selected_community }}/manage">
|
||||||
{{ icon "settings" }}
|
{{ icon "settings" }}
|
||||||
<span>{{ text "general:action.manage" }}</span>
|
<span>{{ text "general:action.manage" }}</span>
|
||||||
|
@ -83,6 +82,7 @@ hide_user_menu=true) }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if can_manage_channels %}
|
{% if can_manage_channels %}
|
||||||
|
@ -138,6 +138,8 @@ hide_user_menu=true) }}
|
||||||
:root {
|
:root {
|
||||||
--list-bar-width: 64px;
|
--list-bar-width: 64px;
|
||||||
--channels-bar-width: 256px;
|
--channels-bar-width: 256px;
|
||||||
|
--sidebar-height: calc(100dvh - 42px);
|
||||||
|
--channel-header-height: 48px;
|
||||||
}
|
}
|
||||||
|
|
||||||
html,
|
html,
|
||||||
|
@ -253,7 +255,7 @@ hide_user_menu=true) }}
|
||||||
border-right: solid 1px var(--color-super-lowered);
|
border-right: solid 1px var(--color-super-lowered);
|
||||||
padding: 0.4rem;
|
padding: 0.4rem;
|
||||||
width: max-content;
|
width: max-content;
|
||||||
height: calc(100dvh - 42px);
|
height: var(--sidebar-height);
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
transition: left 0.15s;
|
transition: left 0.15s;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
|
@ -274,7 +276,7 @@ hide_user_menu=true) }}
|
||||||
width: calc(
|
width: calc(
|
||||||
100dvw - var(--list-bar-width) - var(--channels-bar-width)
|
100dvw - var(--list-bar-width) - var(--channels-bar-width)
|
||||||
) !important;
|
) !important;
|
||||||
height: calc(100dvh - 42px);
|
height: var(--sidebar-height);
|
||||||
}
|
}
|
||||||
|
|
||||||
.message {
|
.message {
|
||||||
|
@ -294,13 +296,36 @@ hide_user_menu=true) }}
|
||||||
}
|
}
|
||||||
|
|
||||||
.message.grouped {
|
.message.grouped {
|
||||||
padding: 0.25rem 1rem 0.25rem calc(1rem + 0.5rem + 52px);
|
padding: 0.25rem 1rem 0.25rem calc(1rem + 0.5rem + 42px);
|
||||||
}
|
}
|
||||||
|
|
||||||
turbo-frame {
|
turbo-frame {
|
||||||
display: contents;
|
display: contents;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.channel_header {
|
||||||
|
height: var(--channel-header-height);
|
||||||
|
}
|
||||||
|
|
||||||
|
.members_list_half {
|
||||||
|
padding-top: 1rem;
|
||||||
|
border-top: solid 1px var(--color-super-lowered);
|
||||||
|
}
|
||||||
|
|
||||||
|
.channels_list_half:not(.no_members),
|
||||||
|
.members_list_half {
|
||||||
|
overflow: auto;
|
||||||
|
height: calc(
|
||||||
|
(var(--sidebar-height) - var(--channel-header-height) - 8rem) /
|
||||||
|
2
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 900px) {
|
@media screen and (max-width: 900px) {
|
||||||
|
:root {
|
||||||
|
--sidebar-height: calc(100dvh - 42px * 2);
|
||||||
|
}
|
||||||
|
|
||||||
.message.grouped {
|
.message.grouped {
|
||||||
padding: 0.25rem 1rem 0.25rem calc(1rem + 0.5rem + 39px);
|
padding: 0.25rem 1rem 0.25rem calc(1rem + 0.5rem + 39px);
|
||||||
}
|
}
|
||||||
|
@ -316,7 +341,7 @@ hide_user_menu=true) }}
|
||||||
|
|
||||||
#stream {
|
#stream {
|
||||||
width: 100dvw !important;
|
width: 100dvw !important;
|
||||||
height: calc(100dvh - 42px * 2);
|
height: var(--sidebar-height);
|
||||||
}
|
}
|
||||||
|
|
||||||
nav::after {
|
nav::after {
|
||||||
|
@ -327,10 +352,6 @@ hide_user_menu=true) }}
|
||||||
.chats_nav {
|
.chats_nav {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar {
|
|
||||||
height: calc(100dvh - 42px * 2);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
@ -360,6 +381,10 @@ hide_user_menu=true) }}
|
||||||
anchor.href += `?nav=${window.SIDEBARS_OPEN}`;
|
anchor.href += `?nav=${window.SIDEBARS_OPEN}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function mention_user(username) {
|
||||||
|
document.getElementById("content").value += ` @${username} `;
|
||||||
|
}
|
||||||
|
|
||||||
function toggle_sidebars() {
|
function toggle_sidebars() {
|
||||||
window.SIDEBARS_OPEN = !window.SIDEBARS_OPEN;
|
window.SIDEBARS_OPEN = !window.SIDEBARS_OPEN;
|
||||||
|
|
||||||
|
@ -388,6 +413,58 @@ hide_user_menu=true) }}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
globalThis.add_member = async (id) => {
|
||||||
|
await trigger("atto::debounce", ["channels::add_member"]);
|
||||||
|
const member = await trigger("atto::prompt", ["Member username:"]);
|
||||||
|
|
||||||
|
if (!member) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch(`/api/v1/channels/${id}/add`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
member,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.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,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
globalThis.kick_member = async (cid, uid) => {
|
globalThis.kick_member = async (cid, uid) => {
|
||||||
if (
|
if (
|
||||||
!(await trigger("atto::confirm", [
|
!(await trigger("atto::confirm", [
|
||||||
|
@ -493,6 +570,7 @@ hide_user_menu=true) }}
|
||||||
});
|
});
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
window.LAST_MESSAGE_AUTHOR_ID = null;
|
||||||
window.socket.addEventListener("message", async (event) => {
|
window.socket.addEventListener("message", async (event) => {
|
||||||
if (event.data === "Ping") {
|
if (event.data === "Ping") {
|
||||||
return socket.send("Pong");
|
return socket.send("Pong");
|
||||||
|
@ -515,6 +593,8 @@ hide_user_menu=true) }}
|
||||||
if (document.getElementById("stream_body")) {
|
if (document.getElementById("stream_body")) {
|
||||||
const element = document.createElement("div");
|
const element = document.createElement("div");
|
||||||
element.style.display = "contents";
|
element.style.display = "contents";
|
||||||
|
|
||||||
|
const message_owner = JSON.parse(msg.data)[1].owner;
|
||||||
element.innerHTML = await (
|
element.innerHTML = await (
|
||||||
await fetch(
|
await fetch(
|
||||||
`/chats/${window.CHAT_PROPS.selected_community}/${window.CHAT_PROPS.selected_channel}/_render`,
|
`/chats/${window.CHAT_PROPS.selected_community}/${window.CHAT_PROPS.selected_channel}/_render`,
|
||||||
|
@ -525,6 +605,9 @@ hide_user_menu=true) }}
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
data: msg.data,
|
data: msg.data,
|
||||||
|
grouped:
|
||||||
|
message_owner ===
|
||||||
|
window.LAST_MESSAGE_AUTHOR_ID,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -534,6 +617,8 @@ hide_user_menu=true) }}
|
||||||
.getElementById("stream_body")
|
.getElementById("stream_body")
|
||||||
.prepend(element);
|
.prepend(element);
|
||||||
clean_text();
|
clean_text();
|
||||||
|
|
||||||
|
window.LAST_MESSAGE_AUTHOR_ID = message_owner;
|
||||||
} else {
|
} else {
|
||||||
console.log("abandoned remote");
|
console.log("abandoned remote");
|
||||||
socket.close();
|
socket.close();
|
||||||
|
@ -557,7 +642,7 @@ hide_user_menu=true) }}
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
content: e.target.content.value,
|
content: e.target.content.value.trim(),
|
||||||
channel: window.CHAT_PROPS.selected_channel,
|
channel: window.CHAT_PROPS.selected_channel,
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,5 +1,9 @@
|
||||||
|
{%- import "components.html" as components -%}
|
||||||
<turbo-frame id="channels_list_frame">
|
<turbo-frame id="channels_list_frame">
|
||||||
{% for channel in channels %} {% if selected_community == 0 %}
|
<div
|
||||||
|
class="channels_list_half flex flex-col gap-2 {% if selected_community != 0 or selected_channel == 0%}no_members{% endif %}"
|
||||||
|
>
|
||||||
|
{% for channel in channels %}
|
||||||
<div class="flex flex-row gap-1">
|
<div class="flex flex-row gap-1">
|
||||||
<a
|
<a
|
||||||
class="w-full justify-start button {% if selected_channel == channel.id %}quaternary{% else %}camo{% endif %}"
|
class="w-full justify-start button {% if selected_channel == channel.id %}quaternary{% else %}camo{% endif %}"
|
||||||
|
@ -21,7 +25,25 @@
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div class="inner">
|
<div class="inner">
|
||||||
{% if user.id == channel.owner %}
|
{% if user.id == channel.owner %} {% if selected_community
|
||||||
|
== 0 %}
|
||||||
|
<button
|
||||||
|
class="quaternary small"
|
||||||
|
onclick="add_member('{{ channel.id }}')"
|
||||||
|
>
|
||||||
|
{{ icon "user-plus" }}
|
||||||
|
<span>{{ text "chats:action.add_someone" }}</span>
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="quaternary small"
|
||||||
|
onclick="update_channel_title('{{ channel.id }}')"
|
||||||
|
>
|
||||||
|
{{ icon "pencil" }}
|
||||||
|
<span>{{ text "chats:action.rename" }}</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onclick="delete_channel('{{ channel.id }}')"
|
onclick="delete_channel('{{ channel.id }}')"
|
||||||
class="red"
|
class="red"
|
||||||
|
@ -41,13 +63,13 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% endfor %}
|
||||||
<a
|
</div>
|
||||||
class="w-full justify-start button {% if selected_channel == channel.id %}quaternary{% else %}camo{% endif %}"
|
|
||||||
href="/chats/{{ selected_community }}/{{ channel.id }}"
|
{% if selected_community == 0 and selected_channel %}
|
||||||
>
|
<div class="members_list_half flex flex-col gap-2">
|
||||||
{{ icon "rss" }}
|
{% for member in members %} {{ components::user_plate(user=member,
|
||||||
<b class="name shortest">{{ channel.title }}</b>
|
show_kick=user.id == channel.owner) }} {% endfor %}
|
||||||
</a>
|
</div>
|
||||||
{% endif %} {% endfor %}
|
{% endif %}
|
||||||
</turbo-frame>
|
</turbo-frame>
|
||||||
|
|
|
@ -1,2 +1,2 @@
|
||||||
{%- import "components.html" as components -%} {{ components::message(user=user,
|
{%- import "components.html" as components -%} {{ components::message(user=user,
|
||||||
message=message) }}
|
message=message, grouped=grouped) }}
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
{% if message %}
|
{% if message %}
|
||||||
<div class="card flex gap-2 small tertiary flex-wrap">
|
<div class="card flex gap-2 small tertiary flex-wrap">
|
||||||
<b>{{ text "chats:label.viewing_single_message" }}</b>
|
<b>{{ text "chats:label.viewing_single_message" }}</b>
|
||||||
<a href="/chats/{{ community }}/{{ channel }}/_stream?page={{ page }}" class="button small" onclick="window.VIEWING_SINGLE = false">
|
<a href="/chats/{{ community }}/{{ channel }}?page={{ page }}" class="button small" onclick="window.VIEWING_SINGLE = false" target="_top">
|
||||||
{{ text "chats:label.go_back" }}
|
{{ text "chats:label.go_back" }}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -925,7 +925,7 @@ if state and state.data %}
|
||||||
{%- endmacro %} {% macro connection_url(key, value) -%} {% if value[0].data.url
|
{%- endmacro %} {% macro connection_url(key, value) -%} {% if value[0].data.url
|
||||||
%} {{ value[0].data.url }} {% elif key == "LastFm" %} https://last.fm/user/{{
|
%} {{ value[0].data.url }} {% elif key == "LastFm" %} https://last.fm/user/{{
|
||||||
value[0].data.name }} {% endif %} {%- endmacro %} {% macro
|
value[0].data.name }} {% endif %} {%- endmacro %} {% macro
|
||||||
message_actions(can_manage_message, user, message) -%}
|
message_actions(can_manage_message, owner, message, owner) -%}
|
||||||
<div class="dropdown">
|
<div class="dropdown">
|
||||||
<button
|
<button
|
||||||
class="camo small"
|
class="camo small"
|
||||||
|
@ -956,6 +956,11 @@ message_actions(can_manage_message, user, message) -%}
|
||||||
{{ icon "copy" }}
|
{{ icon "copy" }}
|
||||||
<span>{{ text "general:action.copy_link" }}</span>
|
<span>{{ text "general:action.copy_link" }}</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<button onclick="mention_user('{{ owner.username }}')">
|
||||||
|
{{ icon "at-sign" }}
|
||||||
|
<span>{{ text "chats:action.mention_user" }}</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{%- endmacro %} {% macro message(user, message, can_manage_message=false,
|
{%- endmacro %} {% macro message(user, message, can_manage_message=false,
|
||||||
|
@ -966,7 +971,7 @@ grouped=false) -%}
|
||||||
>
|
>
|
||||||
{% if not grouped %}
|
{% if not grouped %}
|
||||||
<a href="/@{{ user.username }}" target="_top">
|
<a href="/@{{ user.username }}" target="_top">
|
||||||
{{ self::avatar(username=user.username, size="52px") }}
|
{{ self::avatar(username=user.username, size="42px") }}
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
@ -985,7 +990,7 @@ grouped=false) -%}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex gap-2 hidden">
|
<div class="flex gap-2 hidden">
|
||||||
{{ self::message_actions(user=user, message=message,
|
{{ self::message_actions(owner=user, message=message,
|
||||||
can_manage_message=can_manage_message) }}
|
can_manage_message=can_manage_message) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -996,7 +1001,7 @@ grouped=false) -%}
|
||||||
|
|
||||||
{% if grouped %}
|
{% if grouped %}
|
||||||
<div class="hidden">
|
<div class="hidden">
|
||||||
{{ self::message_actions(user=user, message=message,
|
{{ self::message_actions(owner=user, message=message,
|
||||||
can_manage_message=can_manage_message) }}
|
can_manage_message=can_manage_message) }}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -1096,7 +1101,7 @@ other_user.connections.Spotify[1].data.track %}
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
{% endif %} {%- endmacro %} {% macro user_plate(user, show_menu=false,
|
{% endif %} {%- endmacro %} {% macro user_plate(user, show_menu=false,
|
||||||
secondary=false) -%}
|
show_kick=false, secondary=false) -%}
|
||||||
<div
|
<div
|
||||||
class="flex gap-2 items-center card tiny user_plate {% if secondary %}secondary{% endif %}"
|
class="flex gap-2 items-center card tiny user_plate {% if secondary %}secondary{% endif %}"
|
||||||
>
|
>
|
||||||
|
@ -1125,6 +1130,26 @@ secondary=false) -%}
|
||||||
|
|
||||||
{{ self::user_menu() }}
|
{{ self::user_menu() }}
|
||||||
</div>
|
</div>
|
||||||
|
{% elif show_kick %}
|
||||||
|
<div class="dropdown" style="margin-left: auto">
|
||||||
|
<button
|
||||||
|
class="camo small square"
|
||||||
|
onclick="trigger('atto::hooks::dropdown', [event])"
|
||||||
|
exclude="dropdown"
|
||||||
|
>
|
||||||
|
{{ icon "ellipsis" }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="inner">
|
||||||
|
<button
|
||||||
|
class="red"
|
||||||
|
onclick="kick_member('{{ channel.id }}', '{{ user.id }}')"
|
||||||
|
>
|
||||||
|
{{ icon "x" }}
|
||||||
|
<span>{{ text "chats:action.kick_member" }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{%- endmacro %} {% macro emoji_picker(element_id, render_dialog=false) -%}
|
{%- endmacro %} {% macro emoji_picker(element_id, render_dialog=false) -%}
|
||||||
|
|
|
@ -80,10 +80,12 @@ pub async fn create_group_request(
|
||||||
Err(e) => return Json(e.into()),
|
Err(e) => return Json(e.into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
if other_user.settings.private_chats && data
|
if other_user.settings.private_chats
|
||||||
|
&& data
|
||||||
.get_userfollow_by_initiator_receiver(other_user.id, user.id)
|
.get_userfollow_by_initiator_receiver(other_user.id, user.id)
|
||||||
.await
|
.await
|
||||||
.is_err() {
|
.is_err()
|
||||||
|
{
|
||||||
return Json(Error::NotAllowed.into());
|
return Json(Error::NotAllowed.into());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -168,6 +170,28 @@ pub async fn update_position_request(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn add_member_request(
|
||||||
|
jar: CookieJar,
|
||||||
|
Extension(data): Extension<State>,
|
||||||
|
Path(id): Path<usize>,
|
||||||
|
Json(req): Json<KickMember>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
let data = &(data.read().await).0;
|
||||||
|
let user = match get_user_from_token!(jar, data) {
|
||||||
|
Some(ua) => ua,
|
||||||
|
None => return Json(Error::NotAllowed.into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
match data.add_channel_member(id, user, req.member).await {
|
||||||
|
Ok(_) => Json(ApiReturn {
|
||||||
|
ok: true,
|
||||||
|
message: "Member added".to_string(),
|
||||||
|
payload: (),
|
||||||
|
}),
|
||||||
|
Err(e) => Json(e.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn kick_member_request(
|
pub async fn kick_member_request(
|
||||||
jar: CookieJar,
|
jar: CookieJar,
|
||||||
Extension(data): Extension<State>,
|
Extension(data): Extension<State>,
|
||||||
|
|
|
@ -300,6 +300,10 @@ pub fn routes() -> Router {
|
||||||
post(channels::channels::update_position_request),
|
post(channels::channels::update_position_request),
|
||||||
)
|
)
|
||||||
.route("/channels/{id}", delete(channels::channels::delete_request))
|
.route("/channels/{id}", delete(channels::channels::delete_request))
|
||||||
|
.route(
|
||||||
|
"/channels/{id}/add",
|
||||||
|
post(channels::channels::add_member_request),
|
||||||
|
)
|
||||||
.route(
|
.route(
|
||||||
"/channels/{id}/kick",
|
"/channels/{id}/kick",
|
||||||
post(channels::channels::kick_member_request),
|
post(channels::channels::kick_member_request),
|
||||||
|
|
|
@ -15,6 +15,7 @@ use serde::Deserialize;
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct RenderMessage {
|
pub struct RenderMessage {
|
||||||
pub data: String,
|
pub data: String,
|
||||||
|
pub grouped: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn redirect_request() -> impl IntoResponse {
|
pub async fn redirect_request() -> impl IntoResponse {
|
||||||
|
@ -276,6 +277,7 @@ pub async fn message_request(
|
||||||
|
|
||||||
context.insert("channel", &channel);
|
context.insert("channel", &channel);
|
||||||
context.insert("community", &community);
|
context.insert("community", &community);
|
||||||
|
context.insert("grouped", &req.grouped);
|
||||||
|
|
||||||
// return
|
// return
|
||||||
Ok(Html(data.1.render("chats/message.html", &context).unwrap()))
|
Ok(Html(data.1.render("chats/message.html", &context).unwrap()))
|
||||||
|
@ -285,7 +287,7 @@ pub async fn message_request(
|
||||||
pub async fn channels_request(
|
pub async fn channels_request(
|
||||||
jar: CookieJar,
|
jar: CookieJar,
|
||||||
Extension(data): Extension<State>,
|
Extension(data): Extension<State>,
|
||||||
Path((community, channel)): Path<(usize, usize)>,
|
Path((community, channel_id)): Path<(usize, usize)>,
|
||||||
Query(props): Query<PaginatedQuery>,
|
Query(props): Query<PaginatedQuery>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let data = data.read().await;
|
let data = data.read().await;
|
||||||
|
@ -310,14 +312,42 @@ pub async fn channels_request(
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let channel = if channel_id != 0 {
|
||||||
|
Some(match data.0.get_channel_by_id(channel_id).await {
|
||||||
|
Ok(p) => p,
|
||||||
|
Err(e) => return Err(Html(render_error(e, &jar, &data, &Some(user)).await)),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let members = if community == 0 && channel.is_some() {
|
||||||
|
let ignore_users = data.0.get_userblocks_receivers(user.id).await;
|
||||||
|
|
||||||
|
let mut channel = channel.as_ref().unwrap().clone();
|
||||||
|
channel.members.insert(0, channel.owner); // include the owner in the members list (at the start)
|
||||||
|
|
||||||
|
Some(
|
||||||
|
match data.0.fill_members(&channel.members, ignore_users).await {
|
||||||
|
Ok(p) => p,
|
||||||
|
Err(e) => return Err(Html(render_error(e, &jar, &data, &Some(user)).await)),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
let lang = get_lang!(jar, data.0);
|
let lang = get_lang!(jar, data.0);
|
||||||
let mut context = initial_context(&data.0.0, lang, &Some(user)).await;
|
let mut context = initial_context(&data.0.0, lang, &Some(user)).await;
|
||||||
|
|
||||||
context.insert("channels", &channels);
|
context.insert("channels", &channels);
|
||||||
context.insert("page", &props.page);
|
context.insert("page", &props.page);
|
||||||
|
|
||||||
|
context.insert("members", &members);
|
||||||
|
context.insert("channel", &channel);
|
||||||
|
|
||||||
context.insert("selected_community", &community);
|
context.insert("selected_community", &community);
|
||||||
context.insert("selected_channel", &channel);
|
context.insert("selected_channel", &channel_id);
|
||||||
|
|
||||||
// return
|
// return
|
||||||
Ok(Html(
|
Ok(Html(
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "tetratto-core"
|
name = "tetratto-core"
|
||||||
version = "2.1.0"
|
version = "2.2.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
|
|
|
@ -34,6 +34,25 @@ impl DataManager {
|
||||||
|
|
||||||
auto_method!(get_channel_by_id(usize as i64)@get_channel_from_row -> "SELECT * FROM channels WHERE id = $1" --name="channel" --returns=Channel --cache-key-tmpl="atto.channel:{}");
|
auto_method!(get_channel_by_id(usize as i64)@get_channel_from_row -> "SELECT * FROM channels WHERE id = $1" --name="channel" --returns=Channel --cache-key-tmpl="atto.channel:{}");
|
||||||
|
|
||||||
|
/// Get all member profiles from a channel members list.
|
||||||
|
pub async fn fill_members(
|
||||||
|
&self,
|
||||||
|
members: &Vec<usize>,
|
||||||
|
ignore_users: Vec<usize>,
|
||||||
|
) -> Result<Vec<User>> {
|
||||||
|
let mut out = Vec::new();
|
||||||
|
|
||||||
|
for member in members {
|
||||||
|
if ignore_users.contains(&member) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
out.push(self.get_user_by_id(member.to_owned()).await?);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
|
|
||||||
/// Get all channels by community.
|
/// Get all channels by community.
|
||||||
///
|
///
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
|
@ -214,6 +233,55 @@ impl DataManager {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn add_channel_member(&self, id: usize, user: User, member: String) -> Result<()> {
|
||||||
|
let mut y = self.get_channel_by_id(id).await?;
|
||||||
|
|
||||||
|
if user.id != y.owner && member != user.username {
|
||||||
|
if !user.permissions.check(FinePermission::MANAGE_CHANNELS) {
|
||||||
|
return Err(Error::NotAllowed);
|
||||||
|
} else {
|
||||||
|
self.create_audit_log_entry(AuditLogEntry::new(
|
||||||
|
user.id,
|
||||||
|
format!("invoked `add_channel_member` with x value `{member}`"),
|
||||||
|
))
|
||||||
|
.await?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// check permissions
|
||||||
|
let member = self.get_user_by_username(&member).await?;
|
||||||
|
|
||||||
|
if self
|
||||||
|
.get_userblock_by_initiator_receiver(member.id, user.id)
|
||||||
|
.await
|
||||||
|
.is_ok()
|
||||||
|
{
|
||||||
|
return Err(Error::NotAllowed);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ...
|
||||||
|
y.members.push(member.id);
|
||||||
|
|
||||||
|
let conn = match self.connect().await {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
|
||||||
|
};
|
||||||
|
|
||||||
|
let res = execute!(
|
||||||
|
&conn,
|
||||||
|
"UPDATE channels SET members = $1 WHERE id = $2",
|
||||||
|
params![&serde_json::to_string(&y.members).unwrap(), &(id as i64)]
|
||||||
|
);
|
||||||
|
|
||||||
|
if let Err(e) = res {
|
||||||
|
return Err(Error::DatabaseError(e.to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
self.2.remove(format!("atto.channel:{}", id)).await;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn remove_channel_member(&self, id: usize, user: User, member: usize) -> Result<()> {
|
pub async fn remove_channel_member(&self, id: usize, user: User, member: usize) -> Result<()> {
|
||||||
let mut y = self.get_channel_by_id(id).await?;
|
let mut y = self.get_channel_by_id(id).await?;
|
||||||
|
|
||||||
|
@ -230,7 +298,10 @@ impl DataManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
y.members
|
y.members
|
||||||
.remove(y.members.iter().position(|x| *x == member).unwrap());
|
.remove(match y.members.iter().position(|x| *x == member) {
|
||||||
|
Some(i) => i,
|
||||||
|
None => return Err(Error::GeneralNotFound("member".to_string())),
|
||||||
|
});
|
||||||
|
|
||||||
let conn = match self.connect().await {
|
let conn = match self.connect().await {
|
||||||
Ok(c) => c,
|
Ok(c) => c,
|
||||||
|
|
|
@ -4,7 +4,7 @@ use tetratto_shared::{snow::Snowflake, unix_epoch_timestamp};
|
||||||
use super::communities_permissions::CommunityPermission;
|
use super::communities_permissions::CommunityPermission;
|
||||||
|
|
||||||
/// A channel is a more "chat-like" feed in communities.
|
/// A channel is a more "chat-like" feed in communities.
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Clone, Serialize, Deserialize)]
|
||||||
pub struct Channel {
|
pub struct Channel {
|
||||||
pub id: usize,
|
pub id: usize,
|
||||||
pub community: usize,
|
pub community: usize,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "tetratto-l10n"
|
name = "tetratto-l10n"
|
||||||
version = "2.1.0"
|
version = "2.2.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
authors.workspace = true
|
authors.workspace = true
|
||||||
repository.workspace = true
|
repository.workspace = true
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "tetratto-shared"
|
name = "tetratto-shared"
|
||||||
version = "2.1.0"
|
version = "2.2.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
authors.workspace = true
|
authors.workspace = true
|
||||||
repository.workspace = true
|
repository.workspace = true
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue