first pass at server invites
This commit is contained in:
Generated
+1
@@ -749,6 +749,7 @@ name = "spacetimedb_rust"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"log",
|
||||
"rand 0.8.5",
|
||||
"spacetimedb",
|
||||
]
|
||||
|
||||
|
||||
@@ -11,3 +11,4 @@ crate-type = ["cdylib"]
|
||||
[dependencies]
|
||||
spacetimedb = { version = "2.1.0" }
|
||||
log = "0.4"
|
||||
rand = { version = "0.8", features = ["small_rng"] }
|
||||
|
||||
@@ -125,7 +125,7 @@ pub fn on_connect(ctx: &ReducerContext) {
|
||||
});
|
||||
|
||||
// Minimal auto-join
|
||||
join_server(ctx, 1);
|
||||
join_server(ctx, Some(1), None);
|
||||
|
||||
// System Welcome DM
|
||||
let system_identity = Identity::from_hex(SYSTEM_IDENTITY).unwrap();
|
||||
@@ -167,6 +167,8 @@ pub fn on_disconnect(ctx: &ReducerContext) {
|
||||
ctx.db.typing_activity().delete(ta);
|
||||
}
|
||||
|
||||
ctx.db.join_server_status().identity().delete(ctx.sender());
|
||||
|
||||
clear_user_presence(&ctx.db, ctx.sender());
|
||||
}
|
||||
|
||||
|
||||
+137
-8
@@ -242,15 +242,144 @@ pub fn create_server(ctx: &ReducerContext, name: String) {
|
||||
sync_server_access(&ctx.db, ctx.sender(), s.id);
|
||||
}
|
||||
|
||||
#[spacetimedb::reducer]
|
||||
pub fn join_server(ctx: &ReducerContext, server_id: u64) {
|
||||
let user = ctx.db.user().identity().find(ctx.sender()).expect("User not found");
|
||||
let s = ctx.db.server().id().find(server_id).expect("Server not found");
|
||||
if !s.public && user.subject.is_none() && user.name.is_none() { panic!("Name required for private server"); }
|
||||
use rand::distributions::{Alphanumeric, DistString};
|
||||
|
||||
if ctx.db.server_member().identity().filter(ctx.sender()).any(|m| m.server_id == server_id) { return; }
|
||||
ctx.db.server_member().insert(ServerMember { id: 0, identity: ctx.sender(), server_id, name: user.name.clone(), avatar_id: user.avatar_id, online: user.online });
|
||||
sync_server_access(&ctx.db, ctx.sender(), server_id);
|
||||
#[spacetimedb::reducer]
|
||||
pub fn create_invite(ctx: &ReducerContext, server_id: u64, max_uses: Option<u32>, expires_in_hrs: Option<u32>) {
|
||||
// Only members can invite
|
||||
if !ctx.db.server_member().identity().filter(ctx.sender()).any(|m| m.server_id == server_id) {
|
||||
panic!("Only server members can create invites");
|
||||
}
|
||||
|
||||
// Generate a 12-character alphanumeric code using the provided deterministic RNG
|
||||
let code = Alphanumeric.sample_string(&mut ctx.rng(), 12);
|
||||
|
||||
let expires_at = expires_in_hrs.map(|h| {
|
||||
let duration = spacetimedb::TimeDuration::from_micros(h as i64 * 3600 * 1000000);
|
||||
ctx.timestamp + duration
|
||||
});
|
||||
|
||||
ctx.db.invite().insert(Invite {
|
||||
code: code.clone(),
|
||||
server_id,
|
||||
inviter: ctx.sender(),
|
||||
expires_at,
|
||||
uses_remaining: max_uses,
|
||||
});
|
||||
|
||||
// Send the generated code back to the client via JoinServerStatus
|
||||
ctx.db.join_server_status().identity().delete(ctx.sender());
|
||||
ctx.db.join_server_status().insert(JoinServerStatus {
|
||||
identity: ctx.sender(),
|
||||
status: "invite_created".to_string(),
|
||||
server_id: Some(server_id),
|
||||
error: Some(code), // We use the error field to carry the code for this status
|
||||
});
|
||||
}
|
||||
|
||||
#[spacetimedb::reducer]
|
||||
pub fn join_server(ctx: &ReducerContext, server_id: Option<u64>, invite_code: Option<String>) {
|
||||
let sender = ctx.sender();
|
||||
|
||||
// Helper to record error and return
|
||||
let fail = |db: &spacetimedb::Local, err: &str| {
|
||||
db.join_server_status().identity().delete(sender);
|
||||
db.join_server_status().insert(JoinServerStatus {
|
||||
identity: sender,
|
||||
status: "error".to_string(),
|
||||
server_id: None,
|
||||
error: Some(err.to_string()),
|
||||
});
|
||||
};
|
||||
|
||||
let user = ctx.db.user().identity().find(sender).expect("User not found");
|
||||
|
||||
let target_server_id = if let Some(id) = server_id {
|
||||
id
|
||||
} else if let Some(ref code) = invite_code {
|
||||
let invite = match ctx.db.invite().code().find(code.clone()) {
|
||||
Some(i) => i,
|
||||
None => {
|
||||
fail(&ctx.db, "Invalid invite code");
|
||||
return;
|
||||
}
|
||||
};
|
||||
invite.server_id
|
||||
} else {
|
||||
fail(&ctx.db, "Either server_id or invite_code must be provided");
|
||||
return;
|
||||
};
|
||||
|
||||
let s = match ctx.db.server().id().find(target_server_id) {
|
||||
Some(s) => s,
|
||||
None => {
|
||||
fail(&ctx.db, "Server not found");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// Permission check: if private, must have a valid invite code
|
||||
if !s.public {
|
||||
if let Some(code) = invite_code {
|
||||
let invite = match ctx.db.invite().code().find(code) {
|
||||
Some(i) => i,
|
||||
None => {
|
||||
fail(&ctx.db, "Invalid invite code");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if invite.server_id != target_server_id {
|
||||
fail(&ctx.db, "Invite code does not match server");
|
||||
return;
|
||||
}
|
||||
|
||||
// Expiry check
|
||||
if let Some(expiry) = invite.expires_at {
|
||||
if ctx.timestamp > expiry {
|
||||
fail(&ctx.db, "Invite code expired");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Uses check
|
||||
if let Some(mut uses) = invite.uses_remaining {
|
||||
if uses == 0 {
|
||||
fail(&ctx.db, "Invite code usage limit reached");
|
||||
return;
|
||||
}
|
||||
uses -= 1;
|
||||
let mut invite = invite.clone();
|
||||
invite.uses_remaining = Some(uses);
|
||||
ctx.db.invite().code().update(invite);
|
||||
}
|
||||
} else {
|
||||
fail(&ctx.db, "Invite code required for private server");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if !s.public && user.subject.is_none() && user.name.is_none() {
|
||||
fail(&ctx.db, "DisplayName required for private server");
|
||||
return;
|
||||
}
|
||||
|
||||
if ctx.db.server_member().identity().filter(sender).any(|m| m.server_id == target_server_id) {
|
||||
fail(&ctx.db, "Already a member of this server");
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.db.server_member().insert(ServerMember { id: 0, identity: sender, server_id: target_server_id, name: user.name.clone(), avatar_id: user.avatar_id, online: user.online });
|
||||
sync_server_access(&ctx.db, sender, target_server_id);
|
||||
|
||||
// Record success
|
||||
ctx.db.join_server_status().identity().delete(sender);
|
||||
ctx.db.join_server_status().insert(JoinServerStatus {
|
||||
identity: sender,
|
||||
status: "success".to_string(),
|
||||
server_id: Some(target_server_id),
|
||||
error: None,
|
||||
});
|
||||
}
|
||||
|
||||
#[spacetimedb::reducer]
|
||||
|
||||
@@ -281,3 +281,24 @@ pub struct UploadStatus {
|
||||
pub status: String, // "pending", "success", "error"
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
#[spacetimedb::table(accessor = join_server_status, public)]
|
||||
#[derive(Clone)]
|
||||
pub struct JoinServerStatus {
|
||||
#[primary_key]
|
||||
pub identity: Identity,
|
||||
pub status: String, // "success", "error"
|
||||
pub server_id: Option<u64>,
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
#[spacetimedb::table(accessor = invite)]
|
||||
#[derive(Clone)]
|
||||
pub struct Invite {
|
||||
#[primary_key]
|
||||
pub code: String,
|
||||
pub server_id: u64,
|
||||
pub inviter: Identity,
|
||||
pub expires_at: Option<Timestamp>,
|
||||
pub uses_remaining: Option<u32>,
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
import ServerSettingsPanel from "./components/ServerSettingsPanel.svelte";
|
||||
import ImageViewer from "./components/ImageViewer.svelte";
|
||||
import ProfileModal from "./components/ProfileModal.svelte";
|
||||
import InviteModal from "./components/InviteModal.svelte";
|
||||
import UserContextMenu from "./components/UserContextMenu.svelte";
|
||||
import ConfirmModal from "./components/ConfirmModal.svelte";
|
||||
|
||||
@@ -27,6 +28,30 @@
|
||||
const chat = new ChatService($spacetime.identity!);
|
||||
const webrtc = new WebRTCService($spacetime.identity, undefined);
|
||||
|
||||
// 0. Invite Link Handling
|
||||
onMount(() => {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const inviteCode = params.get("invite");
|
||||
if (inviteCode) {
|
||||
console.log(`[Invite] Detected invite code in URL: ${inviteCode}`);
|
||||
// Wait for chat to be ready before joining
|
||||
const checkReady = setInterval(() => {
|
||||
if (chat.isReady) {
|
||||
clearInterval(checkReady);
|
||||
chat.handleJoinServer(undefined, inviteCode);
|
||||
|
||||
// Clear URL parameter without reloading
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.delete("invite");
|
||||
window.history.replaceState({}, document.title, url.pathname + url.search);
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
});
|
||||
|
||||
setContext("chat", chat);
|
||||
setContext("webrtc", webrtc);
|
||||
|
||||
$effect(() => {
|
||||
if ($spacetime.identity) {
|
||||
chat.identity = $spacetime.identity;
|
||||
@@ -41,8 +66,6 @@
|
||||
webrtc.connectedChannelId = undefined;
|
||||
}
|
||||
});
|
||||
setContext("chat", chat);
|
||||
setContext("webrtc", webrtc);
|
||||
|
||||
let showSettings = $state(false);
|
||||
let showMemberList = $state(true);
|
||||
@@ -306,8 +329,11 @@
|
||||
<ProfileModal user={chat.viewingProfileUser} onClose={() => (chat.viewingProfileUser = null)} />
|
||||
{/if}
|
||||
|
||||
{#if chat.userContextMenu}
|
||||
<UserContextMenu
|
||||
{#if chat.showInviteModal}
|
||||
<InviteModal />
|
||||
{/if}
|
||||
|
||||
{#if chat.userContextMenu} <UserContextMenu
|
||||
{...chat.userContextMenu}
|
||||
onClose={() => (chat.userContextMenu = null)}
|
||||
onAction={closeSidebars}
|
||||
|
||||
@@ -197,7 +197,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="action-btn close" onclick={(e) => { e.stopPropagation(); onClose(); }}>
|
||||
<button class="action-btn close" onclick={(e) => { e.stopPropagation(); onClose(); }} aria-label="Close viewer">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
<script lang="ts">
|
||||
import { getContext } from "svelte";
|
||||
import type { ChatService } from "../services/chat.svelte";
|
||||
import Modal from "./Modal.svelte";
|
||||
import ModalLayout from "./ui/ModalLayout.svelte";
|
||||
import Button from "./ui/Button.svelte";
|
||||
import Input from "./ui/Input.svelte";
|
||||
|
||||
const chat = getContext<ChatService>("chat");
|
||||
|
||||
let inviteCode = $state("");
|
||||
let copied = $state(false);
|
||||
const inviteLink = $derived(inviteCode ? `${window.location.origin}${window.location.pathname}?invite=${inviteCode}` : "");
|
||||
|
||||
// Reset code when the modal is opened or the server changes
|
||||
$effect(() => {
|
||||
const _ = chat.activeServerId;
|
||||
inviteCode = "";
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
const status = chat.joinServerStatus;
|
||||
|
||||
// Only update if the status is for this server
|
||||
if (status?.status === "invite_created" && status.error) {
|
||||
const statusServerId = status.serverId?.toString();
|
||||
const currentServerId = chat.activeServerId?.toString();
|
||||
|
||||
console.log(`[InviteModal] Received code. Comparing: ${statusServerId} === ${currentServerId}`);
|
||||
if (statusServerId === currentServerId) {
|
||||
inviteCode = status.error;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function handleGenerate() {
|
||||
if (chat.activeServerId) {
|
||||
inviteCode = ""; // Clear existing before generating
|
||||
chat.handleCreateInvite(chat.activeServerId);
|
||||
}
|
||||
}
|
||||
|
||||
function copyToClipboard() {
|
||||
if (inviteLink) {
|
||||
navigator.clipboard.writeText(inviteLink);
|
||||
copied = true;
|
||||
setTimeout(() => copied = false, 2000);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal onClose={() => (chat.showInviteModal = false)}>
|
||||
<ModalLayout title="Invite friends to {chat.activeServer?.name}" onClose={() => (chat.showInviteModal = false)}>
|
||||
<div class="invite-content">
|
||||
<p class="invite-description">
|
||||
Share this link with others so they can join your private server.
|
||||
</p>
|
||||
|
||||
<div class="generate-section">
|
||||
{#if !inviteCode}
|
||||
<Button onclick={handleGenerate} style="width: 100%;">
|
||||
Generate Invite Link
|
||||
</Button>
|
||||
<p class="help-text">Links are unique to you and the current server.</p>
|
||||
{:else}
|
||||
<div class="code-box-wrapper">
|
||||
<Input
|
||||
id="generated-link"
|
||||
value={inviteLink}
|
||||
disabled
|
||||
label="Your Invite Link"
|
||||
/>
|
||||
<Button
|
||||
variant={copied ? "success" : "primary"}
|
||||
size="small"
|
||||
onclick={copyToClipboard}
|
||||
style="height: 38px; margin-bottom: 2px;"
|
||||
>
|
||||
{copied ? "Copied!" : "Copy"}
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</ModalLayout>
|
||||
</Modal>
|
||||
|
||||
<style>
|
||||
.invite-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.invite-description {
|
||||
color: var(--text-normal);
|
||||
font-size: 0.95rem;
|
||||
line-height: 1.4;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.generate-section {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.help-text {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-muted);
|
||||
text-align: center;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.code-box-wrapper {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: flex-end;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -9,49 +9,105 @@
|
||||
const chat = getContext<ChatService>("chat");
|
||||
|
||||
let searchTerm = $state("");
|
||||
let inviteCode = $state("");
|
||||
let joinError = $state<string | null>(null);
|
||||
|
||||
$effect(() => {
|
||||
const status = chat.joinServerStatus;
|
||||
if (status?.status === "error") {
|
||||
joinError = status.error || "Failed to join server";
|
||||
} else if (status?.status === "success") {
|
||||
joinError = null;
|
||||
inviteCode = "";
|
||||
|
||||
// Navigate to the joined server
|
||||
if (status.serverId) {
|
||||
console.log(`[Discovery] Success! Navigating to server ${status.serverId}`);
|
||||
chat.activeServerId = status.serverId;
|
||||
}
|
||||
|
||||
chat.showDiscoveryModal = false;
|
||||
}
|
||||
});
|
||||
|
||||
let filteredServers = $derived(
|
||||
chat.availableServers.filter((s) =>
|
||||
s.name.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
)
|
||||
);
|
||||
|
||||
function handleJoinWithCode() {
|
||||
if (inviteCode.trim()) {
|
||||
joinError = null;
|
||||
chat.handleJoinServer(undefined, inviteCode.trim());
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal onClose={() => (chat.showDiscoveryModal = false)}>
|
||||
<ModalLayout title="Discover Servers" onClose={() => (chat.showDiscoveryModal = false)}>
|
||||
<div class="discovery-content">
|
||||
<Input
|
||||
id="server-search"
|
||||
autofocus
|
||||
placeholder="Search for servers..."
|
||||
bind:value={searchTerm}
|
||||
style="margin-bottom: 16px;"
|
||||
/>
|
||||
{#if joinError}
|
||||
<div class="form-error" style="color: var(--status-danger); background-color: rgba(219, 64, 64, 0.1); padding: 12px; border-radius: 4px; margin-bottom: 16px; font-size: 0.85rem; display: flex; align-items: center; gap: 8px;">
|
||||
<i class="fas fa-exclamation-circle"></i> {joinError}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="server-list-container">
|
||||
{#if filteredServers.length === 0}
|
||||
<p class="no-results">
|
||||
No servers found.
|
||||
</p>
|
||||
{:else}
|
||||
{#each filteredServers as server (server.id.toString())}
|
||||
<div class="server-item">
|
||||
<div class="server-info">
|
||||
<div class="server-icon-small">
|
||||
{server.name.substring(0, 2).toUpperCase()}
|
||||
<div class="invite-code-section">
|
||||
<h3>Join with Invite Code</h3>
|
||||
<div class="invite-row">
|
||||
<Input
|
||||
id="invite-code"
|
||||
placeholder="e.g. sender-timestamp"
|
||||
bind:value={inviteCode}
|
||||
/>
|
||||
<Button
|
||||
onclick={handleJoinWithCode}
|
||||
disabled={!inviteCode.trim() || !chat.isFullyAuthenticated}
|
||||
size="small"
|
||||
style="height: 38px; align-self: flex-end;"
|
||||
>
|
||||
Join
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-separator" style="margin: 24px 0;"></div>
|
||||
|
||||
<div class="public-servers-section">
|
||||
<h3>Public Servers</h3>
|
||||
<Input
|
||||
id="server-search"
|
||||
placeholder="Search for servers..."
|
||||
bind:value={searchTerm}
|
||||
style="margin-bottom: 16px;"
|
||||
/>
|
||||
|
||||
<div class="server-list-container">
|
||||
{#if filteredServers.length === 0}
|
||||
<p class="no-results">
|
||||
No public servers found.
|
||||
</p>
|
||||
{:else}
|
||||
{#each filteredServers as server (server.id.toString())}
|
||||
<div class="server-item">
|
||||
<div class="server-info">
|
||||
<div class="server-icon-small">
|
||||
{server.name.substring(0, 2).toUpperCase()}
|
||||
</div>
|
||||
<span class="server-name">{server.name}</span>
|
||||
</div>
|
||||
<span class="server-name">{server.name}</span>
|
||||
<Button
|
||||
onclick={() => chat.handleJoinServer(server.id)}
|
||||
disabled={!chat.isFullyAuthenticated}
|
||||
size="small"
|
||||
>
|
||||
Join
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
onclick={() => chat.handleJoinServer(server.id)}
|
||||
disabled={!chat.isFullyAuthenticated}
|
||||
size="small"
|
||||
>
|
||||
Join
|
||||
</Button>
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ModalLayout>
|
||||
@@ -64,6 +120,20 @@
|
||||
max-height: 60vh;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 800;
|
||||
color: var(--text-muted);
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.invite-row {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.server-list-container {
|
||||
overflow-y: auto;
|
||||
flex-grow: 1;
|
||||
|
||||
@@ -31,6 +31,19 @@
|
||||
|
||||
{#if showServerDropdown}
|
||||
<div class="server-dropdown shadow-box">
|
||||
<button
|
||||
class="server-dropdown-item"
|
||||
onclick={() => {
|
||||
chat.showInviteModal = true;
|
||||
showServerDropdown = false;
|
||||
}}
|
||||
style="color: var(--brand); font-weight: bold;"
|
||||
>
|
||||
<i class="fas fa-user-plus" style="width: 16px; margin-right: 8px;"></i>
|
||||
Invite People
|
||||
</button>
|
||||
<div class="dropdown-separator"></div>
|
||||
|
||||
{#if isOwner}
|
||||
<button
|
||||
class="server-dropdown-item"
|
||||
|
||||
@@ -371,24 +371,6 @@
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.input-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: var(--background-primary);
|
||||
border-radius: 4px;
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
.form-group input[type="text"] {
|
||||
flex: 1;
|
||||
padding: 10px;
|
||||
background: none;
|
||||
color: var(--text-normal);
|
||||
border: none;
|
||||
font-size: 1rem;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.emoji-prefix, .emoji-suffix {
|
||||
padding: 0 8px;
|
||||
color: var(--text-muted);
|
||||
|
||||
@@ -179,7 +179,7 @@
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-group label, .label-heading {
|
||||
.label-heading {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
color: var(--header-secondary);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { untrack } from "svelte";
|
||||
import { Identity } from "spacetimedb";
|
||||
import { SvelteSet } from "svelte/reactivity";
|
||||
import * as Types from "../../module_bindings/types";
|
||||
@@ -48,6 +49,17 @@ export class ChatService {
|
||||
this.#encryption = new EncryptionService(this.#db, () => this.identity);
|
||||
this.#media = new MediaCacheService(this.#db, () => this.identity);
|
||||
|
||||
// Global Join Handler: Automatically navigate when we successfully join a server
|
||||
$effect(() => {
|
||||
const status = this.joinServerStatus;
|
||||
if (status?.status === "success" && status.serverId) {
|
||||
console.log(`[ChatService] Global join success detected for server ${status.serverId}. Navigating...`);
|
||||
untrack(() => {
|
||||
this.activeServerId = status.serverId!;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Load notification preferences from localStorage
|
||||
$effect(() => {
|
||||
const myId = this.identity?.toHexString();
|
||||
@@ -241,6 +253,12 @@ export class ChatService {
|
||||
set showServerSettings(v) {
|
||||
this.#ui.showServerSettings = v;
|
||||
}
|
||||
get showInviteModal() {
|
||||
return this.#ui.showInviteModal;
|
||||
}
|
||||
set showInviteModal(v) {
|
||||
this.#ui.showInviteModal = v;
|
||||
}
|
||||
get authError() {
|
||||
return this.#ui.authError;
|
||||
}
|
||||
@@ -717,8 +735,12 @@ export class ChatService {
|
||||
this.showSetNameModal = false;
|
||||
};
|
||||
|
||||
handleJoinServer = (serverId: bigint) => {
|
||||
this.#server.handleJoinServer(serverId);
|
||||
handleJoinServer = (serverId?: bigint, inviteCode?: string) => {
|
||||
this.#server.handleJoinServer(serverId, inviteCode);
|
||||
};
|
||||
|
||||
handleCreateInvite = (serverId: bigint, maxUses?: number, expiresInHrs?: number) => {
|
||||
this.#server.handleCreateInvite(serverId, maxUses, expiresInHrs);
|
||||
};
|
||||
|
||||
handleLeaveServer = (serverId: bigint) => {
|
||||
@@ -733,8 +755,8 @@ export class ChatService {
|
||||
this.#server.handleUpdateServerName(serverId, name);
|
||||
};
|
||||
|
||||
handleSetServerPublic = (serverId: bigint, isPublic: boolean) => {
|
||||
this.#server.handleSetServerPublic(serverId, isPublic);
|
||||
handleSetServerPublic = (serverId: bigint, publicFlag: boolean) => {
|
||||
this.#server.handleSetServerPublic(serverId, publicFlag);
|
||||
};
|
||||
|
||||
get activeDms() {
|
||||
@@ -743,6 +765,11 @@ export class ChatService {
|
||||
get directMessages() {
|
||||
return this.#db.directMessages;
|
||||
}
|
||||
get joinServerStatus() {
|
||||
const myId = this.identity;
|
||||
if (!myId) return null;
|
||||
return this.#db.joinServerStatus.find(s => s.identity.isEqual(myId)) || null;
|
||||
}
|
||||
|
||||
handleDeleteServer = (serverId: bigint) => {
|
||||
this.#server.handleDeleteServer(serverId);
|
||||
|
||||
@@ -17,6 +17,7 @@ export class DatabaseService {
|
||||
typingActivity = $state<readonly Types.TypingActivity[]>([]);
|
||||
systemConfiguration = $state<readonly Types.SystemConfiguration[]>([]);
|
||||
uploadStatus = $state<readonly Types.UploadStatus[]>([]);
|
||||
joinServerStatus = $state<readonly Types.JoinServerStatus[]>([]);
|
||||
isUsersReady = $state(false);
|
||||
isServersReady = $state(false);
|
||||
isChannelsReady = $state(false);
|
||||
@@ -87,6 +88,7 @@ export class DatabaseService {
|
||||
const [typingActivityStore] = useTable(tables.visible_typing_activity);
|
||||
const [systemConfigStore] = useTable(tables.system_configuration);
|
||||
const [uploadStatusStore] = useTable(tables.upload_status);
|
||||
const [joinServerStatusStore] = useTable(tables.join_server_status);
|
||||
|
||||
serversStore.subscribe((v) => (this.servers = v));
|
||||
serversReadyStore.subscribe((v) => (this.isServersReady = v));
|
||||
@@ -113,5 +115,6 @@ export class DatabaseService {
|
||||
typingActivityStore.subscribe((v) => (this.typingActivity = v));
|
||||
systemConfigStore.subscribe((v) => (this.systemConfiguration = v));
|
||||
uploadStatusStore.subscribe((v) => (this.uploadStatus = v));
|
||||
joinServerStatusStore.subscribe((v) => (this.joinServerStatus = v));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ export class ServerManagementService {
|
||||
#updateServerNameReducer = useReducer(reducers.updateServerName);
|
||||
#setServerPublicReducer = useReducer(reducers.setServerPublic);
|
||||
#deleteServerReducer = useReducer(reducers.deleteServer);
|
||||
#createInviteReducer = useReducer(reducers.createInvite);
|
||||
|
||||
handleCreateServer = (name: string) => {
|
||||
if (name.trim()) {
|
||||
@@ -27,8 +28,12 @@ export class ServerManagementService {
|
||||
}
|
||||
};
|
||||
|
||||
handleJoinServer = (serverId: bigint) => {
|
||||
this.#joinServerReducer({ serverId });
|
||||
handleJoinServer = (serverId?: bigint, inviteCode?: string) => {
|
||||
this.#joinServerReducer({ serverId, inviteCode });
|
||||
};
|
||||
|
||||
handleCreateInvite = (serverId: bigint, maxUses?: number, expiresInHrs?: number) => {
|
||||
this.#createInviteReducer({ serverId, maxUses, expiresInHrs });
|
||||
};
|
||||
|
||||
handleLeaveServer = (serverId: bigint) => {
|
||||
|
||||
@@ -22,6 +22,7 @@ export class ThemeService {
|
||||
threadMessageText = $state("");
|
||||
showDiscoveryModal = $state(false);
|
||||
showServerSettings = $state(false);
|
||||
showInviteModal = $state(false);
|
||||
authError = $state("");
|
||||
viewingImageId = $state<bigint | null>(null);
|
||||
viewingProfileUser = $state<any | null>(null);
|
||||
|
||||
Reference in New Issue
Block a user