welcome dm
This commit is contained in:
+27
-1
@@ -1,4 +1,4 @@
|
||||
use spacetimedb::{ReducerContext, Table};
|
||||
use spacetimedb::{Identity, ReducerContext, Table};
|
||||
|
||||
mod reducers;
|
||||
mod tables;
|
||||
@@ -10,8 +10,28 @@ pub use tables::*;
|
||||
pub use utils::*;
|
||||
pub use views::*;
|
||||
|
||||
pub const SYSTEM_IDENTITY: &str = "0000000000000000000000000000000000000000000000000000000000000000";
|
||||
|
||||
#[spacetimedb::reducer(init)]
|
||||
pub fn init(ctx: &ReducerContext) {
|
||||
let system_identity = Identity::from_hex(SYSTEM_IDENTITY).unwrap();
|
||||
|
||||
// Create system user if not exists
|
||||
if ctx.db.user().identity().find(system_identity).is_none() {
|
||||
ctx.db.user().insert(User {
|
||||
identity: system_identity,
|
||||
name: Some("Zep".to_string()),
|
||||
online: true,
|
||||
issuer: None,
|
||||
subject: None,
|
||||
anonymous: false,
|
||||
avatar_id: None,
|
||||
banner_id: None,
|
||||
biography: Some("I am the Zep system assistant.".to_string()),
|
||||
status: Some("Online".to_string()),
|
||||
});
|
||||
}
|
||||
|
||||
if ctx
|
||||
.db
|
||||
.system_configuration()
|
||||
@@ -86,6 +106,12 @@ pub fn on_connect(ctx: &ReducerContext) {
|
||||
|
||||
// Minimal auto-join
|
||||
auto_join_community_server(&ctx.db, ctx.sender());
|
||||
|
||||
// System Welcome DM
|
||||
let system_identity = Identity::from_hex(SYSTEM_IDENTITY).unwrap();
|
||||
let channel_id = internal_open_direct_message(&ctx.db, system_identity, ctx.sender());
|
||||
let welcome_text = "Welcome to Zep! We're glad to have you here.\n\nZep is a decentralized, private, and fast chat service built on SpacetimeDB. You can join servers, create channels, and message friends directly—all with the security and performance of a modern relational backend.";
|
||||
internal_send_message(&ctx.db, system_identity, channel_id, welcome_text.to_string(), ctx.timestamp);
|
||||
}
|
||||
|
||||
sync_server_member_info(&ctx.db, ctx.sender());
|
||||
|
||||
@@ -1132,46 +1132,7 @@ pub fn open_direct_message(ctx: &ReducerContext, recipient: Identity) {
|
||||
panic!("You cannot open a direct message with yourself");
|
||||
}
|
||||
|
||||
// Check if a DM already exists using indexes
|
||||
let existing = ctx
|
||||
.db
|
||||
.direct_message()
|
||||
.sender()
|
||||
.filter(ctx.sender())
|
||||
.find(|dm| dm.recipient == recipient)
|
||||
.or_else(|| {
|
||||
ctx.db
|
||||
.direct_message()
|
||||
.recipient()
|
||||
.filter(ctx.sender())
|
||||
.find(|dm| dm.sender == recipient)
|
||||
});
|
||||
|
||||
if let Some(mut dm) = existing {
|
||||
if dm.sender == ctx.sender() {
|
||||
dm.is_open_sender = true;
|
||||
} else {
|
||||
dm.is_open_recipient = true;
|
||||
}
|
||||
ctx.db.direct_message().id().update(dm);
|
||||
} else {
|
||||
// Create a new DM channel
|
||||
let chan = ctx.db.channel().insert(Channel {
|
||||
id: 0,
|
||||
server_id: 0,
|
||||
name: "dm".to_string(),
|
||||
kind: ChannelKind::Text,
|
||||
});
|
||||
|
||||
ctx.db.direct_message().insert(DirectMessage {
|
||||
id: 0,
|
||||
channel_id: chan.id,
|
||||
sender: ctx.sender(),
|
||||
recipient,
|
||||
is_open_sender: true,
|
||||
is_open_recipient: true,
|
||||
});
|
||||
}
|
||||
internal_open_direct_message(&ctx.db, ctx.sender(), recipient);
|
||||
}
|
||||
|
||||
#[spacetimedb::reducer]
|
||||
|
||||
@@ -68,6 +68,7 @@ pub struct Channel {
|
||||
}
|
||||
|
||||
#[spacetimedb::table(accessor = direct_message)]
|
||||
#[derive(Clone)]
|
||||
pub struct DirectMessage {
|
||||
#[primary_key]
|
||||
#[auto_inc]
|
||||
|
||||
@@ -328,6 +328,104 @@ pub fn auto_join_community_server(db: &Local, identity: Identity) {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn internal_open_direct_message(db: &Local, sender: Identity, recipient: Identity) -> u64 {
|
||||
// Check if a DM already exists
|
||||
let existing = db
|
||||
.direct_message()
|
||||
.sender()
|
||||
.filter(sender)
|
||||
.find(|dm| dm.recipient == recipient)
|
||||
.or_else(|| {
|
||||
db.direct_message()
|
||||
.recipient()
|
||||
.filter(sender)
|
||||
.find(|dm| dm.sender == recipient)
|
||||
});
|
||||
|
||||
if let Some(mut dm) = existing {
|
||||
if dm.sender == sender {
|
||||
dm.is_open_sender = true;
|
||||
} else {
|
||||
dm.is_open_recipient = true;
|
||||
}
|
||||
db.direct_message().id().update(dm.clone());
|
||||
dm.channel_id
|
||||
} else {
|
||||
// Create a new DM channel
|
||||
let chan = db.channel().insert(Channel {
|
||||
id: 0,
|
||||
server_id: 0,
|
||||
name: "dm".to_string(),
|
||||
kind: ChannelKind::Text,
|
||||
});
|
||||
|
||||
db.direct_message().insert(DirectMessage {
|
||||
id: 0,
|
||||
channel_id: chan.id,
|
||||
sender,
|
||||
recipient,
|
||||
is_open_sender: true,
|
||||
is_open_recipient: true,
|
||||
});
|
||||
chan.id
|
||||
}
|
||||
}
|
||||
|
||||
pub fn internal_send_message(db: &Local, sender: Identity, channel_id: u64, text: String, timestamp: spacetimedb::Timestamp) {
|
||||
let msg = db.message().insert(Message {
|
||||
id: 0,
|
||||
sender,
|
||||
sent: timestamp,
|
||||
text,
|
||||
channel_id,
|
||||
thread_id: None,
|
||||
reactions: Vec::new(),
|
||||
image_ids: Vec::new(),
|
||||
thread_name: None,
|
||||
thread_reply_count: 0,
|
||||
edited: false,
|
||||
});
|
||||
|
||||
let seq_id = get_next_seq_id(db, channel_id);
|
||||
db.channel_message_sequence()
|
||||
.insert(ChannelMessageSequence {
|
||||
message_id: msg.id,
|
||||
channel_id,
|
||||
seq_id,
|
||||
});
|
||||
|
||||
db.recent_message().insert(RecentMessage {
|
||||
id: msg.id,
|
||||
sender: msg.sender,
|
||||
sent: msg.sent,
|
||||
text: msg.text,
|
||||
channel_id: msg.channel_id,
|
||||
thread_id: msg.thread_id,
|
||||
seq_id,
|
||||
reactions: msg.reactions,
|
||||
image_ids: msg.image_ids,
|
||||
thread_name: msg.thread_name,
|
||||
thread_reply_count: msg.thread_reply_count,
|
||||
edited: msg.edited,
|
||||
server_id: 0, // DMs have server_id 0
|
||||
});
|
||||
|
||||
let limit = get_recent_message_limit(db);
|
||||
if seq_id > limit {
|
||||
let old_seq_id = seq_id - limit;
|
||||
let to_delete: Vec<_> = db
|
||||
.recent_message()
|
||||
.channel_id()
|
||||
.filter(channel_id)
|
||||
.filter(|m| m.seq_id <= old_seq_id)
|
||||
.map(|m| m.id)
|
||||
.collect();
|
||||
for id in to_delete {
|
||||
db.recent_message().id().delete(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn sync_server_member_info(db: &Local, identity: Identity) {
|
||||
if let Some(user) = db.user().identity().find(identity) {
|
||||
let members: Vec<_> = db.server_member().identity().filter(identity).collect();
|
||||
|
||||
@@ -44,7 +44,7 @@ export class ChatService {
|
||||
this.#voice = new VoiceService(this.#db, this.#nav, () => this.identity);
|
||||
this.#account = new AccountService();
|
||||
this.#server = new ServerManagementService();
|
||||
this.#dm = new DirectMessagingService(this.#db, () => this.identity);
|
||||
this.#dm = new DirectMessagingService(this.#db, this.#nav, () => this.identity);
|
||||
|
||||
// Session-only image processing: creates Blob URLs directly from Database data.
|
||||
// This ditched the persistent IndexedDB cache to prevent stale data between reloads.
|
||||
|
||||
@@ -2,15 +2,18 @@ import { useReducer } from "spacetimedb/svelte";
|
||||
import { reducers } from "../../module_bindings";
|
||||
import type { Identity } from "spacetimedb";
|
||||
import type { DatabaseService } from "./database.svelte";
|
||||
import type { NavigationService } from "./navigation.svelte";
|
||||
|
||||
export class DirectMessagingService {
|
||||
#openDirectMessageReducer = useReducer(reducers.openDirectMessage);
|
||||
#closeDirectMessageReducer = useReducer(reducers.closeDirectMessage);
|
||||
#db: DatabaseService;
|
||||
#nav: NavigationService;
|
||||
#identity: () => Identity | null;
|
||||
|
||||
constructor(db: DatabaseService, identity: () => Identity | null) {
|
||||
constructor(db: DatabaseService, nav: NavigationService, identity: () => Identity | null) {
|
||||
this.#db = db;
|
||||
this.#nav = nav;
|
||||
this.#identity = identity;
|
||||
}
|
||||
|
||||
@@ -25,15 +28,30 @@ export class DirectMessagingService {
|
||||
);
|
||||
|
||||
if (existing) {
|
||||
// Session exists, open it if closed
|
||||
if (existing.sender.isEqual(identity) && !existing.isOpenSender) {
|
||||
this.#openDirectMessageReducer({ recipient });
|
||||
} else if (existing.recipient.isEqual(identity) && !existing.isOpenRecipient) {
|
||||
// Session exists, focus it
|
||||
this.#nav.activeServerId = null; // Go home
|
||||
this.#nav.activeChannelId = existing.channelId;
|
||||
|
||||
// Ensure it's open in DB if it was closed
|
||||
const isMeSender = existing.sender.isEqual(identity);
|
||||
const isClosed = isMeSender ? !existing.isOpenSender : !existing.isOpenRecipient;
|
||||
|
||||
if (isClosed) {
|
||||
this.#openDirectMessageReducer({ recipient });
|
||||
}
|
||||
} else {
|
||||
// No session, create it
|
||||
this.#openDirectMessageReducer({ recipient });
|
||||
// No session, create it and wait for application to focus
|
||||
this.#openDirectMessageReducer({ recipient }).onApplied(() => {
|
||||
// Find the newly created DM
|
||||
const newDm = this.#db.directMessages.find(dm =>
|
||||
(dm.sender.isEqual(identity) && dm.recipient.isEqual(recipient)) ||
|
||||
(dm.sender.isEqual(recipient) && dm.identity.isEqual(identity)) // fallback
|
||||
);
|
||||
if (newDm) {
|
||||
this.#nav.activeServerId = null;
|
||||
this.#nav.activeChannelId = newDm.channelId;
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ export class NavigationService {
|
||||
activeChannelId = $state<bigint | null>(null);
|
||||
activeThreadId = $state<bigint | null>(null);
|
||||
pendingThreadParentMessageId = $state<bigint | null>(null);
|
||||
lastActiveDmId = $state<bigint | null>(null);
|
||||
|
||||
#db: DatabaseService;
|
||||
#identity: () => Identity | null;
|
||||
@@ -27,6 +28,16 @@ export class NavigationService {
|
||||
prevChannelId = currentChannelId;
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
const currentServerId = this.activeServerId;
|
||||
const currentChannelId = this.activeChannelId;
|
||||
if (currentServerId === null && currentChannelId !== null) {
|
||||
untrack(() => {
|
||||
this.lastActiveDmId = currentChannelId;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (this.pendingThreadParentMessageId) {
|
||||
const newThread = this.#db.allThreads.find(
|
||||
@@ -68,7 +79,15 @@ export class NavigationService {
|
||||
|
||||
if (!isCurrentChannelValid) {
|
||||
untrack(() => {
|
||||
this.activeChannelId = null;
|
||||
// Try to restore last active DM if valid
|
||||
if (this.lastActiveDmId && activeDms.some(dm => dm.channelId === this.lastActiveDmId)) {
|
||||
this.activeChannelId = this.lastActiveDmId;
|
||||
} else if (activeDms.length > 0) {
|
||||
// Otherwise pick the first one
|
||||
this.activeChannelId = activeDms[0].channelId;
|
||||
} else {
|
||||
this.activeChannelId = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user