cdx_core/extensions/collaboration/
session.rs1use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5
6use crate::DocumentId;
7
8use super::Collaborator;
9
10#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
12#[serde(rename_all = "camelCase")]
13pub struct CollaborationSession {
14 pub id: String,
16
17 pub document_id: DocumentId,
19
20 pub participants: Vec<Participant>,
22
23 pub started: DateTime<Utc>,
25
26 pub status: SessionStatus,
28}
29
30impl CollaborationSession {
31 #[must_use]
33 pub fn new(id: impl Into<String>, document_id: DocumentId) -> Self {
34 Self {
35 id: id.into(),
36 document_id,
37 participants: Vec::new(),
38 started: Utc::now(),
39 status: SessionStatus::Active,
40 }
41 }
42
43 pub fn add_participant(&mut self, participant: Participant) {
45 self.participants.push(participant);
46 }
47
48 pub fn remove_participant(&mut self, user_id: &str) {
50 self.participants
51 .retain(|p| p.author.user_id.as_deref() != Some(user_id));
52 }
53
54 #[must_use]
56 pub fn participant_count(&self) -> usize {
57 self.participants.len()
58 }
59
60 pub fn end(&mut self) {
62 self.status = SessionStatus::Ended;
63 }
64}
65
66#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
68#[serde(rename_all = "camelCase")]
69pub struct Participant {
70 pub author: Collaborator,
72
73 pub joined: DateTime<Utc>,
75
76 #[serde(default, skip_serializing_if = "Option::is_none")]
78 pub cursor: Option<CursorPosition>,
79
80 #[serde(default, skip_serializing_if = "Option::is_none")]
82 pub color: Option<String>,
83
84 #[serde(default, skip_serializing_if = "Option::is_none")]
86 pub selection: Option<Selection>,
87}
88
89impl Participant {
90 #[must_use]
92 pub fn new(author: Collaborator) -> Self {
93 Self {
94 author,
95 joined: Utc::now(),
96 cursor: None,
97 color: None,
98 selection: None,
99 }
100 }
101
102 #[must_use]
104 pub fn with_cursor(mut self, cursor: CursorPosition) -> Self {
105 self.cursor = Some(cursor);
106 self
107 }
108
109 #[must_use]
111 pub fn with_color(mut self, color: impl Into<String>) -> Self {
112 self.color = Some(color.into());
113 self
114 }
115}
116
117#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
119#[serde(rename_all = "camelCase")]
120pub struct CursorPosition {
121 pub block_ref: String,
123
124 pub offset: usize,
126}
127
128impl CursorPosition {
129 #[must_use]
131 pub fn new(block_ref: impl Into<String>, offset: usize) -> Self {
132 Self {
133 block_ref: block_ref.into(),
134 offset,
135 }
136 }
137}
138
139#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
141#[serde(rename_all = "camelCase")]
142pub struct Selection {
143 pub start: CursorPosition,
145
146 pub end: CursorPosition,
148}
149
150impl Selection {
151 #[must_use]
153 pub fn new(start: CursorPosition, end: CursorPosition) -> Self {
154 Self { start, end }
155 }
156
157 #[must_use]
159 pub fn within_block(
160 block_ref: impl Into<String>,
161 start_offset: usize,
162 end_offset: usize,
163 ) -> Self {
164 let block_ref = block_ref.into();
165 Self {
166 start: CursorPosition::new(block_ref.clone(), start_offset),
167 end: CursorPosition::new(block_ref, end_offset),
168 }
169 }
170}
171
172#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
174#[serde(rename_all = "lowercase")]
175pub enum SessionStatus {
176 #[default]
178 Active,
179 Paused,
181 Ended,
183}