Make notification db representation more flexible

This commit is contained in:
Max Brunsfeld 2023-10-12 17:17:45 -07:00
parent fed3ffb681
commit 3241128840
17 changed files with 197 additions and 175 deletions

View file

@ -17,6 +17,7 @@ clock = { path = "../clock" }
collections = { path = "../collections" }
gpui = { path = "../gpui", optional = true }
util = { path = "../util" }
anyhow.workspace = true
async-lock = "2.4"
async-tungstenite = "0.16"
@ -27,6 +28,7 @@ prost.workspace = true
rand.workspace = true
rsa = "0.4"
serde.workspace = true
serde_json.workspace = true
serde_derive.workspace = true
smol-timeout = "0.6"
strum.workspace = true

View file

@ -1571,10 +1571,9 @@ message AddNotifications {
message Notification {
uint64 id = 1;
uint32 kind = 2;
uint64 timestamp = 3;
bool is_read = 4;
optional uint64 entity_id_1 = 5;
optional uint64 entity_id_2 = 6;
optional uint64 entity_id_3 = 7;
uint64 timestamp = 2;
bool is_read = 3;
string kind = 4;
string content = 5;
optional uint64 actor_id = 6;
}

View file

@ -1,110 +1,94 @@
use strum::{Display, EnumIter, EnumString, IntoEnumIterator};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::borrow::Cow;
use strum::{EnumVariantNames, IntoStaticStr, VariantNames as _};
// An integer indicating a type of notification. The variants' numerical
// values are stored in the database, so they should never be removed
// or changed.
#[repr(i32)]
#[derive(Copy, Clone, Debug, EnumIter, EnumString, Display)]
pub enum NotificationKind {
ContactRequest = 0,
ContactRequestAccepted = 1,
ChannelInvitation = 2,
ChannelMessageMention = 3,
}
const KIND: &'static str = "kind";
const ACTOR_ID: &'static str = "actor_id";
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq, EnumVariantNames, IntoStaticStr, Serialize, Deserialize)]
#[serde(tag = "kind")]
pub enum Notification {
ContactRequest {
requester_id: u64,
actor_id: u64,
},
ContactRequestAccepted {
contact_id: u64,
actor_id: u64,
},
ChannelInvitation {
inviter_id: u64,
actor_id: u64,
channel_id: u64,
},
ChannelMessageMention {
sender_id: u64,
actor_id: u64,
channel_id: u64,
message_id: u64,
},
}
#[derive(Debug)]
pub struct AnyNotification {
pub kind: Cow<'static, str>,
pub actor_id: Option<u64>,
pub content: String,
}
impl Notification {
/// Load this notification from its generic representation, which is
/// used to represent it in the database, and in the wire protocol.
///
/// The order in which a given notification type's fields are listed must
/// match the order they're listed in the `to_parts` method, and it must
/// not change, because they're stored in that order in the database.
pub fn from_parts(kind: NotificationKind, entity_ids: [Option<u64>; 3]) -> Option<Self> {
use NotificationKind::*;
Some(match kind {
ContactRequest => Self::ContactRequest {
requester_id: entity_ids[0]?,
},
ContactRequestAccepted => Self::ContactRequest {
requester_id: entity_ids[0]?,
},
ChannelInvitation => Self::ChannelInvitation {
inviter_id: entity_ids[0]?,
channel_id: entity_ids[1]?,
},
ChannelMessageMention => Self::ChannelMessageMention {
sender_id: entity_ids[0]?,
channel_id: entity_ids[1]?,
message_id: entity_ids[2]?,
},
})
}
/// Convert this notification into its generic representation, which is
/// used to represent it in the database, and in the wire protocol.
///
/// The order in which a given notification type's fields are listed must
/// match the order they're listed in the `from_parts` method, and it must
/// not change, because they're stored in that order in the database.
pub fn to_parts(&self) -> (NotificationKind, [Option<u64>; 3]) {
use NotificationKind::*;
match self {
Self::ContactRequest { requester_id } => {
(ContactRequest, [Some(*requester_id), None, None])
}
Self::ContactRequestAccepted { contact_id } => {
(ContactRequest, [Some(*contact_id), None, None])
}
Self::ChannelInvitation {
inviter_id,
channel_id,
} => (
ChannelInvitation,
[Some(*inviter_id), Some(*channel_id), None],
),
Self::ChannelMessageMention {
sender_id,
channel_id,
message_id,
} => (
ChannelMessageMention,
[Some(*sender_id), Some(*channel_id), Some(*message_id)],
),
pub fn to_any(&self) -> AnyNotification {
let kind: &'static str = self.into();
let mut value = serde_json::to_value(self).unwrap();
let mut actor_id = None;
if let Some(value) = value.as_object_mut() {
value.remove("kind");
actor_id = value
.remove("actor_id")
.and_then(|value| Some(value.as_i64()? as u64));
}
AnyNotification {
kind: Cow::Borrowed(kind),
actor_id,
content: serde_json::to_string(&value).unwrap(),
}
}
}
impl NotificationKind {
pub fn all() -> impl Iterator<Item = Self> {
Self::iter()
pub fn from_any(notification: &AnyNotification) -> Option<Self> {
let mut value = serde_json::from_str::<Value>(&notification.content).ok()?;
let object = value.as_object_mut()?;
object.insert(KIND.into(), notification.kind.to_string().into());
if let Some(actor_id) = notification.actor_id {
object.insert(ACTOR_ID.into(), actor_id.into());
}
serde_json::from_value(value).ok()
}
pub fn from_i32(i: i32) -> Option<Self> {
Self::iter().find(|kind| *kind as i32 == i)
pub fn all_kinds() -> &'static [&'static str] {
Self::VARIANTS
}
}
#[test]
fn test_notification() {
// Notifications can be serialized and deserialized.
for notification in [
Notification::ContactRequest { actor_id: 1 },
Notification::ContactRequestAccepted { actor_id: 2 },
Notification::ChannelInvitation {
actor_id: 0,
channel_id: 100,
},
Notification::ChannelMessageMention {
actor_id: 200,
channel_id: 30,
message_id: 1,
},
] {
let serialized = notification.to_any();
let deserialized = Notification::from_any(&serialized).unwrap();
assert_eq!(deserialized, notification);
}
// When notifications are serialized, redundant data is not stored
// in the JSON.
let notification = Notification::ContactRequest { actor_id: 1 };
assert_eq!(notification.to_any().content, "{}");
}