RBAC backend

This commit is contained in:
2026-04-21 22:29:20 -04:00
parent 1f0631dfbc
commit 04ec43e507
15 changed files with 298 additions and 116 deletions
+80 -43
View File
@@ -81,14 +81,16 @@ pub fn clear_upload_status(ctx: &ReducerContext, client_id: String) {
#[spacetimedb::reducer]
pub fn upload_custom_emoji(ctx: &ReducerContext, name: String, category: String, data: Vec<u8>) {
if data.len() > 256 * 1024 {
panic!("Emoji image exceeds 256KB limit");
return report_error(&ctx.db, ctx.sender(), "upload_emoji", "Emoji image exceeds 256KB limit", ctx.timestamp);
}
// Note: Emojis are currently global.
ctx.db.custom_emoji().insert(CustomEmoji {
id: 0,
name,
category,
data,
});
report_success(&ctx.db, ctx.sender(), "upload_emoji", ctx.timestamp);
}
#[spacetimedb::reducer]
@@ -141,12 +143,16 @@ pub fn upload_server_avatar(
data: Vec<u8>,
mime_type: String,
) {
let mut s = ctx
.db
.server()
.id()
.find(server_id)
.expect("Server not found");
if !has_permission(&ctx.db, ctx.sender(), server_id, PERM_MANAGE_SERVER) {
return report_error(&ctx.db, ctx.sender(), "update_server", "Insufficient permissions", ctx.timestamp);
}
let mut s = match ctx.db.server().id().find(server_id) {
Some(s) => s,
None => return report_error(&ctx.db, ctx.sender(), "update_server", "Server not found", ctx.timestamp),
};
if data.len() > 4 * 1024 * 1024 {
return report_error(&ctx.db, ctx.sender(), "update_server", "Image exceeds 4MB limit", ctx.timestamp);
}
let img = ctx.db.image().insert(Image {
id: 0,
mime_type,
@@ -158,23 +164,29 @@ pub fn upload_server_avatar(
});
s.avatar_id = Some(img.id);
ctx.db.server().id().update(s);
report_success(&ctx.db, ctx.sender(), "update_server", ctx.timestamp);
}
#[spacetimedb::reducer]
pub fn update_server_name(ctx: &ReducerContext, server_id: u64, name: String) {
validate_name(&name).expect("Invalid name");
let mut s = ctx
.db
.server()
.id()
.find(server_id)
.expect("Server not found");
s.name = name;
ctx.db.server().id().update(s);
if !has_permission(&ctx.db, ctx.sender(), server_id, PERM_MANAGE_SERVER) {
return report_error(&ctx.db, ctx.sender(), "update_server", "Insufficient permissions", ctx.timestamp);
}
if let Err(e) = validate_name(&name) {
return report_error(&ctx.db, ctx.sender(), "update_server", &e, ctx.timestamp);
}
if let Some(mut s) = ctx.db.server().id().find(server_id) {
s.name = name;
ctx.db.server().id().update(s);
report_success(&ctx.db, ctx.sender(), "update_server", ctx.timestamp);
}
}
#[spacetimedb::reducer]
pub fn delete_server(ctx: &ReducerContext, server_id: u64) {
if !has_permission(&ctx.db, ctx.sender(), server_id, PERM_DELETE_SERVER) {
return report_error(&ctx.db, ctx.sender(), "delete_server", "Insufficient permissions", ctx.timestamp);
}
for c in ctx.db.channel().server_id().filter(server_id) {
for msg in ctx.db.message().channel_id().filter(c.id) {
ctx.db.message().id().delete(msg.id);
@@ -185,6 +197,7 @@ pub fn delete_server(ctx: &ReducerContext, server_id: u64) {
ctx.db.server_member().id().delete(m.id);
}
ctx.db.server().id().delete(server_id);
report_success(&ctx.db, ctx.sender(), "delete_server", ctx.timestamp);
}
#[spacetimedb::reducer]
@@ -202,7 +215,10 @@ pub fn edit_message(ctx: &ReducerContext, message_id: u64, new_text: String) {
#[spacetimedb::reducer]
pub fn delete_message(ctx: &ReducerContext, message_id: u64) {
if let Some(msg) = ctx.db.message().id().find(message_id) {
if msg.sender == ctx.sender() {
let is_owner = msg.sender == ctx.sender();
let can_moderate = has_permission(&ctx.db, ctx.sender(), msg.server_id, PERM_MODERATE_MESSAGES);
if is_owner || can_moderate {
ctx.db.message().id().delete(message_id);
}
}
@@ -210,18 +226,32 @@ pub fn delete_message(ctx: &ReducerContext, message_id: u64) {
#[spacetimedb::reducer]
pub fn set_server_public(ctx: &ReducerContext, server_id: u64, public: bool) {
let mut s = ctx
.db
.server()
.id()
.find(server_id)
.expect("Server not found");
if s.owner == Some(ctx.sender()) {
if !has_permission(&ctx.db, ctx.sender(), server_id, PERM_MANAGE_SERVER) {
return report_error(&ctx.db, ctx.sender(), "update_server", "Insufficient permissions", ctx.timestamp);
}
if let Some(mut s) = ctx.db.server().id().find(server_id) {
s.public = public;
ctx.db.server().id().update(s);
report_success(&ctx.db, ctx.sender(), "update_server", ctx.timestamp);
}
}
#[spacetimedb::reducer]
pub fn set_member_permissions(ctx: &ReducerContext, server_id: u64, identity: Identity, permissions: u128) {
if !has_permission(&ctx.db, ctx.sender(), server_id, PERM_MANAGE_ROLES) {
return report_error(&ctx.db, ctx.sender(), "set_permissions", "Insufficient permissions", ctx.timestamp);
}
let existing = ctx.db.server_permission().server_id().filter(server_id).find(|p| p.identity == identity);
if let Some(mut p) = existing {
p.permissions = permissions;
ctx.db.server_permission().id().update(p);
} else {
ctx.db.server_permission().insert(ServerPermission { id: 0, server_id, identity, permissions });
}
report_success(&ctx.db, ctx.sender(), "set_permissions", ctx.timestamp);
}
#[spacetimedb::reducer]
pub fn toggle_reaction(
ctx: &ReducerContext,
@@ -398,8 +428,16 @@ pub fn create_server(ctx: &ReducerContext, name: String) {
kind: c2.kind,
});
ctx.db.server().id().update(s.clone());
sync_server_access(&ctx.db, ctx.sender(), s.id);
// RBAC: Grant all permissions to the creator
ctx.db.server_permission().insert(ServerPermission {
id: 0,
server_id: s.id,
identity: ctx.sender(),
permissions: u128::MAX,
});
sync_server_access(&ctx.db, ctx.sender(), s.id);
report_success(&ctx.db, ctx.sender(), "create_server", ctx.timestamp);
}
@@ -412,19 +450,13 @@ pub fn create_invite(
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)
{
// RBAC: Only members with permission can invite
if !has_permission(&ctx.db, ctx.sender(), server_id, PERM_CREATE_INVITES) {
return report_error(
&ctx.db,
ctx.sender(),
"create_invite",
"Only server members can create invites",
"Insufficient permissions",
ctx.timestamp,
);
}
@@ -648,6 +680,9 @@ pub fn leave_server(ctx: &ReducerContext, server_id: u64) {
#[spacetimedb::reducer]
pub fn create_channel(ctx: &ReducerContext, name: String, server_id: u64, is_voice: bool) {
if !has_permission(&ctx.db, ctx.sender(), server_id, PERM_MANAGE_CHANNELS) {
return report_error(&ctx.db, ctx.sender(), "create_channel", "Insufficient permissions", ctx.timestamp);
}
if let Err(e) = validate_name(&name) {
return report_error(&ctx.db, ctx.sender(), "create_channel", &e, ctx.timestamp);
}
@@ -663,15 +698,6 @@ pub fn create_channel(ctx: &ReducerContext, name: String, server_id: u64, is_voi
);
}
};
if s.owner != Some(ctx.sender()) {
return report_error(
&ctx.db,
ctx.sender(),
"create_channel",
"Only owner can create channels",
ctx.timestamp,
);
}
let kind = if is_voice {
ChannelKind::Voice
@@ -698,6 +724,16 @@ pub fn create_channel(ctx: &ReducerContext, name: String, server_id: u64, is_voi
#[spacetimedb::reducer]
pub fn join_voice(ctx: &ReducerContext, channel_id: u64) {
let chan = match ctx.db.channel().id().find(channel_id) {
Some(c) => c,
None => return report_error(&ctx.db, ctx.sender(), "join_voice", "Channel not found", ctx.timestamp),
};
// RBAC: Check voice permission if it's a server channel
if chan.server_id != 0 && !has_permission(&ctx.db, ctx.sender(), chan.server_id, PERM_USE_VOICE) {
return report_error(&ctx.db, ctx.sender(), "join_voice", "Insufficient permissions", ctx.timestamp);
}
if let Some(mut state) = ctx.db.user_state().identity().find(ctx.sender()) {
if state.channel_id != channel_id {
clear_signaling_for_user(&ctx.db, ctx.sender());
@@ -718,6 +754,7 @@ pub fn join_voice(ctx: &ReducerContext, channel_id: u64) {
watching: None,
});
}
report_success(&ctx.db, ctx.sender(), "join_voice", ctx.timestamp);
}
#[spacetimedb::reducer]
+13
View File
@@ -247,6 +247,19 @@ impl From<RecentMessage> for Message {
}
}
#[spacetimedb::table(accessor = server_permission)]
#[derive(Clone)]
pub struct ServerPermission {
#[primary_key]
#[auto_inc]
pub id: u64,
#[index(btree)]
pub server_id: u64,
#[index(btree)]
pub identity: Identity,
pub permissions: u128,
}
#[spacetimedb::table(accessor = custom_emoji, public)]
#[derive(Clone)]
pub struct CustomEmoji {
+21
View File
@@ -475,3 +475,24 @@ pub fn report_success_with_payload(
last_update: timestamp,
});
}
pub const PERM_MANAGE_SERVER: u128 = 0x01;
pub const PERM_MANAGE_ROLES: u128 = 0x02;
pub const PERM_MANAGE_CHANNELS: u128 = 0x04;
pub const PERM_CREATE_INVITES: u128 = 0x08;
pub const PERM_KICK_MEMBERS: u128 = 0x10;
pub const PERM_BAN_MEMBERS: u128 = 0x20;
pub const PERM_MODERATE_MESSAGES: u128 = 0x40;
pub const PERM_USE_VOICE: u128 = 0x80;
pub const PERM_SHARE_SCREEN: u128 = 0x100;
pub const PERM_USE_THREADS: u128 = 0x200;
pub const PERM_MANAGE_EMOJIS: u128 = 0x400;
pub const PERM_DELETE_SERVER: u128 = 0x800;
pub fn has_permission(db: &Local, identity: Identity, server_id: u64, bit: u128) -> bool {
if let Some(perm) = db.server_permission().server_id().filter(server_id)
.find(|p| p.identity == identity) {
return (perm.permissions & bit) != 0;
}
false
}
+16
View File
@@ -242,6 +242,22 @@ pub fn visible_thread_messages(ctx: &ViewContext) -> Vec<Message> {
results
}
#[spacetimedb::view(accessor = visible_server_permissions, public)]
pub fn visible_server_permissions(ctx: &ViewContext) -> Vec<ServerPermission> {
let identity = ctx.sender();
let mut results = Vec::new();
// Show me everyone's permissions for servers I share with them
// (This allows mods to see who else is a mod)
for member in ctx.db.server_member().identity().filter(identity) {
for perm in ctx.db.server_permission().server_id().filter(member.server_id) {
results.push(perm.clone());
}
}
results
}
#[spacetimedb::view(accessor = visible_images, public)]
pub fn visible_images(ctx: &ViewContext) -> Vec<VisibleImageRow> {
let image_ids = get_visible_image_ids_read_only(&ctx.db, ctx.sender());
+10 -8
View File
@@ -158,17 +158,19 @@
</div>
</div>
<div class="voice-status-actions">
<button
class="icon-btn {webrtc.isSharingScreen ? 'active danger' : ''}"
onclick={() => webrtc.isSharingScreen ? webrtc.stopScreenShare() : webrtc.startScreenShare()}
title={webrtc.isSharingScreen ? "Stop Screen Share" : "Share Screen"}
>
<i class="fas fa-desktop"></i>
</button>
{#if chat.can(Permissions.SHARE_SCREEN)}
<button
class="icon-btn {webrtc.isSharingScreen ? 'active danger' : ''}"
onclick={() => webrtc.isSharingScreen ? webrtc.stopScreenShare() : webrtc.startScreenShare()}
title={webrtc.isSharingScreen ? "Stop Screen Share" : "Share Screen"}
>
<i class="fas fa-desktop"></i>
</button>
{/if}
<button
class="icon-btn danger"
onclick={() => chat.handleLeaveVoice()}
title="Disconnect"
title="Leave Voice"
>
<i class="fas fa-phone-slash"></i>
</button>
+13
View File
@@ -332,6 +332,19 @@
const currentText = messageText;
const currentStaged = [...stagedImages];
// RBAC: Thread Creation requires PERM_USE_THREADS
if (!activeThreadId && chat.pendingThreadParentMessageId) {
if (!chat.can(Permissions.USE_THREADS)) {
chat.confirmModal = {
title: "Insufficient Permissions",
message: "You do not have permission to start threads in this channel.",
confirmText: "Close",
onConfirm: () => {}
};
return;
}
}
const byteLength = new TextEncoder().encode(currentText).length;
if (byteLength > chat.maxMessageLength) {
uploadError = `Message exceeds maximum length of ${chat.maxMessageLength} bytes (${Math.round(chat.maxMessageLength / 1024)}KB).`;
+28 -1
View File
@@ -2,6 +2,7 @@
import { getContext, tick, untrack } from "svelte";
import { portal } from "../../portal";
import type { ChatService } from "../services/chat.svelte";
import { Permissions } from "../services/chat.svelte";
import RichText from "./RichText.svelte";
import EmojiPicker from "./EmojiPicker.svelte";
import Avatar from "./Avatar.svelte";
@@ -164,6 +165,10 @@
const msgUsername = $derived(chat.getUsername(msg.sender));
const existingThread = $derived(chat.allThreads.find((t) => t.parentMessageId === msg.id));
const isMe = $derived(chat.identity?.isEqual(msg.sender));
const canModerate = $derived(chat.can(Permissions.MODERATE_MESSAGES));
const canUseThreads = $derived(chat.can(Permissions.USE_THREADS));
// Reactions and images are now nested in the message object
const emojiGroups = $derived.by(() => {
const groups: Record<string, Types.Reaction[]> = {};
@@ -355,7 +360,7 @@
</div>
{/if}
{#if !isThread && chat.isFullyAuthenticated}
{#if !isThread && (canUseThreads || existingThread)}
<button
class="toolbar-btn"
onclick={() => chat.handleStartThread(msg)}
@@ -365,6 +370,28 @@
<i class="fas fa-comment"></i>
</button>
{/if}
{#if isMe || canModerate}
<button
class="toolbar-btn danger"
onclick={(e) => {
e.preventDefault();
e.stopPropagation();
chat.confirmModal = {
title: "Delete Message",
message: "Are you sure you want to delete this message? This action cannot be undone.",
confirmText: "Delete",
cancelText: "Cancel",
isDanger: true,
onConfirm: () => chat.handleDeleteMessage(msg.id)
};
}}
title="Delete Message"
aria-label="Delete message"
>
<i class="fas fa-trash-alt"></i>
</button>
{/if}
</div>
{#if isGrouped}
+1 -1
View File
@@ -68,7 +68,7 @@
if (codeFromUrl) {
finalCode = codeFromUrl;
}
} catch (e) {
} catch {
// Not a valid URL, treat as raw code
}
}
+30 -26
View File
@@ -2,6 +2,7 @@
import { getContext } from "svelte";
import type { ChatService } from "../services/chat.svelte";
import { optimizeEmoji } from "../utils";
import { Permissions } from "../services/chat.svelte";
import Button from "./ui/Button.svelte";
import Input from "./ui/Input.svelte";
import Switch from "./ui/Switch.svelte";
@@ -19,7 +20,8 @@
let errorMessage = $state<string | null>(null);
const server = $derived(chat.activeServer);
const isOwner = true; // temporarily enabled for everyone
const canManageServer = $derived(chat.can(Permissions.MANAGE_SERVER));
const canDeleteServer = $derived(chat.can(Permissions.DELETE_SERVER));
$effect(() => {
if (server) {
@@ -143,29 +145,31 @@
<i class="fas fa-sign-out-alt"></i>
Leave Server
</Button>
<Button
variant="danger"
size="medium"
class="sidebar-item-btn"
onclick={() => {
chat.confirmModal = {
title: `Delete '${server?.name}'`,
message: `Are you sure you want to delete ${server?.name}? This action is permanent and will delete all channels, messages, and associated data. This cannot be undone.`,
confirmText: "Delete Server",
cancelText: "Cancel",
isDanger: true,
onConfirm: async () => {
if (server) {
chat.handleDeleteServer(server.id);
onClose();
{#if canDeleteServer}
<Button
variant="danger"
size="medium"
class="sidebar-item-btn"
onclick={() => {
chat.confirmModal = {
title: `Delete '${server?.name}'`,
message: `Are you sure you want to delete ${server?.name}? This action is permanent and will delete all channels, messages, and associated data. This cannot be undone.`,
confirmText: "Delete Server",
cancelText: "Cancel",
isDanger: true,
onConfirm: async () => {
if (server) {
chat.handleDeleteServer(server.id);
onClose();
}
}
}
};
}}
>
<i class="fas fa-trash-alt"></i>
Delete Server
</Button>
};
}}
>
<i class="fas fa-trash-alt"></i>
Delete Server
</Button>
{/if}
</div>
<div class="settings-main">
<div class="settings-content">
@@ -192,7 +196,7 @@
</div>
{/if}
</div>
{#if isOwner}
{#if canManageServer}
<label class="btn primary small" style="margin-top: 12px; cursor: pointer;">
Change Avatar
<input type="file" accept="image/*" onchange={handleAvatarChange} disabled={isUploading} style="display: none;" />
@@ -205,14 +209,14 @@
id="server-name"
label="Server Name"
bind:value={serverName}
disabled={!isOwner || isUploading}
disabled={!canManageServer || isUploading}
/>
<Switch
label="Public Server"
description="Whether this server is visible in the Discovery tab."
bind:checked={isPublic}
disabled={!isOwner || isUploading}
disabled={!canManageServer || isUploading}
style="margin-top: 24px;"
/>
</div>
@@ -1,12 +1,15 @@
<script lang="ts">
import { getContext } from "svelte";
import type { ChatService } from "../../services/chat.svelte";
import { Permissions } from "../../services/chat.svelte";
const chat = getContext<ChatService>("chat");
let showServerDropdown = $state(false);
const isOwner = true; // temporarily enabled for everyone
const canManageServer = $derived(chat.can(Permissions.MANAGE_SERVER));
const canManageChannels = $derived(chat.can(Permissions.MANAGE_CHANNELS));
const canCreateInvites = $derived(chat.can(Permissions.CREATE_INVITES));
function handleOpenSettings() {
chat.showServerSettings = true;
@@ -31,20 +34,22 @@
{#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 canCreateInvites}
<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}
{#if isOwner}
{#if canManageServer || canManageChannels}
<button
class="server-dropdown-item"
onclick={handleOpenSettings}
@@ -1,6 +1,7 @@
<script lang="ts">
import { getContext } from "svelte";
import type { ChatService } from "../../services/chat.svelte";
import { Permissions } from "../../services/chat.svelte";
const chat = getContext<ChatService>("chat");
</script>
@@ -8,17 +9,19 @@
<div class="channel-section">
<div class="section-header">
<span>TEXT CHANNELS</span>
<button
class="add-btn"
onclick={() => {
chat.isVoiceChannel = false;
chat.showCreateChannelModal = true;
}}
title="Create Text Channel"
aria-label="Create Text Channel"
>
<i class="fas fa-plus"></i>
</button>
{#if chat.can(Permissions.MANAGE_CHANNELS)}
<button
class="add-btn"
onclick={() => {
chat.isVoiceChannel = false;
chat.showCreateChannelModal = true;
}}
title="Create Text Channel"
aria-label="Create Text Channel"
>
<i class="fas fa-plus"></i>
</button>
{/if}
</div>
{#each chat.textChannels as channel (channel.id.toString())}
{@const messageCount = chat.synchronizedMessages.filter(m => m.channelId === channel.id && !m.threadId).length}
@@ -2,6 +2,7 @@
import { getContext, onMount } from "svelte";
import { portal } from "../../../portal";
import type { ChatService } from "../../services/chat.svelte";
import { Permissions } from "../../services/chat.svelte";
import type { WebRTCService } from "../../services/webrtc/webrtc.svelte";
import Avatar from "../Avatar.svelte";
@@ -11,6 +12,8 @@
let hoveredPeer = $state<string | null>(null);
let popoverPos = $state<{ x: number; y: number } | null>(null);
const canManageChannels = $derived(chat.can(Permissions.MANAGE_CHANNELS));
const getStatusColor = (
status: string | undefined,
): "green" | "yellow" | "red" => {
@@ -75,17 +78,19 @@
<div class="channel-section">
<div class="section-header">
<span>VOICE CHANNELS</span>
<button
class="add-btn"
onclick={() => {
chat.isVoiceChannel = true;
chat.showCreateChannelModal = true;
}}
title="Create Voice Channel"
aria-label="Create Voice Channel"
>
<i class="fas fa-plus"></i>
</button>
{#if canManageChannels}
<button
class="add-btn"
onclick={() => {
chat.isVoiceChannel = true;
chat.showCreateChannelModal = true;
}}
title="Create Voice Channel"
aria-label="Create Voice Channel"
>
<i class="fas fa-plus"></i>
</button>
{/if}
</div>
{#each chat.voiceChannels as channel (channel.id.toString())}
{@const messageCount = chat.synchronizedMessages.filter(m => m.channelId === channel.id && !m.threadId).length}
+33
View File
@@ -16,6 +16,23 @@ import { EncryptionService } from "./encryption.svelte";
import { MediaCacheService } from "./media-cache.svelte";
import { sounds } from "./sound.svelte";
export const Permissions = {
MANAGE_SERVER: 0x01n,
MANAGE_ROLES: 0x02n,
MANAGE_CHANNELS: 0x04n,
CREATE_INVITES: 0x08n,
KICK_MEMBERS: 0x10n,
BAN_MEMBERS: 0x20n,
MODERATE_MESSAGES: 0x40n,
USE_VOICE: 0x80n,
SHARE_SCREEN: 0x100n,
USE_THREADS: 0x200n,
MANAGE_EMOJIS: 0x400n,
DELETE_SERVER: 0x800n,
} as const;
export type PermissionBit = typeof Permissions[keyof typeof Permissions];
export class ChatService {
#db: DatabaseService;
#nav: NavigationService;
@@ -200,6 +217,22 @@ export class ChatService {
return this.#nav.availableServers;
}
get myPermissions() {
const myId = this.identity;
const serverId = this.activeServerId;
if (!myId || !serverId) return 0n;
return this.#db.serverPermissions.find(p =>
p.serverId === serverId && p.identity.isEqual(myId)
)?.permissions || 0n;
}
can(bit: PermissionBit | bigint | undefined | null): boolean {
if (bit === undefined || bit === null) return false;
const b = typeof bit === "bigint" ? bit : BigInt(bit);
return (this.myPermissions & b) !== 0n;
}
// Facade Getters/Setters for UI
get showCreateServerModal() {
return this.#ui.showCreateServerModal;
+3
View File
@@ -16,6 +16,7 @@ export class DatabaseService {
directMessages = $state<readonly Types.DirectMessage[]>([]);
users = $state<readonly Types.User[]>([]);
serverMembers = $state<readonly Types.ServerMember[]>([]);
serverPermissions = $state<readonly Types.ServerPermission[]>([]);
threadMessages = $state<readonly Types.Message[]>([]);
recentMessages = $state<readonly Types.Message[]>([]);
@@ -114,6 +115,7 @@ export class DatabaseService {
const [usersStore, usersReadyStore] = useTable(tables.user);
const [userStatesStore] = useTable(tables.visible_user_states);
const [serverMembersStore, membersReadyStore] = useTable(tables.visible_server_members);
const [serverPermissionsStore] = useTable(tables.visible_server_permissions);
const [recentMessagesStore] = useTable(tables.visible_recent_activity);
const [threadMessagesStore] = useTable(tables.visible_thread_messages);
const [imagesStore, imagesReadyStore] = useTable(tables.visible_images);
@@ -133,6 +135,7 @@ export class DatabaseService {
usersReadyStore.subscribe((v) => (this.isUsersReady = v));
userStatesStore.subscribe((v) => (this.userStates = v));
serverMembersStore.subscribe((v) => (this.serverMembers = v));
serverPermissionsStore.subscribe((v) => (this.serverPermissions = v));
membersReadyStore.subscribe((v) => (this.isMembersReady = v));
recentMessagesStore.subscribe((v) => (this.recentMessages = v));
threadMessagesStore.subscribe((v) => (this.threadMessages = v));
+1 -1
View File
@@ -5,7 +5,7 @@ import { reducers, tables } from "../../module_bindings";
import * as Types from "../../module_bindings/types";
import { getConnection } from "../../config";
import { untrack } from "svelte";
import { SvelteMap, SvelteSet } from "svelte/reactivity";
import { SvelteSet } from "svelte/reactivity";
import type { Identity } from "spacetimedb";
export class MessagingService {