+
+
+
+
+
@@ -222,13 +268,13 @@ function onServerContextMenu(event: MouseEvent, server: Server) {
Cancel
- Create
+ {{ dialogTab === 'create' ? 'Create' : 'Join' }}
diff --git a/frontend/src/pages/auth/join.vue b/frontend/src/pages/auth/join.vue
index e3d1e68..a204e15 100644
--- a/frontend/src/pages/auth/join.vue
+++ b/frontend/src/pages/auth/join.vue
@@ -1,12 +1,13 @@
+
+
+
+
+
+
+
+ {{ error }}
+ Retour à l’accueil
+
+
+
+
+
diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts
index dca3bc8..0b73a87 100644
--- a/frontend/src/router/index.ts
+++ b/frontend/src/router/index.ts
@@ -41,6 +41,12 @@ const router = createRouter({
path: '/',
component: AppLayout,
children: [
+ {
+ path: ':serverId([0-9a-fA-F-]{36})',
+ name: 'server-invite',
+ component: () => import('@/pages/server/invite.vue'),
+ props: true,
+ },
{
path: '',
name: 'home',
@@ -101,7 +107,10 @@ router.beforeEach(async (to) => {
if (authRequired && !authStore.isAuthenticated) {
// Non connecté -> Login
- return '/auth/login'
+ if (to.name === 'server-invite') {
+ return {name: 'join', query: {serverId: String(to.params.serverId)}}
+ }
+ return {name: 'login', query: {redirect: to.fullPath}}
} else if (to.name === 'login' && authStore.isAuthenticated) {
// Déjà connecté -> Accueil
return '/'
diff --git a/frontend/src/stores/server.ts b/frontend/src/stores/server.ts
index e19adb6..876306f 100644
--- a/frontend/src/stores/server.ts
+++ b/frontend/src/stores/server.ts
@@ -13,6 +13,20 @@ export interface Server {
unread_count?: number
}
+export type OrderedResourceType = 'channel' | 'category'
+
+export interface ServerItemOrderReference {
+ resource_id: string
+ resource_type: OrderedResourceType
+}
+
+export interface ReorderServerItemPayload extends ServerItemOrderReference {
+ server_id: string
+ parent_category_id: string | null
+ reference: ServerItemOrderReference | null
+ position: 'before' | 'after'
+}
+
export const useServerStore = defineStore("server", {
state: () => ({
servers: [] as Server[],
@@ -62,6 +76,14 @@ export const useServerStore = defineStore("server", {
this.loading = false;
}
},
+ async joinServer(serverId: string, password?: string | null) {
+ const response = await useApi().post(`/servers/${serverId}/join`, {password: password || null});
+ const error = !response.ok ? await response.json().catch(() => null) : null;
+ if (!response.ok) throw new Error(error?.error || 'Failed to join server');
+ const server: Server = await response.json();
+ if (!this.servers.some(item => item.id === server.id)) this.servers.push(server);
+ return server;
+ },
async updateServer(serverId: string, payload: { name: string; is_default?: boolean }) {
const api = useApi();
const response = await api.put(`/servers/${serverId}`, {
@@ -111,6 +133,13 @@ export const useServerStore = defineStore("server", {
return tree.items;
},
+ async reorderItem(payload: ReorderServerItemPayload) {
+ const response = await useApi().put('/server-item-orders/reorder', payload)
+ if (!response.ok) {
+ const error = await response.json().catch(() => null)
+ throw new Error(error?.error || 'Failed to reorder server item')
+ }
+ },
applyChannelReadState(serverId: string, channelId: string, unreadCount: number) {
let previousUnreadCount = 0;
diff --git a/src/domain/dto/core.rs b/src/domain/dto/core.rs
index c3fc8d9..3b142b7 100644
--- a/src/domain/dto/core.rs
+++ b/src/domain/dto/core.rs
@@ -1,6 +1,7 @@
use serde::Deserialize;
use utoipa::ToSchema;
use validator::Validate;
+use uuid::Uuid;
#[derive(Deserialize, Validate, ToSchema)]
pub struct JoinRequest {
@@ -11,4 +12,5 @@ pub struct JoinRequest {
#[validate(must_match(other = "password", message = "Passwords do not match"))]
pub password_valid: String,
pub superuser_token: Option
,
+ pub server_id: Option,
}
diff --git a/src/domain/dto/mod.rs b/src/domain/dto/mod.rs
index 16badc0..c8edaa4 100644
--- a/src/domain/dto/mod.rs
+++ b/src/domain/dto/mod.rs
@@ -9,4 +9,5 @@ pub mod message;
pub mod reaction;
pub mod role;
pub mod server;
+pub mod server_item_order;
pub mod user;
diff --git a/src/domain/dto/server.rs b/src/domain/dto/server.rs
index 2f9ba23..f88159a 100644
--- a/src/domain/dto/server.rs
+++ b/src/domain/dto/server.rs
@@ -14,6 +14,11 @@ pub struct CreateServerRequest {
pub is_default: bool,
}
+#[derive(Debug, Deserialize, ToSchema)]
+pub struct JoinServerRequest {
+ pub password: Option,
+}
+
#[derive(Debug, Serialize, Deserialize, ToSchema)]
pub struct UpdateServerRequest {
pub name: String,
diff --git a/src/domain/dto/server_item_order.rs b/src/domain/dto/server_item_order.rs
new file mode 100644
index 0000000..cfddc6d
--- /dev/null
+++ b/src/domain/dto/server_item_order.rs
@@ -0,0 +1,29 @@
+use crate::models::server_item_order::OrderedResourceType;
+use serde::{Deserialize, Serialize};
+use utoipa::ToSchema;
+use uuid::Uuid;
+
+#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
+pub struct ServerItemOrderReference {
+ pub resource_id: Uuid,
+ pub resource_type: OrderedResourceType,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum ServerItemOrderPosition {
+ Before,
+ After,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
+pub struct ReorderServerItemRequest {
+ pub server_id: Uuid,
+ pub resource_id: Uuid,
+ pub resource_type: OrderedResourceType,
+ /// Nouvelle catégorie parente du canal. Doit être nul pour une catégorie.
+ pub parent_category_id: Option,
+ /// Élément devant ou derrière lequel insérer la ressource.
+ pub reference: Option,
+ pub position: ServerItemOrderPosition,
+}
diff --git a/src/repositories/read_state.rs b/src/repositories/read_state.rs
index 42cede1..8f4138f 100644
--- a/src/repositories/read_state.rs
+++ b/src/repositories/read_state.rs
@@ -1,7 +1,10 @@
use crate::models::{channel, channel_user_read_state, message};
use crate::repositories::{AnyResult, RepositoryContext};
use chrono::Utc;
-use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, QuerySelect, Set};
+use sea_orm::{
+ ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, QuerySelect, Set,
+ RelationTrait, sea_query::OnConflict,
+};
use std::collections::HashMap;
use std::sync::Arc;
use uuid::Uuid;
@@ -12,6 +15,58 @@ pub struct ReadStateRepository {
}
impl ReadStateRepository {
+ /// Marque tous les canaux d'un serveur comme lus en une opération groupée.
+ pub async fn mark_server_read(&self, server_id: Uuid, user_id: Uuid) -> AnyResult<()> {
+ let last_messages = message::Entity::find()
+ .select_only()
+ .column(message::Column::ChannelId)
+ .column_as(message::Column::Id.max(), "last_read_message_id")
+ .join(
+ sea_orm::JoinType::InnerJoin,
+ message::Relation::Channel.def(),
+ )
+ .filter(channel::Column::ServerId.eq(server_id))
+ .group_by(message::Column::ChannelId)
+ .into_tuple::<(Uuid, Uuid)>()
+ .all(&self.context.db)
+ .await?;
+
+ if last_messages.is_empty() {
+ return Ok(());
+ }
+
+ let now = Utc::now();
+ let states = last_messages
+ .into_iter()
+ .map(|(channel_id, last_read_message_id)| {
+ channel_user_read_state::ActiveModel {
+ id: Set(Uuid::now_v7()),
+ channel_id: Set(channel_id),
+ user_id: Set(user_id),
+ last_read_message_id: Set(Some(last_read_message_id)),
+ updated_at: Set(now),
+ }
+ })
+ .collect::>();
+
+ channel_user_read_state::Entity::insert_many(states)
+ .on_conflict(
+ OnConflict::columns([
+ channel_user_read_state::Column::ChannelId,
+ channel_user_read_state::Column::UserId,
+ ])
+ .update_columns([
+ channel_user_read_state::Column::LastReadMessageId,
+ channel_user_read_state::Column::UpdatedAt,
+ ])
+ .to_owned(),
+ )
+ .exec(&self.context.db)
+ .await?;
+
+ Ok(())
+ }
+
pub async fn get(
&self,
channel_id: Uuid,
diff --git a/src/repositories/server.rs b/src/repositories/server.rs
index 9783ded..b66d73b 100644
--- a/src/repositories/server.rs
+++ b/src/repositories/server.rs
@@ -1,7 +1,7 @@
use super::{AnyResult, RepositoryContext};
use crate::models::{role, server, server_role_permission, server_user, server_user_permission};
use sea_orm::prelude::*;
-use sea_orm::{ActiveModelTrait, QuerySelect, Set};
+use sea_orm::{ActiveModelTrait, JoinType, QuerySelect, RelationTrait, Set};
use sea_orm::sea_query::OnConflict;
use std::sync::Arc;
@@ -17,6 +17,27 @@ impl ServerRepository {
Ok(server::Entity::find().all(&self.context.db).await?)
}
+ pub async fn get_all_for_user(&self, user_id: Uuid) -> AnyResult> {
+ Ok(server::Entity::find()
+ .join(JoinType::InnerJoin, server::Relation::ServerUser.def())
+ .filter(server_user::Column::UserId.eq(user_id))
+ .distinct()
+ .all(&self.context.db)
+ .await?)
+ }
+
+ pub async fn get_by_id_for_user(
+ &self,
+ id: Uuid,
+ user_id: Uuid,
+ ) -> AnyResult