1use crate::comments::{Thread, ThreadItem, UserSummary};
2use crate::events::Event;
3use crate::labels::LabelValidationError;
4use crate::notebooks::operations::Operation;
5use base64uuid::Base64Uuid;
6#[cfg(feature = "fp-bindgen")]
7use fp_bindgen::prelude::*;
8use serde::{Deserialize, Serialize};
9use std::cmp::Ordering;
10use std::fmt::Debug;
11use time::OffsetDateTime;
12use typed_builder::TypedBuilder;
13
14#[derive(Clone, Debug, Deserialize, Serialize)]
16#[cfg_attr(
17 feature = "fp-bindgen",
18 derive(Serializable),
19 fp(rust_module = "fiberplane_models::realtime")
20)]
21#[non_exhaustive]
22#[serde(tag = "type", rename_all = "snake_case")]
23pub enum ClientRealtimeMessage {
24 Authenticate(AuthenticateMessage),
26
27 Subscribe(SubscribeMessage),
29
30 Unsubscribe(UnsubscribeMessage),
32
33 ApplyOperation(Box<ApplyOperationMessage>),
35
36 ApplyOperationBatch(Box<ApplyOperationBatchMessage>),
38
39 DebugRequest(DebugRequestMessage),
41
42 FocusInfo(FocusInfoMessage),
43
44 UserTypingComment(UserTypingCommentClientMessage),
46
47 SubscribeWorkspace(SubscribeWorkspaceMessage),
49
50 UnsubscribeWorkspace(UnsubscribeWorkspaceMessage),
52}
53
54impl ClientRealtimeMessage {
55 pub fn op_id(&self) -> &Option<String> {
56 use ClientRealtimeMessage::*;
57 match self {
58 Authenticate(msg) => &msg.op_id,
59 Subscribe(msg) => &msg.op_id,
60 Unsubscribe(msg) => &msg.op_id,
61 ApplyOperation(msg) => &msg.op_id,
62 ApplyOperationBatch(msg) => &msg.op_id,
63 DebugRequest(msg) => &msg.op_id,
64 FocusInfo(msg) => &msg.op_id,
65 UserTypingComment(msg) => &msg.op_id,
66 SubscribeWorkspace(msg) => &msg.op_id,
67 UnsubscribeWorkspace(msg) => &msg.op_id,
68 }
69 }
70}
71
72#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
74#[cfg_attr(
75 feature = "fp-bindgen",
76 derive(Serializable),
77 fp(rust_module = "fiberplane_models::realtime")
78)]
79#[non_exhaustive]
80#[serde(tag = "type", rename_all = "snake_case")]
81pub enum ServerRealtimeMessage {
82 ApplyOperation(Box<ApplyOperationMessage>),
84
85 Ack(AckMessage),
88
89 Err(ErrMessage),
92
93 DebugResponse(DebugResponseMessage),
96
97 EventAdded(EventAddedMessage),
99
100 EventUpdated(EventUpdatedMessage),
102
103 EventDeleted(EventDeletedMessage),
105
106 Mention(MentionMessage),
109
110 Rejected(RejectedMessage),
113
114 SubscriberAdded(SubscriberAddedMessage),
116
117 SubscriberRemoved(SubscriberRemovedMessage),
119
120 SubscriberChangedFocus(SubscriberChangedFocusMessage),
121
122 ThreadAdded(ThreadAddedMessage),
124
125 ThreadItemAdded(ThreadItemAddedMessage),
127
128 ThreadItemUpdated(ThreadItemUpdatedMessage),
130
131 ThreadDeleted(ThreadDeletedMessage),
133
134 UserTypingComment(UserTypingCommentServerMessage),
136}
137
138#[derive(Clone, Deserialize, PartialEq, Eq, Serialize, TypedBuilder)]
139#[cfg_attr(
140 feature = "fp-bindgen",
141 derive(Serializable),
142 fp(rust_module = "fiberplane_models::realtime")
143)]
144#[non_exhaustive]
145#[serde(rename_all = "camelCase")]
146pub struct AuthenticateMessage {
147 #[builder(setter(into))]
149 pub token: String,
150
151 #[builder(default, setter(into, strip_option))]
153 #[serde(skip_serializing_if = "Option::is_none")]
154 pub op_id: Option<String>,
155}
156
157impl Debug for AuthenticateMessage {
158 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
159 f.debug_struct("AuthenticateMessage")
160 .field("token", &"[REDACTED]")
161 .field("op_id", &self.op_id)
162 .finish()
163 }
164}
165
166#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, TypedBuilder)]
167#[cfg_attr(
168 feature = "fp-bindgen",
169 derive(Serializable),
170 fp(rust_module = "fiberplane_models::realtime")
171)]
172#[non_exhaustive]
173#[serde(rename_all = "camelCase")]
174pub struct SubscribeMessage {
175 #[builder(setter(into))]
177 pub notebook_id: String,
178
179 #[builder(default, setter(into))]
183 #[serde(skip_serializing_if = "Option::is_none")]
184 pub revision: Option<u32>,
185
186 #[builder(default, setter(into, strip_option))]
188 #[serde(skip_serializing_if = "Option::is_none")]
189 pub op_id: Option<String>,
190}
191
192#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, TypedBuilder)]
193#[cfg_attr(
194 feature = "fp-bindgen",
195 derive(Serializable),
196 fp(rust_module = "fiberplane_models::realtime")
197)]
198#[non_exhaustive]
199#[serde(rename_all = "camelCase")]
200pub struct UnsubscribeMessage {
201 #[builder(setter(into))]
203 pub notebook_id: String,
204
205 #[builder(default, setter(into, strip_option))]
207 #[serde(skip_serializing_if = "Option::is_none")]
208 pub op_id: Option<String>,
209}
210
211#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, TypedBuilder)]
212#[cfg_attr(
213 feature = "fp-bindgen",
214 derive(Serializable),
215 fp(rust_module = "fiberplane_models::realtime")
216)]
217#[non_exhaustive]
218#[serde(rename_all = "camelCase")]
219pub struct ApplyOperationMessage {
220 #[builder(setter(into))]
222 pub notebook_id: String,
223
224 pub operation: Operation,
226
227 pub revision: u32,
230
231 #[builder(default, setter(into))]
233 #[serde(skip_serializing_if = "Option::is_none")]
234 pub op_id: Option<String>,
235}
236
237impl ApplyOperationMessage {
238 pub fn new(
239 notebook_id: String,
240 operation: Operation,
241 revision: u32,
242 op_id: Option<String>,
243 ) -> Self {
244 Self {
245 notebook_id,
246 operation,
247 revision,
248 op_id,
249 }
250 }
251}
252
253#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, TypedBuilder)]
254#[cfg_attr(
255 feature = "fp-bindgen",
256 derive(Serializable),
257 fp(rust_module = "fiberplane_models::realtime")
258)]
259#[non_exhaustive]
260#[serde(rename_all = "camelCase")]
261pub struct ApplyOperationBatchMessage {
262 #[builder(setter(into))]
264 pub notebook_id: String,
265
266 #[builder(default)]
268 pub operations: Vec<Operation>,
269
270 pub revision: u32,
273
274 #[builder(default, setter(into, strip_option))]
276 #[serde(skip_serializing_if = "Option::is_none")]
277 pub op_id: Option<String>,
278}
279
280impl ApplyOperationBatchMessage {
281 pub fn new(
282 notebook_id: String,
283 operations: Vec<Operation>,
284 revision: u32,
285 op_id: Option<String>,
286 ) -> Self {
287 Self {
288 notebook_id,
289 operations,
290 revision,
291 op_id,
292 }
293 }
294}
295
296#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, TypedBuilder)]
297#[cfg_attr(
298 feature = "fp-bindgen",
299 derive(Serializable),
300 fp(rust_module = "fiberplane_models::realtime")
301)]
302#[non_exhaustive]
303#[serde(rename_all = "camelCase")]
304pub struct AckMessage {
305 pub op_id: String,
307}
308
309impl AckMessage {
310 pub fn new(op_id: String) -> Self {
311 Self { op_id }
312 }
313}
314
315#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, TypedBuilder)]
316#[cfg_attr(
317 feature = "fp-bindgen",
318 derive(Serializable),
319 fp(rust_module = "fiberplane_models::realtime")
320)]
321#[non_exhaustive]
322#[serde(rename_all = "camelCase")]
323pub struct ErrMessage {
324 #[builder(setter(into))]
326 pub error_message: String,
327
328 #[builder(default, setter(into))]
330 #[serde(skip_serializing_if = "Option::is_none")]
331 pub op_id: Option<String>,
332}
333
334#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, TypedBuilder)]
335#[cfg_attr(
336 feature = "fp-bindgen",
337 derive(Serializable),
338 fp(rust_module = "fiberplane_models::realtime")
339)]
340#[non_exhaustive]
341#[serde(rename_all = "camelCase")]
342pub struct DebugRequestMessage {
343 #[serde(skip_serializing_if = "Option::is_none")]
345 pub op_id: Option<String>,
346}
347
348#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, TypedBuilder)]
349#[cfg_attr(
350 feature = "fp-bindgen",
351 derive(Serializable),
352 fp(rust_module = "fiberplane_models::realtime")
353)]
354#[non_exhaustive]
355#[serde(rename_all = "camelCase")]
356pub struct DebugResponseMessage {
357 pub sid: String,
359
360 #[builder(default)]
362 pub subscribed_notebooks: Vec<String>,
363
364 #[builder(default, setter(into))]
366 #[serde(skip_serializing_if = "Option::is_none")]
367 pub op_id: Option<String>,
368}
369
370#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, TypedBuilder)]
371#[cfg_attr(
372 feature = "fp-bindgen",
373 derive(Serializable),
374 fp(rust_module = "fiberplane_models::realtime")
375)]
376#[non_exhaustive]
377#[serde(rename_all = "camelCase")]
378pub struct EventAddedMessage {
379 pub workspace_id: Base64Uuid,
381
382 pub event: Event,
384}
385
386#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, TypedBuilder)]
387#[cfg_attr(
388 feature = "fp-bindgen",
389 derive(Serializable),
390 fp(rust_module = "fiberplane_models::realtime")
391)]
392#[non_exhaustive]
393#[serde(rename_all = "camelCase")]
394pub struct EventUpdatedMessage {
395 pub workspace_id: Base64Uuid,
397
398 pub event: Event,
400}
401
402#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, TypedBuilder)]
403#[cfg_attr(
404 feature = "fp-bindgen",
405 derive(Serializable),
406 fp(rust_module = "fiberplane_models::realtime")
407)]
408#[non_exhaustive]
409#[serde(rename_all = "camelCase")]
410pub struct EventDeletedMessage {
411 pub workspace_id: Base64Uuid,
413
414 pub event_id: String,
416}
417
418#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, TypedBuilder)]
419#[cfg_attr(
420 feature = "fp-bindgen",
421 derive(Serializable),
422 fp(rust_module = "fiberplane_models::realtime")
423)]
424#[non_exhaustive]
425#[serde(rename_all = "camelCase")]
426pub struct MentionMessage {
427 pub notebook_id: String,
429
430 pub cell_id: String,
432
433 pub mentioned_by: MentionedBy,
435}
436
437#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize, TypedBuilder)]
438#[cfg_attr(
439 feature = "fp-bindgen",
440 derive(Serializable),
441 fp(rust_module = "fiberplane_models::realtime")
442)]
443#[non_exhaustive]
444#[serde(rename_all = "camelCase")]
445pub struct MentionedBy {
446 pub name: String,
447}
448
449#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, TypedBuilder)]
451#[cfg_attr(
452 feature = "fp-bindgen",
453 derive(Serializable),
454 fp(rust_module = "fiberplane_models::realtime")
455)]
456#[non_exhaustive]
457#[serde(rename_all = "camelCase")]
458pub struct RejectedMessage {
459 pub reason: Box<RejectReason>,
461
462 #[serde(skip_serializing_if = "Option::is_none")]
464 pub op_id: Option<String>,
465}
466
467impl RejectedMessage {
468 pub fn new(reason: RejectReason, op_id: Option<String>) -> Self {
469 Self {
470 reason: Box::new(reason),
471 op_id,
472 }
473 }
474}
475
476#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
478#[cfg_attr(
479 feature = "fp-bindgen",
480 derive(Serializable),
481 fp(rust_module = "fiberplane_models::realtime")
482)]
483#[non_exhaustive]
484#[serde(tag = "type", rename_all = "snake_case")]
485pub enum RejectReason {
486 CellIndexOutOfBounds,
488
489 #[serde(rename_all = "camelCase")]
491 CellNotFound { cell_id: String },
492
493 #[serde(rename_all = "camelCase")]
495 DuplicateCellId { cell_id: String },
496
497 DuplicateLabel(DuplicateLabelRejectReason),
499
500 #[serde(rename_all = "camelCase")]
502 FailedPrecondition { message: String },
503
504 InvalidLabel(InvalidLabelRejectReason),
506
507 InconsistentState,
509
510 #[serde(rename_all = "camelCase")]
512 NoTextCell { cell_id: String },
513
514 Outdated(OutdatedRejectReason),
517}
518
519#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, TypedBuilder)]
520#[cfg_attr(
521 feature = "fp-bindgen",
522 derive(Serializable),
523 fp(rust_module = "fiberplane_models::realtime")
524)]
525#[non_exhaustive]
526#[serde(rename_all = "camelCase")]
527pub struct OutdatedRejectReason {
528 pub current_revision: u32,
530}
531
532#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, TypedBuilder)]
533#[cfg_attr(
534 feature = "fp-bindgen",
535 derive(Serializable),
536 fp(rust_module = "fiberplane_models::realtime")
537)]
538#[non_exhaustive]
539#[serde(rename_all = "camelCase")]
540pub struct InvalidLabelRejectReason {
541 pub key: String,
543
544 pub validation_error: LabelValidationError,
546}
547
548#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, TypedBuilder)]
549#[cfg_attr(
550 feature = "fp-bindgen",
551 derive(Serializable),
552 fp(rust_module = "fiberplane_models::realtime")
553)]
554#[non_exhaustive]
555#[serde(rename_all = "camelCase")]
556pub struct DuplicateLabelRejectReason {
557 pub key: String,
559}
560
561#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, TypedBuilder)]
562#[cfg_attr(
563 feature = "fp-bindgen",
564 derive(Serializable),
565 fp(rust_module = "fiberplane_models::realtime")
566)]
567#[non_exhaustive]
568#[serde(rename_all = "camelCase")]
569pub struct SubscriberAddedMessage {
570 #[builder(setter(into))]
572 pub notebook_id: String,
573
574 #[builder(setter(into))]
579 pub session_id: String,
580
581 #[builder(setter(into))]
583 #[serde(with = "time::serde::rfc3339")]
584 pub created_at: OffsetDateTime,
585
586 #[builder(setter(into))]
588 #[serde(with = "time::serde::rfc3339")]
589 pub updated_at: OffsetDateTime,
590
591 pub user: User,
593
594 #[builder(default)]
596 #[serde(default)]
597 pub focus: NotebookFocus,
598}
599
600#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, TypedBuilder)]
601#[cfg_attr(
602 feature = "fp-bindgen",
603 derive(Serializable),
604 fp(rust_module = "fiberplane_models::realtime")
605)]
606#[non_exhaustive]
607#[serde(rename_all = "camelCase")]
608pub struct SubscriberRemovedMessage {
609 pub notebook_id: String,
611
612 pub session_id: String,
614}
615
616#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, TypedBuilder)]
617#[cfg_attr(
618 feature = "fp-bindgen",
619 derive(Serializable),
620 fp(rust_module = "fiberplane_models::realtime")
621)]
622#[non_exhaustive]
623#[serde(rename_all = "camelCase")]
624pub struct User {
625 #[builder(setter(into))]
628 pub id: String,
629
630 #[builder(setter(into))]
632 pub name: String,
633}
634
635#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, TypedBuilder)]
636#[cfg_attr(
637 feature = "fp-bindgen",
638 derive(Serializable),
639 fp(rust_module = "fiberplane_models::realtime")
640)]
641#[non_exhaustive]
642#[serde(rename_all = "camelCase")]
643pub struct FocusInfoMessage {
644 #[builder(setter(into))]
646 pub notebook_id: String,
647
648 #[builder(default)]
650 #[serde(default)]
651 pub focus: NotebookFocus,
652
653 #[builder(default, setter(into, strip_option))]
655 #[serde(skip_serializing_if = "Option::is_none")]
656 pub op_id: Option<String>,
657}
658
659#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, TypedBuilder)]
660#[cfg_attr(
661 feature = "fp-bindgen",
662 derive(Serializable),
663 fp(rust_module = "fiberplane_models::realtime")
664)]
665#[non_exhaustive]
666#[serde(rename_all = "camelCase")]
667pub struct UserTypingCommentClientMessage {
668 #[builder(setter(into))]
669 pub notebook_id: Base64Uuid,
670 #[builder(setter(into))]
671 pub thread_id: Base64Uuid,
672
673 #[builder(default, setter(into, strip_option))]
675 #[serde(skip_serializing_if = "Option::is_none")]
676 pub op_id: Option<String>,
677}
678
679#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, TypedBuilder)]
680#[cfg_attr(
681 feature = "fp-bindgen",
682 derive(Serializable),
683 fp(rust_module = "fiberplane_models::realtime")
684)]
685#[non_exhaustive]
686#[serde(rename_all = "camelCase")]
687pub struct SubscribeWorkspaceMessage {
688 pub workspace_id: Base64Uuid,
690
691 #[builder(default, setter(into, strip_option))]
693 #[serde(skip_serializing_if = "Option::is_none")]
694 pub op_id: Option<String>,
695}
696
697#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, TypedBuilder)]
698#[cfg_attr(
699 feature = "fp-bindgen",
700 derive(Serializable),
701 fp(rust_module = "fiberplane_models::realtime")
702)]
703#[non_exhaustive]
704#[serde(rename_all = "camelCase")]
705pub struct UnsubscribeWorkspaceMessage {
706 pub workspace_id: Base64Uuid,
708
709 #[serde(skip_serializing_if = "Option::is_none")]
711 pub op_id: Option<String>,
712}
713
714#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, TypedBuilder)]
715#[cfg_attr(
716 feature = "fp-bindgen",
717 derive(Serializable),
718 fp(rust_module = "fiberplane_models::realtime")
719)]
720#[non_exhaustive]
721#[serde(rename_all = "camelCase")]
722pub struct SubscriberChangedFocusMessage {
723 pub session_id: String,
725
726 pub notebook_id: String,
728
729 #[serde(default)]
731 pub focus: NotebookFocus,
732}
733
734#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, TypedBuilder)]
740#[cfg_attr(
741 feature = "fp-bindgen",
742 derive(Serializable),
743 fp(rust_module = "fiberplane_models::realtime")
744)]
745#[non_exhaustive]
746#[serde(rename_all = "camelCase")]
747pub struct FocusPosition {
748 #[builder(setter(into))]
753 pub cell_id: String,
754
755 #[serde(skip_serializing_if = "Option::is_none")]
763 pub field: Option<String>,
764
765 #[serde(skip_serializing_if = "Option::is_none")]
768 pub offset: Option<u32>,
769}
770
771#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
773#[cfg_attr(
774 feature = "fp-bindgen",
775 derive(Serializable),
776 fp(rust_module = "fiberplane_models::realtime")
777)]
778#[non_exhaustive]
779#[serde(rename_all = "snake_case", tag = "type")]
780pub enum NotebookFocus {
781 None,
783 Collapsed(FocusPosition),
786 Selection {
789 anchor: FocusPosition,
790 focus: FocusPosition,
791 },
792}
793
794impl NotebookFocus {
795 pub fn anchor_cell_id(&self) -> Option<&str> {
796 match self {
797 Self::None => None,
798 Self::Collapsed(collapsed) => Some(&collapsed.cell_id),
799 Self::Selection { anchor, .. } => Some(&anchor.cell_id),
800 }
801 }
802
803 pub fn anchor_cell_index(&self, cell_ids: &[&str]) -> Option<usize> {
804 cell_ids
805 .iter()
806 .position(|cell_id| Some(*cell_id) == self.anchor_cell_id())
807 }
808
809 pub fn anchor_field(&self) -> Option<&str> {
810 match self {
811 Self::None => None,
812 Self::Collapsed(collapsed) => collapsed.field.as_deref(),
813 Self::Selection { anchor, .. } => anchor.field.as_deref(),
814 }
815 }
816
817 pub fn anchor_offset(&self) -> u32 {
818 match self {
819 Self::None => 0,
820 Self::Collapsed(position) => position.offset.unwrap_or_default(),
821 Self::Selection { anchor, .. } => anchor.offset.unwrap_or_default(),
822 }
823 }
824
825 pub fn anchor_position(&self) -> Option<&FocusPosition> {
826 match self {
827 Self::None => None,
828 Self::Collapsed(position) => Some(position),
829 Self::Selection { anchor, .. } => Some(anchor),
830 }
831 }
832
833 pub fn end_cell_id(&self, cell_ids: &[&str]) -> Option<&str> {
834 match self {
835 Self::None => None,
836 Self::Collapsed(position) => Some(&position.cell_id),
837 Self::Selection { anchor, focus } => {
838 let anchor_cell_index = self.anchor_cell_index(cell_ids).unwrap_or_default();
839 let focus_cell_index = self.focus_cell_index(cell_ids).unwrap_or_default();
840 if anchor_cell_index > focus_cell_index {
841 Some(&anchor.cell_id)
842 } else {
843 Some(&focus.cell_id)
844 }
845 }
846 }
847 }
848
849 pub fn end_offset(&self, cell_ids: &[&str]) -> u32 {
850 match self {
851 Self::None => 0,
852 Self::Collapsed(position) => position.offset.unwrap_or_default(),
853 Self::Selection { anchor, focus } => {
854 let anchor_cell_index = self.anchor_cell_index(cell_ids).unwrap_or_default();
855 let anchor_offset = anchor.offset.unwrap_or_default();
856 let focus_cell_index = self.focus_cell_index(cell_ids).unwrap_or_default();
857 let focus_offset = focus.offset.unwrap_or_default();
858 match anchor_cell_index.cmp(&focus_cell_index) {
859 Ordering::Greater => anchor_offset,
860 Ordering::Equal => std::cmp::max(anchor_offset, focus_offset),
861 Ordering::Less => focus_offset,
862 }
863 }
864 }
865 }
866
867 pub fn focus_cell_id(&self) -> Option<&str> {
868 match self {
869 Self::None => None,
870 Self::Collapsed(collapsed) => Some(&collapsed.cell_id),
871 Self::Selection { focus, .. } => Some(&focus.cell_id),
872 }
873 }
874
875 pub fn focus_cell_index(&self, cell_ids: &[&str]) -> Option<usize> {
876 cell_ids
877 .iter()
878 .position(|cell_id| Some(*cell_id) == self.focus_cell_id())
879 }
880
881 pub fn focus_field(&self) -> Option<&str> {
882 match self {
883 Self::None => None,
884 Self::Collapsed(collapsed) => collapsed.field.as_deref(),
885 Self::Selection { focus, .. } => focus.field.as_deref(),
886 }
887 }
888
889 pub fn focus_offset(&self) -> u32 {
890 match self {
891 Self::None => 0,
892 Self::Collapsed(position) => position.offset.unwrap_or_default(),
893 Self::Selection { focus, .. } => focus.offset.unwrap_or_default(),
894 }
895 }
896
897 pub fn focus_position(&self) -> Option<&FocusPosition> {
898 match self {
899 Self::None => None,
900 Self::Collapsed(position) => Some(position),
901 Self::Selection { focus, .. } => Some(focus),
902 }
903 }
904
905 pub fn has_selection(&self) -> bool {
906 !self.is_collapsed()
907 }
908
909 pub fn is_collapsed(&self) -> bool {
912 match self {
913 Self::None | Self::Collapsed(_) => true,
914 Self::Selection { focus, anchor } => *focus == *anchor,
915 }
916 }
917
918 pub fn is_none(&self) -> bool {
919 matches!(self, Self::None)
920 }
921
922 pub fn start_cell_id(&self, cell_ids: &[&str]) -> Option<&str> {
923 match self {
924 Self::None => None,
925 Self::Collapsed(position) => Some(&position.cell_id),
926 Self::Selection { anchor, focus } => {
927 if self.anchor_cell_index(cell_ids).unwrap_or_default()
928 < self.focus_cell_index(cell_ids).unwrap_or_default()
929 {
930 Some(&anchor.cell_id)
931 } else {
932 Some(&focus.cell_id)
933 }
934 }
935 }
936 }
937
938 pub fn start_offset(&self, cell_ids: &[&str]) -> u32 {
939 match self {
940 Self::None => 0,
941 Self::Collapsed(position) => position.offset.unwrap_or_default(),
942 Self::Selection { anchor, focus } => {
943 let anchor_cell_index = self.anchor_cell_index(cell_ids).unwrap_or_default();
944 let anchor_offset = anchor.offset.unwrap_or_default();
945 let focus_cell_index = self.focus_cell_index(cell_ids).unwrap_or_default();
946 let focus_offset = focus.offset.unwrap_or_default();
947 match anchor_cell_index.cmp(&focus_cell_index) {
948 Ordering::Less => anchor_offset,
949 Ordering::Equal => std::cmp::min(anchor_offset, focus_offset),
950 Ordering::Greater => focus_offset,
951 }
952 }
953 }
954 }
955}
956
957impl Default for NotebookFocus {
958 fn default() -> Self {
959 Self::None
960 }
961}
962
963#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, TypedBuilder)]
964#[cfg_attr(
965 feature = "fp-bindgen",
966 derive(Serializable),
967 fp(rust_module = "fiberplane_models::realtime")
968)]
969#[non_exhaustive]
970#[serde(rename_all = "camelCase")]
971pub struct ThreadAddedMessage {
972 pub notebook_id: String,
973 pub thread: Thread,
974}
975
976#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, TypedBuilder)]
977#[cfg_attr(
978 feature = "fp-bindgen",
979 derive(Serializable),
980 fp(rust_module = "fiberplane_models::realtime")
981)]
982#[non_exhaustive]
983#[serde(rename_all = "camelCase")]
984pub struct ThreadItemAddedMessage {
985 pub notebook_id: String,
986 pub thread_id: String,
987 pub thread_item: ThreadItem,
988}
989
990#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, TypedBuilder)]
991#[cfg_attr(
992 feature = "fp-bindgen",
993 derive(Serializable),
994 fp(rust_module = "fiberplane_models::realtime")
995)]
996#[non_exhaustive]
997#[serde(rename_all = "camelCase")]
998pub struct ThreadItemUpdatedMessage {
999 pub notebook_id: String,
1000 pub thread_id: String,
1001 pub thread_item: ThreadItem,
1002}
1003
1004#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, TypedBuilder)]
1005#[cfg_attr(
1006 feature = "fp-bindgen",
1007 derive(Serializable),
1008 fp(rust_module = "fiberplane_models::realtime")
1009)]
1010#[non_exhaustive]
1011#[serde(rename_all = "camelCase")]
1012pub struct ThreadDeletedMessage {
1013 pub notebook_id: String,
1014 pub thread_id: String,
1015}
1016
1017#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, TypedBuilder)]
1018#[cfg_attr(
1019 feature = "fp-bindgen",
1020 derive(Serializable),
1021 fp(rust_module = "fiberplane_models::realtime")
1022)]
1023#[non_exhaustive]
1024#[serde(rename_all = "camelCase")]
1025pub struct UserTypingCommentServerMessage {
1026 pub notebook_id: String,
1027 pub thread_id: String,
1028 pub user: UserSummary,
1029 #[serde(with = "time::serde::rfc3339")]
1030 pub updated_at: OffsetDateTime,
1031}
1032
1033#[cfg(test)]
1034mod tests {
1035 use super::*;
1036
1037 #[test]
1038 fn serialize_reject_reason() {
1039 let reason = OutdatedRejectReason {
1040 current_revision: 1,
1041 };
1042 let reason = RejectReason::Outdated(reason);
1043 let result = serde_json::to_string(&reason);
1044 if let Err(err) = result {
1045 panic!("Unexpected error occurred: {err:?}");
1046 }
1047 }
1048}