diff --git a/Cargo.lock b/Cargo.lock
index 86eccc9..11d719e 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -3744,7 +3744,7 @@ dependencies = [
[[package]]
name = "tetratto"
-version = "2.1.0"
+version = "2.2.0"
dependencies = [
"ammonia",
"async-stripe",
@@ -3775,7 +3775,7 @@ dependencies = [
[[package]]
name = "tetratto-core"
-version = "2.1.0"
+version = "2.2.0"
dependencies = [
"async-recursion",
"base16ct",
@@ -3799,7 +3799,7 @@ dependencies = [
[[package]]
name = "tetratto-l10n"
-version = "2.1.0"
+version = "2.2.0"
dependencies = [
"pathbufd",
"serde",
@@ -3808,7 +3808,7 @@ dependencies = [
[[package]]
name = "tetratto-shared"
-version = "2.1.0"
+version = "2.2.0"
dependencies = [
"ammonia",
"chrono",
diff --git a/crates/app/Cargo.toml b/crates/app/Cargo.toml
index b232819..492741f 100644
--- a/crates/app/Cargo.toml
+++ b/crates/app/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "tetratto"
-version = "2.1.0"
+version = "2.2.0"
edition = "2024"
[features]
diff --git a/crates/app/src/langs/en-US.toml b/crates/app/src/langs/en-US.toml
index b731547..3b45ceb 100644
--- a/crates/app/src/langs/en-US.toml
+++ b/crates/app/src/langs/en-US.toml
@@ -160,3 +160,6 @@ version = "1.0.0"
"chats:label.go_back" = "Go back"
"chats:action.leave" = "Leave"
"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"
diff --git a/crates/app/src/public/html/chats/app.html b/crates/app/src/public/html/chats/app.html
index 92a1ed9..440bc0e 100644
--- a/crates/app/src/public/html/chats/app.html
+++ b/crates/app/src/public/html/chats/app.html
@@ -47,7 +47,7 @@ hide_user_menu=true) }}
{%- endmacro %} {% macro emoji_picker(element_id, render_dialog=false) -%}
diff --git a/crates/app/src/routes/api/v1/channels/channels.rs b/crates/app/src/routes/api/v1/channels/channels.rs
index 16a3f3a..cb8c3c1 100644
--- a/crates/app/src/routes/api/v1/channels/channels.rs
+++ b/crates/app/src/routes/api/v1/channels/channels.rs
@@ -80,10 +80,12 @@ pub async fn create_group_request(
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)
.await
- .is_err() {
+ .is_err()
+ {
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,
+ Path(id): Path,
+ Json(req): Json,
+) -> 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(
jar: CookieJar,
Extension(data): Extension,
diff --git a/crates/app/src/routes/api/v1/mod.rs b/crates/app/src/routes/api/v1/mod.rs
index 6146ccd..e44254b 100644
--- a/crates/app/src/routes/api/v1/mod.rs
+++ b/crates/app/src/routes/api/v1/mod.rs
@@ -300,6 +300,10 @@ pub fn routes() -> Router {
post(channels::channels::update_position_request),
)
.route("/channels/{id}", delete(channels::channels::delete_request))
+ .route(
+ "/channels/{id}/add",
+ post(channels::channels::add_member_request),
+ )
.route(
"/channels/{id}/kick",
post(channels::channels::kick_member_request),
diff --git a/crates/app/src/routes/pages/chats.rs b/crates/app/src/routes/pages/chats.rs
index 4b6f73a..733e4fb 100644
--- a/crates/app/src/routes/pages/chats.rs
+++ b/crates/app/src/routes/pages/chats.rs
@@ -15,6 +15,7 @@ use serde::Deserialize;
#[derive(Deserialize)]
pub struct RenderMessage {
pub data: String,
+ pub grouped: bool,
}
pub async fn redirect_request() -> impl IntoResponse {
@@ -276,6 +277,7 @@ pub async fn message_request(
context.insert("channel", &channel);
context.insert("community", &community);
+ context.insert("grouped", &req.grouped);
// return
Ok(Html(data.1.render("chats/message.html", &context).unwrap()))
@@ -285,7 +287,7 @@ pub async fn message_request(
pub async fn channels_request(
jar: CookieJar,
Extension(data): Extension,
- Path((community, channel)): Path<(usize, usize)>,
+ Path((community, channel_id)): Path<(usize, usize)>,
Query(props): Query,
) -> impl IntoResponse {
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 mut context = initial_context(&data.0.0, lang, &Some(user)).await;
context.insert("channels", &channels);
context.insert("page", &props.page);
+ context.insert("members", &members);
+ context.insert("channel", &channel);
+
context.insert("selected_community", &community);
- context.insert("selected_channel", &channel);
+ context.insert("selected_channel", &channel_id);
// return
Ok(Html(
diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml
index 692c5c5..1169c31 100644
--- a/crates/core/Cargo.toml
+++ b/crates/core/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "tetratto-core"
-version = "2.1.0"
+version = "2.2.0"
edition = "2024"
[features]
diff --git a/crates/core/src/database/channels.rs b/crates/core/src/database/channels.rs
index 6026c48..527008b 100644
--- a/crates/core/src/database/channels.rs
+++ b/crates/core/src/database/channels.rs
@@ -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:{}");
+ /// Get all member profiles from a channel members list.
+ pub async fn fill_members(
+ &self,
+ members: &Vec,
+ ignore_users: Vec,
+ ) -> Result> {
+ 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.
///
/// # Arguments
@@ -214,6 +233,55 @@ impl DataManager {
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<()> {
let mut y = self.get_channel_by_id(id).await?;
@@ -230,7 +298,10 @@ impl DataManager {
}
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 {
Ok(c) => c,
diff --git a/crates/core/src/model/channels.rs b/crates/core/src/model/channels.rs
index 987cd31..5bbefdf 100644
--- a/crates/core/src/model/channels.rs
+++ b/crates/core/src/model/channels.rs
@@ -4,7 +4,7 @@ use tetratto_shared::{snow::Snowflake, unix_epoch_timestamp};
use super::communities_permissions::CommunityPermission;
/// A channel is a more "chat-like" feed in communities.
-#[derive(Serialize, Deserialize)]
+#[derive(Clone, Serialize, Deserialize)]
pub struct Channel {
pub id: usize,
pub community: usize,
diff --git a/crates/l10n/Cargo.toml b/crates/l10n/Cargo.toml
index 7f96ff0..8a55128 100644
--- a/crates/l10n/Cargo.toml
+++ b/crates/l10n/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "tetratto-l10n"
-version = "2.1.0"
+version = "2.2.0"
edition = "2024"
authors.workspace = true
repository.workspace = true
diff --git a/crates/shared/Cargo.toml b/crates/shared/Cargo.toml
index baa7c8b..23e28a6 100644
--- a/crates/shared/Cargo.toml
+++ b/crates/shared/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "tetratto-shared"
-version = "2.1.0"
+version = "2.2.0"
edition = "2024"
authors.workspace = true
repository.workspace = true